import assert from "node:assert/strict";
import test from "node:test";
import { BookRepository } from "../../lib/server/repositories/books.ts";
import { D1UploadSessionStore } from "../../lib/server/repositories/uploads.ts";
import { createTestD1 } from "../helpers/sqlite-d1.mjs";

const session = {
  id: "book-1",
  r2UploadId: "r2-secret",
  objectKey: "sources/book-1/The-Book.pdf",
  fileName: "The Book.pdf",
  mimeType: "application/pdf",
  format: "pdf",
  totalBytes: 12345,
  partSize: 8 * 1024 * 1024,
  partCount: 1,
  uploadedParts: [],
  status: "created",
  expiresAt: "2026-08-19T00:00:00.000Z",
  createdAt: "2026-08-12T00:00:00.000Z",
  updatedAt: "2026-08-12T00:00:00.000Z",
};

test("round-trips resumable upload sessions through D1", async () => {
  const { d1 } = await createTestD1();
  const store = new D1UploadSessionStore(d1);

  await store.create(session);
  assert.deepEqual(await store.get(session.id), session);

  const updated = {
    ...session,
    status: "uploading",
    uploadedParts: [{ partNumber: 1, etag: "etag-1" }],
    updatedAt: "2026-08-12T00:05:00.000Z",
  };
  await store.save(updated);
  assert.deepEqual(await store.get(session.id), updated);
  assert.equal(await store.get("missing"), null);
});

test("idempotently registers a completed source as a book and storage object", async () => {
  const { d1, database } = await createTestD1();
  const books = new BookRepository(d1);
  const completed = { ...session, status: "complete" };

  assert.deepEqual(await books.registerCompletedUpload(completed), {
    id: "book-1",
    title: "The Book",
    author: "未知作者",
    format: "pdf",
    sourceBytes: 12345,
    coverUrl: null,
    progress: null,
    status: "processing",
    statusDetail: null,
    preferredZhVoice: null,
    preferredEnVoice: null,
    preferredRate: null,
    lastListenedAt: null,
    createdAt: "2026-08-12T00:00:00.000Z",
    updatedAt: "2026-08-12T00:00:00.000Z",
  });
  await books.registerCompletedUpload(completed);

  assert.equal(database.prepare("SELECT COUNT(*) count FROM books").get().count, 1);
  assert.deepEqual(
    { ...database.prepare("SELECT object_key, kind, book_id, bytes FROM storage_ledger").get() },
    {
      object_key: session.objectKey,
      kind: "source",
      book_id: "book-1",
      bytes: 12345,
    },
  );
});

test("lists by latest activity, escapes wildcard searches, and updates metadata", async () => {
  const { d1, database } = await createTestD1();
  const books = new BookRepository(d1);
  for (const [id, title, author, createdAt, lastListenedAt] of [
    ["b1", "A 100% Story", "Alice", "2026-08-01T00:00:00.000Z", null],
    ["b2", "第二本书", "王明", "2026-08-02T00:00:00.000Z", "2026-08-10T00:00:00.000Z"],
    ["b3", "A_Manual", "Bob", "2026-08-03T00:00:00.000Z", "2026-08-11T00:00:00.000Z"],
  ]) {
    database.prepare(`
      INSERT INTO books
        (id, title, author, format, source_object_key, source_bytes, status, created_at, updated_at, last_listened_at)
      VALUES (?, ?, ?, 'epub', ?, 10, 'ready', ?, ?, ?)
    `).run(id, title, author, `sources/${id}/book.epub`, createdAt, createdAt, lastListenedAt);
  }

  assert.deepEqual((await books.list()).map((book) => book.id), ["b3", "b2", "b1"]);
  assert.deepEqual((await books.list("100%")).map((book) => book.id), ["b1"]);
  assert.deepEqual((await books.list("_")).map((book) => book.id), ["b3"]);
  assert.deepEqual((await books.list("王明")).map((book) => book.id), ["b2"]);

  const updated = await books.updateMetadata("b1", {
    title: "  New title  ",
    author: "  New author ",
    preferredRate: 1.5,
  });
  assert.equal(updated.title, "New title");
  assert.equal(updated.author, "New author");
  assert.equal(updated.preferredRate, 1.5);
  await assert.rejects(books.updateMetadata("b1", { title: " " }), /书名不能为空/);
});

test("shows a book's current chapter and approximate listening percentage", async () => {
  const { d1, database } = await createTestD1();
  const books = new BookRepository(d1);
  await books.registerCompletedUpload({ ...session, status: "complete" });
  database.exec(`
    INSERT INTO chapters (id, book_id, title, position) VALUES
      ('c1', 'book-1', 'First', 0), ('c2', 'book-1', 'Second', 1);
    INSERT INTO segments (id, chapter_id, position, text, language, content_hash) VALUES
      ('s1', 'c1', 0, 'One.', 'en-US', 'h1'),
      ('s2', 'c1', 1, 'Two.', 'en-US', 'h2'),
      ('s3', 'c2', 0, 'Three.', 'en-US', 'h3'),
      ('s4', 'c2', 1, 'Four.', 'en-US', 'h4');
    INSERT INTO playback_progress (book_id, chapter_id, segment_id, offset_ms, playback_rate, updated_at)
      VALUES ('book-1', 'c2', 's3', 250, 1.25, '2026-08-13T06:00:00.000Z');
  `);

  const book = await books.get("book-1");
  assert.deepEqual(book.progress, {
    chapterTitle: "Second",
    chapterPosition: 1,
    segmentPosition: 0,
    percent: 50,
    updatedAt: "2026-08-13T06:00:00.000Z",
  });
});

test("replaces and removes cover metadata without leaving stale ledger rows", async () => {
  const { d1, database } = await createTestD1();
  const books = new BookRepository(d1);
  await books.registerCompletedUpload({ ...session, status: "complete" });
  const first = await books.setCover("book-1", {
    objectKey: "covers/book-1/one.jpg", bytes: 100, now: "2026-08-13T06:00:00.000Z",
  });
  assert.equal(first.previousObjectKey, null);
  assert.match(first.book.coverUrl, /\/api\/books\/book-1\/cover\?v=/);

  const second = await books.setCover("book-1", {
    objectKey: "covers/book-1/two.png", bytes: 200, now: "2026-08-13T06:01:00.000Z",
  });
  assert.equal(second.previousObjectKey, "covers/book-1/one.jpg");
  assert.deepEqual(database.prepare("SELECT object_key, bytes FROM storage_ledger WHERE kind = 'cover'").all().map((row) => ({ ...row })), [
    { object_key: "covers/book-1/two.png", bytes: 200 },
  ]);

  const removed = await books.removeCover("book-1", "2026-08-13T06:02:00.000Z");
  assert.equal(removed.previousObjectKey, "covers/book-1/two.png");
  assert.equal(removed.book.coverUrl, null);
  assert.equal((await books.cover("book-1")).objectKey, null);
  assert.equal(await books.cover("missing"), null);
});

test("returns every R2 key before deleting a book and its D1 dependants", async () => {
  const { d1, database } = await createTestD1();
  const books = new BookRepository(d1);
  await books.registerCompletedUpload({ ...session, status: "complete" });
  database.prepare("INSERT INTO storage_ledger (object_key, kind, book_id, bytes) VALUES (?, 'cover', ?, 50)")
    .run("covers/book-1.jpg", "book-1");

  assert.deepEqual(await books.delete("book-1"), [
    "covers/book-1.jpg",
    session.objectKey,
  ]);
  assert.equal(database.prepare("SELECT COUNT(*) count FROM books").get().count, 0);
  await assert.rejects(books.delete("missing"), /找不到这本书/);
});
