import assert from "node:assert/strict";
import { strToU8, zipSync } from "fflate";
import { prepareChaptersForSync } from "../lib/client/import/prepare.ts";
import { parseEpub } from "../lib/client/parsers/epub.ts";
import { parsePdf } from "../lib/client/parsers/pdf.ts";

const baseUrl = process.env.HEARBOOKS_URL ?? "http://localhost:3000";
const liveTts = process.argv.includes("--live-tts");

function sampleEpub() {
  return zipSync({
    mimetype: strToU8("application/epub+zip"),
    "META-INF/container.xml": strToU8(`<?xml version="1.0"?>
      <container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
        <rootfiles><rootfile full-path="OPS/package.opf" media-type="application/oebps-package+xml"/></rootfiles>
      </container>`),
    "OPS/package.opf": strToU8(`<?xml version="1.0"?>
      <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="id">
        <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
          <dc:identifier id="id">urn:hearbooks:smoke</dc:identifier>
          <dc:title>听阅验收书 / HearBooks Smoke Book</dc:title>
          <dc:creator>HearBooks</dc:creator>
        </metadata>
        <manifest>
          <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
          <item id="c1" href="chapter.xhtml" media-type="application/xhtml+xml"/>
        </manifest>
        <spine><itemref idref="c1"/></spine>
      </package>`),
    "OPS/nav.xhtml": strToU8(`<!doctype html><html xmlns="http://www.w3.org/1999/xhtml"><body>
      <nav epub:type="toc" xmlns:epub="http://www.idpf.org/2007/ops"><ol><li><a href="chapter.xhtml">中英短章</a></li></ol></nav>
    </body></html>`),
    "OPS/chapter.xhtml": strToU8(`<!doctype html><html xmlns="http://www.w3.org/1999/xhtml"><body>
      <h1>中英短章</h1><p>你好，这是中文朗读验收句。</p><p>Hello, this is the English playback smoke test.</p>
      <table><tr><td>应跳过</td><td>42</td></tr></table><p>E = mc² + ∑ αᵢ。</p>
    </body></html>`),
  }, { level: 0 });
}

function samplePdf(text) {
  const escaped = text.replaceAll("\\", "\\\\").replaceAll("(", "\\(").replaceAll(")", "\\)");
  const stream = text ? `BT /F1 18 Tf 72 720 Td (${escaped}) Tj ET` : "";
  const objects = [
    "<< /Type /Catalog /Pages 2 0 R >>",
    "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
    "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
    "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
    `<< /Length ${new TextEncoder().encode(stream).byteLength} >>\nstream\n${stream}\nendstream`,
    "<< /Title (HearBooks PDF) /Author (HearBooks) >>",
  ];
  let pdf = "%PDF-1.4\n";
  const offsets = [0];
  for (let index = 0; index < objects.length; index += 1) {
    offsets.push(new TextEncoder().encode(pdf).byteLength);
    pdf += `${index + 1} 0 obj\n${objects[index]}\nendobj\n`;
  }
  const xrefOffset = new TextEncoder().encode(pdf).byteLength;
  pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
  for (const offset of offsets.slice(1)) {
    pdf += `${String(offset).padStart(10, "0")} 00000 n \n`;
  }
  pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R /Info 6 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
  return new TextEncoder().encode(pdf);
}

async function loadPdfForNode(bytes) {
  const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
  return pdfjs.getDocument({
    data: bytes.slice(),
    useSystemFonts: true,
  }).promise;
}

async function request(path, init = {}, expectedStatus = 200) {
  const response = await fetch(new URL(path, baseUrl), init);
  const text = await response.text();
  let body = null;
  if (text) {
    try {
      body = JSON.parse(text);
    } catch {
      body = { raw: text };
    }
  }
  if (response.status !== expectedStatus) {
    throw new Error(`${init.method ?? "GET"} ${path} returned ${response.status}: ${JSON.stringify(body)}`);
  }
  if (response.status !== 204 && (!body || "raw" in body)) {
    throw new Error(`${init.method ?? "GET"} ${path} did not return JSON: ${JSON.stringify(body)}`);
  }
  return body;
}

const bytes = sampleEpub();
let uploadId = null;
let bookId = null;

try {
  const textPdf = await parsePdf(
    samplePdf("Readable text PDF with enough words for direct extraction and listening."),
    loadPdfForNode,
  );
  assert.equal(textPdf.title, "HearBooks PDF");
  assert.equal(textPdf.requiresOcr, false);
  assert.ok(textPdf.chapters[0].blocks.some((block) => block.text.includes("Readable text PDF")));

  const scannedPdf = await parsePdf(samplePdf(""), loadPdfForNode);
  assert.equal(scannedPdf.requiresOcr, true);
  assert.equal(scannedPdf.pageCount, 1);

  const parsed = await parseEpub(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
  assert.equal(parsed.title, "听阅验收书 / HearBooks Smoke Book");
  assert.equal(parsed.chapters.length, 1);
  assert.equal(parsed.chapters[0].blocks.filter((block) => !block.included).length, 2);

  const created = await request("/api/uploads", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      fileName: "hearbooks-smoke.epub",
      mimeType: "application/epub+zip",
      totalBytes: bytes.byteLength,
    }),
  }, 201);
  uploadId = created.upload.id;
  assert.equal(created.upload.partCount, 1);

  await request(`/api/uploads/${uploadId}/parts/1`, {
    method: "PUT",
    headers: { "content-type": "application/octet-stream" },
    body: bytes,
  });
  const completed = await request(`/api/uploads/${uploadId}/complete`, { method: "POST" }, 201);
  bookId = completed.book.id;

  await request(`/api/books/${bookId}`, {
    method: "PATCH",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ title: parsed.title, author: parsed.author }),
  });

  const prepared = await prepareChaptersForSync(bookId, parsed);
  await request(`/api/books/${bookId}/chapters`, {
    method: "PUT",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ chapters: prepared }),
  });
  await request(`/api/books/${bookId}/chapters/finalize`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ chapterIds: prepared.map((chapter) => chapter.id) }),
  });

  const listed = await request(`/api/books/${bookId}/chapters`);
  assert.equal(listed.chapters.length, 1);
  const included = listed.chapters[0].segments.filter((segment) => segment.included);
  assert.ok(included.some((segment) => segment.language === "zh-CN"));
  assert.ok(included.some((segment) => segment.language === "en-US"));
  assert.ok(listed.chapters[0].segments.some((segment) => !segment.included));

  const current = included[0];
  const updatedAt = new Date().toISOString();
  await request(`/api/books/${bookId}/progress`, {
    method: "PUT",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      chapterId: listed.chapters[0].id,
      segmentId: current.id,
      offsetMs: 1250,
      playbackRate: 1.2,
      updatedAt,
    }),
  });
  const progress = await request(`/api/books/${bookId}/progress`);
  assert.equal(progress.progress.offsetMs, 1250);
  assert.equal(progress.progress.playbackRate, 1.2);

  if (liveTts) {
    const voice = current.language === "zh-CN" ? "zh-CN-XiaoxiaoNeural" : "en-US-AriaNeural";
    const description = await request(`/api/segments/${encodeURIComponent(current.id)}/audio?describe=1&voice=${voice}`);
    const { EdgeTTS } = await import("edge-tts-universal");
    const generated = await new EdgeTTS(description.segment.text, voice).synthesize();
    assert.ok(generated.audio.size > 0);
    const uploaded = await fetch(new URL(`/api/segments/${encodeURIComponent(current.id)}/audio`, baseUrl), {
      method: "PUT",
      headers: {
        "content-type": "audio/mpeg",
        "x-text-version": String(description.segment.textVersion),
        "x-voice": voice,
      },
      body: generated.audio,
    });
    assert.equal(uploaded.status, 201, await uploaded.text());
    const streamed = await fetch(new URL(`/api/segments/${encodeURIComponent(current.id)}/audio?voice=${voice}`, baseUrl), {
      headers: { range: "bytes=0-99" },
    });
    assert.equal(streamed.status, 206);
    assert.match(streamed.headers.get("content-type") ?? "", /^audio\/mpeg/);
    assert.ok((await streamed.arrayBuffer()).byteLength > 0);
  }

  console.log(`Smoke test passed: real text/scanned PDF classification, bilingual EPUB import, sync, progress${liveTts ? ", live Edge TTS, and ranged audio" : ""}.`);
} finally {
  if (bookId) {
    await request(`/api/books/${bookId}`, {
      method: "DELETE",
      headers: { "x-confirm-delete": "DELETE" },
    }, 204).catch((error) => console.warn(`Cleanup warning: ${error.message}`));
  } else if (uploadId) {
    await request(`/api/uploads/${uploadId}`, { method: "DELETE" })
      .catch((error) => console.warn(`Upload cleanup warning: ${error.message}`));
  }
}
