import assert from "node:assert/strict";
import test from "node:test";
import { ChapterRepository } from "../../lib/server/repositories/chapters.ts";
import { createTestD1 } from "../helpers/sqlite-d1.mjs";

function insertBook(database) {
  database.prepare(`
    INSERT INTO books (id, title, author, format, source_object_key, source_bytes, status)
    VALUES ('book-1', '测试书', '作者', 'pdf', 'sources/book-1/book.pdf', 100, 'processing')
  `).run();
}

const chapter = (overrides = {}) => ({
  id: "chapter-1",
  title: "第一章",
  position: 0,
  pageStart: 1,
  pageEnd: 2,
  segments: [
    {
      id: "segment-1",
      position: 0,
      text: "第一句。",
      language: "zh-CN",
      contentHash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      included: true,
      sourceKind: "pdf_text",
      pageNumber: 1,
      ocrConfidence: null,
      warning: null,
    },
    {
      id: "segment-2",
      position: 1,
      text: "Second sentence.",
      language: "en-US",
      contentHash: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      included: true,
      sourceKind: "pdf_text",
      pageNumber: 2,
      ocrConfidence: null,
      warning: null,
    },
  ],
  ...overrides,
});

test("upserts chapter batches, finalizes the book, and lists ordered structure", async () => {
  const { d1, database } = await createTestD1();
  insertBook(database);
  const repository = new ChapterRepository(d1);

  await repository.saveBatch("book-1", [
    chapter(),
    chapter({ id: "chapter-2", title: "第二章", position: 1, pageStart: 3, pageEnd: 3, segments: [
      { ...chapter().segments[0], id: "segment-3", text: "第三句。", contentHash: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", pageNumber: 3 },
    ] }),
  ]);
  await repository.finalize("book-1", ["chapter-1", "chapter-2"]);

  const result = await repository.list("book-1");
  assert.deepEqual(result.map(({ title }) => title), ["第一章", "第二章"]);
  assert.deepEqual(result[0].segments.map(({ textVersion }) => textVersion), [1, 1]);
  assert.equal(database.prepare("SELECT status FROM books WHERE id = 'book-1'").get().status, "ready");
});

test("safely replaces an occupied chapter position and moves merged segments", async () => {
  const { d1, database } = await createTestD1();
  insertBook(database);
  const repository = new ChapterRepository(d1);
  const secondSegment = {
    ...chapter().segments[0],
    id: "segment-3",
    text: "来自第二章。",
    contentHash: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
  };
  await repository.saveBatch("book-1", [
    chapter(),
    chapter({ id: "chapter-2", title: "第二章", position: 1, segments: [secondSegment] }),
  ]);

  await repository.saveBatch("book-1", [
    chapter({ title: "人工重命名章节", segments: [] }),
    chapter({
      id: "chapter-split",
      title: "拆分后章节",
      position: 1,
      segments: [
        { ...chapter().segments[0], id: "segment-new", position: 0 },
        { ...secondSegment, position: 1 },
      ],
    }),
  ]);
  await repository.finalize("book-1", ["chapter-1", "chapter-split"]);

  const result = await repository.list("book-1");
  assert.deepEqual(result.map(({ id, position }) => ({ id, position })), [
    { id: "chapter-1", position: 0 },
    { id: "chapter-split", position: 1 },
  ]);
  assert.deepEqual(result[1].segments.map(({ id, position }) => ({ id, position })), [
    { id: "segment-new", position: 0 },
    { id: "segment-3", position: 1 },
  ]);
});

test("safely swaps existing segment positions within one chapter", async () => {
  const { d1, database } = await createTestD1();
  insertBook(database);
  const repository = new ChapterRepository(d1);
  await repository.saveBatch("book-1", [chapter()]);

  await repository.saveBatch("book-1", [chapter({
    segments: [
      { ...chapter().segments[1], position: 0 },
      { ...chapter().segments[0], position: 1 },
    ],
  })]);

  const [result] = await repository.list("book-1");
  assert.deepEqual(result.segments.map(({ id, position }) => ({ id, position })), [
    { id: "segment-2", position: 0 },
    { id: "segment-1", position: 1 },
  ]);
});

test("increments only the edited segment version and removes only its stale audio", async () => {
  const { d1, database } = await createTestD1();
  insertBook(database);
  const repository = new ChapterRepository(d1);
  await repository.saveBatch("book-1", [chapter()]);
  database.exec(`
    INSERT INTO audio_segments (id, book_id, segment_id, text_version, voice, object_key, status)
      VALUES ('a1', 'book-1', 'segment-1', 1, 'voice', 'audio/a1.mp3', 'ready');
    INSERT INTO audio_segments (id, book_id, segment_id, text_version, voice, object_key, status)
      VALUES ('a2', 'book-1', 'segment-2', 1, 'voice', 'audio/a2.mp3', 'ready');
  `);

  const edited = chapter();
  edited.segments[0] = { ...edited.segments[0], text: "第一句已修改。", contentHash: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" };
  await repository.saveBatch("book-1", [edited]);

  const versions = database.prepare("SELECT id, text_version FROM segments ORDER BY id").all();
  assert.deepEqual(versions.map((row) => ({ ...row })), [
    { id: "segment-1", text_version: 2 },
    { id: "segment-2", text_version: 1 },
  ]);
  assert.deepEqual(database.prepare("SELECT id FROM audio_segments ORDER BY id").all().map((row) => row.id), ["a2"]);
});

test("invalidates audio and its storage ledger when a segment is skipped or removed", async () => {
  const { d1, database } = await createTestD1();
  insertBook(database);
  const repository = new ChapterRepository(d1);
  await repository.saveBatch("book-1", [chapter()]);
  database.exec(`
    INSERT INTO audio_segments (id, book_id, segment_id, text_version, voice, object_key, status)
      VALUES ('a1', 'book-1', 'segment-1', 1, 'voice', 'audio/a1.mp3', 'ready');
    INSERT INTO audio_segments (id, book_id, segment_id, text_version, voice, object_key, status)
      VALUES ('a2', 'book-1', 'segment-2', 1, 'voice', 'audio/a2.mp3', 'ready');
    INSERT INTO storage_ledger (object_key, kind, book_id, bytes)
      VALUES ('audio/a1.mp3', 'audio', 'book-1', 10);
    INSERT INTO storage_ledger (object_key, kind, book_id, bytes)
      VALUES ('audio/a2.mp3', 'audio', 'book-1', 10);
  `);

  const skipped = chapter();
  skipped.segments[0] = { ...skipped.segments[0], included: false };
  const firstResult = await repository.saveBatch("book-1", [skipped]);
  assert.deepEqual(firstResult.invalidatedAudioKeys, ["audio/a1.mp3"]);
  assert.equal(database.prepare("SELECT text_version FROM segments WHERE id = 'segment-1'").get().text_version, 2);
  assert.deepEqual(database.prepare("SELECT object_key FROM storage_ledger ORDER BY object_key").all().map((row) => row.object_key), ["audio/a2.mp3"]);

  const removed = chapter({ segments: [skipped.segments[0]] });
  const secondResult = await repository.saveBatch("book-1", [removed]);
  assert.deepEqual(secondResult.invalidatedAudioKeys, ["audio/a2.mp3"]);
  assert.equal(database.prepare("SELECT COUNT(*) count FROM storage_ledger WHERE kind = 'audio'").get().count, 0);
});

test("deletes removed segments and chapters while preserving unrelated chapters", async () => {
  const { d1, database } = await createTestD1();
  insertBook(database);
  const repository = new ChapterRepository(d1);
  await repository.saveBatch("book-1", [
    chapter(),
    chapter({ id: "chapter-2", title: "第二章", position: 1, segments: [{ ...chapter().segments[0], id: "segment-3" }] }),
  ]);

  await repository.saveBatch("book-1", [chapter({ segments: [chapter().segments[0]] })]);
  await repository.finalize("book-1", ["chapter-1"]);

  assert.deepEqual(database.prepare("SELECT id FROM chapters ORDER BY id").all().map((row) => row.id), ["chapter-1"]);
  assert.deepEqual(database.prepare("SELECT id FROM segments ORDER BY id").all().map((row) => row.id), ["segment-1"]);
});

test("enforces small request batches, unique positions, and ownership", async () => {
  const { d1, database } = await createTestD1();
  insertBook(database);
  const repository = new ChapterRepository(d1);
  await assert.rejects(repository.saveBatch("missing", [chapter()]), /找不到这本书/);
  await assert.rejects(repository.saveBatch("book-1", []), /至少包含一个章节/);
  await assert.rejects(repository.saveBatch("book-1", Array.from({ length: 11 }, (_, index) => chapter({ id: `c${index}`, position: index }))), /最多保存 10 个章节/);
  await assert.rejects(repository.saveBatch("book-1", [chapter(), chapter({ id: "c2", position: 0 })]), /章节顺序不能重复/);
});

test("keeps every D1 statement within the free-plan 100 binding limit", async () => {
  const { d1, database } = await createTestD1({ maxBindings: 100 });
  insertBook(database);
  const repository = new ChapterRepository(d1);
  const segments = Array.from({ length: 50 }, (_, index) => ({
    ...chapter().segments[0],
    id: `segment-${index}`,
    position: index,
    text: `第 ${index} 句。`,
    contentHash: index.toString(16).padStart(64, "0"),
  }));
  await repository.saveBatch("book-1", [chapter({ segments })]);
  assert.equal(database.prepare("SELECT COUNT(*) count FROM segments").get().count, 50);
});
