import assert from "node:assert/strict";
import test from "node:test";
import { createCoverApi } from "../../lib/server/cover-api.ts";

function setup(overrides = {}) {
  const objects = new Map();
  const deleted = [];
  const calls = [];
  const repository = {
    cover: async (id) => id === "book-1" ? { objectKey: null } : null,
    setCover: async (id, input) => {
      calls.push({ operation: "set", id, ...input });
      return { book: { id, coverUrl: `/api/books/${id}/cover?v=2` }, previousObjectKey: "covers/book-1/old.jpg" };
    },
    removeCover: async (id) => id === "book-1"
      ? { book: { id, coverUrl: null }, previousObjectKey: "covers/book-1/old.jpg" }
      : null,
    ...overrides.repository,
  };
  const bucket = {
    put: async (key, value, options) => objects.set(key, { value: new Uint8Array(value), options }),
    get: async (key) => objects.get(key) ? {
      body: objects.get(key).value,
      size: objects.get(key).value.byteLength,
      httpEtag: '"cover-etag"',
      httpMetadata: objects.get(key).options?.httpMetadata,
    } : null,
    delete: async (key) => deleted.push(key),
  };
  return {
    api: createCoverApi({
      repository,
      bucket,
      assertStorageAdmission: async (bytes) => calls.push({ operation: "admit", bytes }),
      idFactory: () => "new-cover",
      now: () => new Date("2026-08-13T06:00:00.000Z"),
    }),
    objects,
    deleted,
    calls,
  };
}

test("accepts a bounded image cover, records it, and removes the replaced object", async () => {
  const { api, objects, deleted, calls } = setup();
  const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1]);
  const response = await api.put(new Request("https://example.test", {
    method: "PUT",
    headers: { "content-type": "image/png" },
    body: png,
  }), "book-1");

  assert.equal(response.status, 201);
  assert.deepEqual(await response.json(), { book: { id: "book-1", coverUrl: "/api/books/book-1/cover?v=2" } });
  assert.ok(objects.has("covers/book-1/new-cover.png"));
  assert.deepEqual(deleted, ["covers/book-1/old.jpg"]);
  assert.deepEqual(calls[0], { operation: "admit", bytes: png.byteLength });
  assert.equal(calls[1].objectKey, "covers/book-1/new-cover.png");
  assert.equal(calls[1].bytes, png.byteLength);
});

test("streams the stored cover with its safe content type", async () => {
  const { api, objects } = setup({
    repository: { cover: async () => ({ objectKey: "covers/book-1/current.webp" }) },
  });
  const webp = new TextEncoder().encode("RIFF0000WEBPdata");
  objects.set("covers/book-1/current.webp", { value: webp, options: { httpMetadata: { contentType: "image/webp" } } });

  const response = await api.get("book-1");
  assert.equal(response.status, 200);
  assert.equal(response.headers.get("content-type"), "image/webp");
  assert.equal(response.headers.get("content-length"), String(webp.byteLength));
  assert.equal(response.headers.get("etag"), '"cover-etag"');
});

test("rejects unsupported or forged cover files before writing R2", async () => {
  const { api, objects } = setup();
  const forged = await api.put(new Request("https://example.test", {
    method: "PUT", headers: { "content-type": "image/png" }, body: new Uint8Array([1, 2, 3]),
  }), "book-1");
  assert.equal(forged.status, 415);
  assert.equal(objects.size, 0);

  const oversized = await api.put(new Request("https://example.test", {
    method: "PUT",
    headers: { "content-type": "image/jpeg", "content-length": String(6 * 1024 * 1024) },
    body: new Uint8Array([0xff, 0xd8, 0xff]),
  }), "book-1");
  assert.equal(oversized.status, 413);
});

test("removes a cover and reports missing books", async () => {
  const { api, deleted } = setup();
  const response = await api.delete("book-1");
  assert.equal(response.status, 200);
  assert.deepEqual(deleted, ["covers/book-1/old.jpg"]);
  assert.equal((await api.get("missing")).status, 404);
  assert.equal((await api.delete("missing")).status, 404);
});
