import assert from "node:assert/strict";
import { spawn, type ChildProcess } from "node:child_process";
import fs from "node:fs/promises";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { after, test } from "node:test";

const dataDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "qiyouxue-article-ssr-"));
const buildLock = path.join(dataDirectory, "site-build.lock");
const contentLock = path.join(dataDirectory, ".content-write.lock");
process.env.CMS_DATA_DIR = dataDirectory;
process.env.CMS_BUILD_LOCK = buildLock;
process.env.CMS_CONTENT_LOCK = contentLock;
process.env.CMS_CONTENT_LOCK_STALE_MS = "30000";
process.env.CMS_API_KEY = "article-ssr-test-key";
process.env.CMS_SECRET = "article-ssr-test-secret";
process.env.CMS_PASSWORD = "article-ssr-test-password";
await fs.writeFile(buildLock, "deployment-build-lock-sentinel\n");

const [{ handleCmsApi }, articleStore, { categoryToSlug }, { withContentWriteLock }] = await Promise.all([
  import("../src/lib/cms-api.ts"),
  import("../src/lib/article-store.ts"),
  import("../src/lib/articles.ts"),
  import("../src/lib/content-lock.ts"),
]);

function apiRequest(route: string, method = "GET", body?: Record<string, unknown>): Promise<Response> {
  return handleCmsApi(new Request(`http://localhost/api/cms/${route}`, {
    method,
    headers: { Authorization: `Bearer ${process.env.CMS_API_KEY}`, ...(body ? { "Content-Type": "application/json" } : {}) },
    body: body ? JSON.stringify(body) : undefined,
  }), route);
}

async function availablePort(): Promise<number> {
  const server = net.createServer();
  await new Promise<void>((resolve, reject) => server.once("error", reject).listen(0, "127.0.0.1", resolve));
  const address = server.address();
  const port = typeof address === "object" && address ? address.port : 0;
  await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
  return port;
}

let devServer: ChildProcess | null = null;
let serverOutput = "";
const port = await availablePort();
const origin = `http://127.0.0.1:${port}`;

async function startDevServer(): Promise<void> {
  const astro = path.join(process.cwd(), "node_modules", "astro", "astro.js");
  devServer = spawn(process.execPath, [astro, "dev", "--host", "127.0.0.1", "--port", String(port)], {
    cwd: process.cwd(),
    env: { ...process.env },
    stdio: ["ignore", "pipe", "pipe"],
  });
  devServer.stdout?.on("data", (chunk) => { serverOutput += chunk.toString(); });
  devServer.stderr?.on("data", (chunk) => { serverOutput += chunk.toString(); });
  const deadline = Date.now() + 20_000;
  while (Date.now() < deadline) {
    if (devServer.exitCode !== null) throw new Error(`Astro dev server exited early:\n${serverOutput}`);
    try {
      const response = await fetch(`${origin}/articles/`);
      if (response.ok) return;
    } catch {}
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  throw new Error(`Astro dev server did not start:\n${serverOutput}`);
}

await startDevServer();

after(async () => {
  if (devServer && devServer.exitCode === null) {
    devServer.kill("SIGTERM");
    await new Promise((resolve) => devServer?.once("exit", resolve));
  }
  await fs.rm(dataDirectory, { recursive: true, force: true });
});

test("article lists read metadata without rendering every Markdown body", async () => {
  articleStore.resetArticleStoreDiagnostics();
  const articles = await articleStore.getPublishedArticles();
  assert.ok(articles.length > 0);
  assert.equal(articleStore.getArticleStoreDiagnostics().markdownRenderCount, 0);
  assert.equal("body" in articles[0], false);
});

test("drafts and invalid public article routes return real 404 responses", async () => {
  const created = await apiRequest("articles", "POST", {
    title: "SSR 即时发布验收文章",
    category: "学习方法",
    body: "# SSR 完整正文\n\n这是无需构建即可上线的唯一正文标记。",
  });
  assert.equal(created.status, 201);
  const result = await created.json() as { slug: string };
  assert.match(result.slug, /^[a-z0-9]+(?:-[a-z0-9]+)*$/);
  process.env.TEST_ARTICLE_SLUG = result.slug;

  for (const pathname of [
    `/articles/${result.slug}/`,
    "/articles/not_valid/",
    "/articles/does-not-exist/",
    "/articles/page/1/",
    "/articles/page/9999/",
    "/articles/topic/not-a-category/",
  ]) {
    const response = await fetch(`${origin}${pathname}`);
    assert.equal(response.status, 404, pathname);
  }
});

test("publishing is immediately visible in SSR HTML, lists, category, llms and sitemap without a build", async () => {
  const slug = process.env.TEST_ARTICLE_SLUG!;
  const published = await apiRequest(`articles/${slug}/publish`, "POST");
  assert.equal(published.status, 200);
  assert.deepEqual(await published.json(), { ok: true, publishStatus: "live", message: "发布成功，文章已上线" });

  const detail = await fetch(`${origin}/articles/${slug}/`);
  const detailHtml = await detail.text();
  assert.equal(detail.status, 200);
  assert.match(detail.headers.get("content-type") || "", /^text\/html/);
  assert.match(detailHtml, /<div class="article-prose"[^>]*>[\s\S]*<h1>SSR 完整正文<\/h1>/);
  assert.match(detailHtml, /<link rel="canonical" href="https:\/\/qiyouxueedu\.com\/articles\//);
  assert.match(detailHtml, /"@type":"Article"/);
  assert.match(detailHtml, /"@type":"BreadcrumbList"/);

  const listHtml = await (await fetch(`${origin}/articles/`)).text();
  assert.match(listHtml, /SSR 即时发布验收文章/);
  const categorySlug = categoryToSlug("学习方法");
  const category = await fetch(`${origin}/articles/topic/${categorySlug}/`);
  assert.equal(category.status, 200);
  assert.match(await category.text(), /SSR 即时发布验收文章/);

  const llms = await fetch(`${origin}/llms.txt`);
  assert.match(llms.headers.get("content-type") || "", /^text\/plain; charset=utf-8/);
  assert.match(await llms.text(), /SSR 即时发布验收文章/);

  const full = await fetch(`${origin}/llms-full.txt`);
  const etag = full.headers.get("etag");
  assert.ok(etag);
  assert.match(await full.text(), /这是无需构建即可上线的唯一正文标记/);
  const notModified = await fetch(`${origin}/llms-full.txt`, { headers: { "If-None-Match": etag! } });
  assert.equal(notModified.status, 304);

  const sitemap = await fetch(`${origin}/article-sitemap.xml`);
  assert.equal(sitemap.headers.get("content-type"), "application/xml; charset=utf-8");
  assert.match(await sitemap.text(), new RegExp(`/articles/${slug}/`));

  assert.equal(await fs.readFile(buildLock, "utf8"), "deployment-build-lock-sentinel\n");
  assert.equal((await apiRequest("build")).status, 404);
});

test("the content write lock returns 409 without changing the draft or acquiring the build lock", async () => {
  const slug = process.env.TEST_ARTICLE_SLUG!;
  const draftFile = path.join(dataDirectory, "articles", `${slug}.md`);
  const before = await fs.readFile(draftFile, "utf8");
  let releaseHold!: () => void;
  const held = withContentWriteLock("并发锁验收", () => new Promise<void>((resolve) => { releaseHold = resolve; }));
  const deadline = Date.now() + 3000;
  while (Date.now() < deadline) {
    try { await fs.access(contentLock); break; } catch { await new Promise((resolve) => setTimeout(resolve, 10)); }
  }
  const conflict = await apiRequest(`articles/${slug}`, "PUT", {
    title: "不应写入的并发修改",
    category: "学习方法",
    body: "不应写入",
  });
  assert.equal(conflict.status, 409);
  assert.match((await conflict.json() as { error: string }).error, /内容操作正在进行/);
  releaseHold();
  await held;
  assert.equal(await fs.readFile(draftFile, "utf8"), before);
  assert.equal(await fs.readFile(buildLock, "utf8"), "deployment-build-lock-sentinel\n");
});

test("a stale content lock is recovered without deleting a newer owner's lock", async () => {
  await fs.writeFile(contentLock, JSON.stringify({
    token: "stale-test-token",
    pid: 99999999,
    hostname: os.hostname(),
    createdAt: new Date(Date.now() - 60_000).toISOString(),
    operation: "已退出的写操作",
  }));
  let entered = false;
  await withContentWriteLock("陈旧锁恢复验收", async () => { entered = true; });
  assert.equal(entered, true);
  await assert.rejects(fs.access(contentLock), { code: "ENOENT" });
});

test("unpublishing and deleting immediately removes an article from every public entry", async () => {
  const slug = process.env.TEST_ARTICLE_SLUG!;
  const unpublished = await apiRequest(`articles/${slug}/unpublish`, "POST");
  assert.equal(unpublished.status, 200);
  assert.equal((await fetch(`${origin}/articles/${slug}/`)).status, 404);
  assert.doesNotMatch(await (await fetch(`${origin}/articles/`)).text(), /SSR 即时发布验收文章/);
  assert.doesNotMatch(await (await fetch(`${origin}/articles/topic/${categoryToSlug("学习方法")}/`)).text(), /SSR 即时发布验收文章/);
  assert.doesNotMatch(await (await fetch(`${origin}/llms.txt`)).text(), /SSR 即时发布验收文章/);
  assert.doesNotMatch(await (await fetch(`${origin}/llms-full.txt`)).text(), /这是无需构建即可上线的唯一正文标记/);
  assert.doesNotMatch(await (await fetch(`${origin}/article-sitemap.xml`)).text(), new RegExp(`/articles/${slug}/`));

  const deleted = await apiRequest(`articles/${slug}`, "DELETE");
  assert.equal(deleted.status, 200);
  assert.equal((await apiRequest(`articles/${slug}`)).status, 404);
  assert.equal(await fs.readFile(buildLock, "utf8"), "deployment-build-lock-sentinel\n");
});
