import assert from "node:assert/strict";
import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { spawn } from "node:child_process";
import net from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { createWranglerCommand, preparePagesRunner, terminateChildTree } from "../../scripts/run-cloudflare-pages.mjs";

function waitForMessage(child) {
  return new Promise((resolve, reject) => {
    let output = "";
    const timeout = setTimeout(() => reject(new Error("Timed out waiting for fixture child.")), 10_000);
    child.stdout.on("data", (chunk) => {
      output += chunk;
      const newline = output.indexOf("\n");
      if (newline === -1) return;
      clearTimeout(timeout);
      resolve(JSON.parse(output.slice(0, newline)));
    });
    child.once("error", reject);
  });
}

async function canConnect(port) {
  return await new Promise((resolve) => {
    const socket = net.connect({ host: "127.0.0.1", port });
    socket.once("connect", () => {
      socket.destroy();
      resolve(true);
    });
    socket.once("error", () => resolve(false));
  });
}

async function waitFor(condition, description) {
  const deadline = Date.now() + 10_000;
  while (Date.now() < deadline) {
    if (await condition()) return;
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
  throw new Error(`Timed out waiting for ${description}.`);
}

test("prepares an isolated Pages runner without inheriting Worker deploy state", async () => {
  const root = await mkdtemp(join(tmpdir(), "hearbooks-pages-runner-"));
  const project = join(root, "project");
  const workspace = join(root, "runner-workspace");
  try {
    await mkdir(join(project, "pages"), { recursive: true });
    await mkdir(join(project, "dist", "pages"), { recursive: true });
    await mkdir(join(project, "drizzle"), { recursive: true });
    await mkdir(join(project, ".wrangler", "deploy"), { recursive: true });
    await writeFile(join(project, "pages", "wrangler.jsonc"), "{\\\"name\\\":\\\"hearbooks-reader-pages\\\"}");
    await writeFile(join(project, "dist", "pages", "_worker.js"), "export default {};");
    await writeFile(join(project, "drizzle", "0000.sql"), "SELECT 1;");
    await writeFile(join(project, ".wrangler", "deploy", "config.json"), "{\\\"configPath\\\":\\\"stale\\\"}");

    const runner = await preparePagesRunner({ projectDirectory: project, workspaceDirectory: workspace });

    assert.equal(await readFile(join(runner.runnerDirectory, "wrangler.jsonc"), "utf8"), "{\\\"name\\\":\\\"hearbooks-reader-pages\\\"}");
    assert.equal(await readFile(join(runner.outputDirectory, "_worker.js"), "utf8"), "export default {};");
    assert.equal(await readFile(join(runner.migrationsDirectory, "0000.sql"), "utf8"), "SELECT 1;");
    await assert.rejects(access(join(runner.runnerDirectory, ".wrangler", "deploy", "config.json")));
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("starts Wrangler through Node instead of a space-sensitive Windows shell", () => {
  const command = createWranglerCommand({ projectDirectory: "C:/hearbooks", platform: "win32" });
  assert.equal(command.executable, process.execPath);
  assert.match(command.arguments[0], /node_modules[\\\\/]wrangler[\\\\/]wrangler-dist[\\\\/]cli\.js$/);
  assert.equal(command.options.shell, undefined);
});

test("Windows tree cleanup releases a descendant listener without touching an unrelated Node process", { skip: process.platform !== "win32" }, async () => {
  const grandchildProgram = [
    'import http from "node:http";',
    'const server = http.createServer((_, response) => response.end("fixture"));',
    'server.listen(0, "127.0.0.1", () => process.stdout.write(JSON.stringify({ port: server.address().port }) + "\\n"));',
    'setInterval(() => {}, 1_000);',
  ].join("");
  const parentProgram = [
    'import { spawn } from "node:child_process";',
    `const child = spawn(process.execPath, ["--input-type=module", "-e", ${JSON.stringify(grandchildProgram)}], { stdio: ["ignore", "pipe", "inherit"] });`,
    'child.stdout.once("data", (chunk) => { const { port } = JSON.parse(chunk); process.stdout.write(JSON.stringify({ descendantPid: child.pid, port }) + "\\n"); });',
    'setInterval(() => {}, 1_000);',
  ].join("");
  const runner = spawn(process.execPath, ["--input-type=module", "-e", parentProgram], {
    stdio: ["ignore", "pipe", "inherit"],
  });
  const unrelated = spawn(process.execPath, ["--input-type=module", "-e", "setInterval(() => {}, 1_000)"], {
    stdio: "ignore",
  });
  try {
    const { descendantPid, port } = await waitForMessage(runner);
    assert.equal(await canConnect(port), true);

    await terminateChildTree(runner);

    await waitFor(async () => !(await canConnect(port)), "the descendant listener to close");
    assert.throws(() => process.kill(descendantPid, 0), { code: "ESRCH" });
    assert.doesNotThrow(() => process.kill(unrelated.pid, 0));
  } finally {
    if (runner.exitCode === null) await terminateChildTree(runner);
    if (unrelated.exitCode === null) unrelated.kill();
  }
});

test("ignores Pages-only generated Wrangler state", async () => {
  const repositoryGitignore = fileURLToPath(new URL("../../.gitignore", import.meta.url));
  const content = await readFile(repositoryGitignore, "utf8");
  assert.match(content, /^\/pages\/\.wrangler\/$/m);
});
