import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";
import { createServer } from "node:net";

const packageRoot = resolve(process.env.DADA_POSTV1_PACKAGE_ROOT ?? ".build/portable-release/Dada-P0A-0.0.0-win-x64");
const port = 43121;

async function waitForHealth(child) {
  const deadline = Date.now() + 15_000;
  while (Date.now() < deadline) {
    if (child.exitCode !== null) throw new Error(`packaged api exited: ${child.exitCode}: ${child.errorOutput ?? ""}`);
    try {
      const response = await fetch(`http://127.0.0.1:${port}/healthz`, { headers: { host: `127.0.0.1:${port}` } });
      if (response.ok) return;
    } catch {}
    await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
  }
  throw new Error("packaged api health timeout");
}

function startApi(configPath, dataRoot) {
  const child = spawn(join(packageRoot, "runtime", "node.exe"), [join(packageRoot, "server", "api.mjs"), "--dada-credential-stdin"], {
    cwd: packageRoot,
    env: { ...process.env, DADA_INSTANCE_CONFIG_PATH: configPath, DADA_SUPPORT_GATE_ROOT: join(packageRoot, "web", "support-gate"), DADA_WEB_ROOT: join(packageRoot, "web") },
    stdio: ["pipe", "ignore", "pipe"],
    windowsHide: true,
  });
  child.errorOutput = "";
  child.stderr.setEncoding("utf8");
  child.stderr.on("data", (chunk) => { child.errorOutput += chunk; });
  child.stdin.end(JSON.stringify({ "Dada/P0A/api/amap": "", "Dada/P0A/api/resend": "", "Dada/P0A/admin/pepper": "portable-test-pepper-00000000000000000000000000000000" }));
  return child;
}

async function stop(child) {
  if (child.exitCode === null) {
    child.kill();
    await new Promise((resolveExit) => child.once("exit", resolveExit));
  }
}

async function verifyWorkerStartup(configPath) {
  const pipeName = `Dada.P0A.PostV1.${process.pid}.${Date.now()}`;
  const pipePath = `\\\\.\\pipe\\${pipeName}`;
  const server = createServer();
  await new Promise((resolveListen, rejectListen) => {
    server.once("error", rejectListen);
    server.listen(pipePath, resolveListen);
  });
  const child = spawn(join(packageRoot, "runtime", "node.exe"), [
    join(packageRoot, "server", "worker.mjs"),
    "--dada-control-pipe", pipeName,
    "--dada-credential-stdin",
  ], {
    cwd: packageRoot,
    env: {
      ...process.env,
      DADA_INSTANCE_CONFIG_PATH: configPath,
      DADA_SQLITE_NATIVE_BINDING: join(packageRoot, "server", "native", "better_sqlite3.node"),
    },
    stdio: ["pipe", "ignore", "ignore"],
    windowsHide: true,
  });
  child.stdin.end(JSON.stringify({ "Dada/P0A/worker/ai-gateway": "synthetic-runtime-token" }));
  try {
    await new Promise((resolveReady, rejectReady) => {
      let settled = false;
      const finish = (error) => {
        if (settled) return;
        settled = true;
        clearTimeout(deadline);
        child.off("exit", onExit);
        if (error) rejectReady(error); else resolveReady();
      };
      const deadline = setTimeout(() => finish(new Error("packaged worker ready timeout")), 15_000);
      const onExit = (code) => finish(new Error(`packaged worker exited before ready: ${code}`));
      child.once("exit", onExit);
      server.once("connection", (connection) => {
        connection.setEncoding("utf8");
        let pending = "";
        connection.on("data", (chunk) => {
          pending += chunk;
          while (pending.includes("\n")) {
            const newline = pending.indexOf("\n");
            const status = pending.slice(0, newline).trim();
            pending = pending.slice(newline + 1);
            if (status === "storage_unavailable") finish(new Error("packaged worker reported storage_unavailable"));
            if (status === "ready") {
              setTimeout(() => {
                if (settled) return;
                connection.write("shutdown\n");
                finish();
              }, 500);
            }
          }
        });
      });
    });
    const exitCode = await new Promise((resolveExit) => child.once("exit", resolveExit));
    assert.equal(exitCode, 0);
  } finally {
    if (child.exitCode === null) child.kill();
    await new Promise((resolveClose) => server.close(resolveClose));
  }
}

test("portable package serves the product and keeps SQLite data across API restart", async () => {
  assert.ok(existsSync(join(packageRoot, "Dada.exe")));
  assert.ok(existsSync(join(packageRoot, "web", "index.html")));
  const runtimeAssetManifest = JSON.parse(await readFile(join(packageRoot, "asset-metadata", "manifest.json"), "utf8"));
  assert.deepEqual(runtimeAssetManifest.counts, {
    dynamic_fonts: 7,
    dynamic_images: 8,
    font_panel_items: 11,
    static_stickers: 1407,
  });
  assert.equal(runtimeAssetManifest.entries.length, 1433);
  const packagedWorker = await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8");
  const packagedOneApiAdapter = await readFile(join(packageRoot, "server", "worker", "dist", "oneapi-generation-adapter.js"), "utf8");
  assert.match(packagedWorker, /GenerationProcessor/);
  assert.match(packagedWorker, /OneApiGenerationAdapter/);
  assert.match(packagedOneApiAdapter, /oneapi\.intelligrow\.cn/);
  assert.doesNotMatch(packagedWorker, /portable-mock-worker/);
  const root = await mkdtemp(join(tmpdir(), "dada-postv1-"));
  const dataRoot = join(root, "data");
  const configPath = join(root, "instance.json");
  await writeFile(configPath, JSON.stringify({ data_root: dataRoot, initialized: true, instance_id: "portable-test", schema_version: 1, secure_config_revision: 1, admin_allowlist_hashes: [], admin_recovery_hashes: [] }));
  let api = startApi(configPath, dataRoot);
  try {
    await waitForHealth(api);
    const initialPage = await fetch(`http://127.0.0.1:${port}/`, { headers: { host: `127.0.0.1:${port}` } });
    assert.equal(initialPage.status, 200);
    assert.match(await initialPage.text(), /当前浏览器无法使用 Dada/);
    const support = await fetch(`http://127.0.0.1:${port}/api/v1/support/check`, {
      method: "POST",
      headers: { host: `127.0.0.1:${port}`, origin: `http://127.0.0.1:${port}`, "content-type": "application/json", "sec-ch-ua": '"Google Chrome";v="150"', "sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"', "sec-ch-ua-platform": '"Windows"' },
      body: JSON.stringify({ brands: [{ brand: "Google Chrome", version: "150" }], full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }], platform: "Windows" }),
    });
    assert.equal(support.status, 200);
    const cookie = support.headers.get("set-cookie")?.split(";", 1)[0];
    assert.ok(cookie);
    const page = await fetch(`http://127.0.0.1:${port}/app`, { headers: { host: `127.0.0.1:${port}`, cookie, "sec-ch-ua": '"Google Chrome";v="150"' } });
    assert.equal(page.status, 200);
    const pageHtml = await page.text();
    assert.match(pageHtml, /<div id="root"><\/div>/);
    const scriptPath = pageHtml.match(/<script[^>]+src="([^"]+)"/)?.[1];
    const stylesheetPath = pageHtml.match(/<link[^>]+href="([^"]+)"/)?.[1];
    assert.ok(scriptPath);
    assert.ok(stylesheetPath);
    const assetHeaders = { host: `127.0.0.1:${port}`, cookie, "sec-ch-ua": '"Google Chrome";v="150"' };
    const script = await fetch(`http://127.0.0.1:${port}${scriptPath}`, { headers: assetHeaders });
    const stylesheet = await fetch(`http://127.0.0.1:${port}${stylesheetPath}`, { headers: assetHeaders });
    assert.equal(script.status, 200);
    assert.match(script.headers.get("content-type") ?? "", /^(?:application|text)\/javascript\b/);
    assert.equal(stylesheet.status, 200);
    assert.match(stylesheet.headers.get("content-type") ?? "", /^text\/css\b/);
    assert.ok(existsSync(join(dataRoot, "db", "dada.sqlite3")));
    await verifyWorkerStartup(configPath);
  } finally {
    await stop(api);
  }
  api = startApi(configPath, dataRoot);
  try {
    await waitForHealth(api);
    assert.ok(existsSync(join(dataRoot, "db", "dada.sqlite3")));
  } finally {
    await stop(api);
    await rm(root, { recursive: true, force: true });
  }
});
