import { spawn } from "node:child_process";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";

type JsonRecord = Record<string, unknown>;

interface MockRequest {
  method: string;
  url: string;
  body: string;
}

function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
  if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}

function runBun(args: string[], env: Record<string, string> = {}): Promise<{ status: number | null; stdout: string; stderr: string; json: JsonRecord | null }> {
  return new Promise((resolve, reject) => {
    const child = spawn("bun", args, {
      cwd: process.cwd(),
      env: { ...process.env, ...env },
    });
    const stdoutChunks: Buffer[] = [];
    const stderrChunks: Buffer[] = [];
    child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
    child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
    child.on("error", reject);
    child.on("close", (status) => {
      const stdout = Buffer.concat(stdoutChunks).toString("utf8");
      let json: JsonRecord | null = null;
      try {
        json = JSON.parse(stdout) as JsonRecord;
      } catch {
        json = null;
      }
      resolve({
        status,
        stdout,
        stderr: Buffer.concat(stderrChunks).toString("utf8"),
        json,
      });
    });
  });
}

function runCli(args: string[], env: Record<string, string> = {}): Promise<{ status: number | null; stdout: string; stderr: string; json: JsonRecord | null }> {
  return runBun(["scripts/cli.ts", ...args], env);
}

function collectBody(req: IncomingMessage): Promise<string> {
  return new Promise((resolve) => {
    const chunks: Buffer[] = [];
    req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
    req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
  });
}

function sendJson(res: ServerResponse, status: number, payload: unknown): void {
  res.statusCode = status;
  res.setHeader("content-type", "application/json");
  res.end(JSON.stringify(payload));
}

function prFixture(): JsonRecord {
  return {
    id: 4200,
    number: 42,
    title: "CLI summary fixture",
    body: "fixture body",
    state: "open",
    html_url: "https://github.example/pikasTech/unidesk/pull/42",
    draft: false,
    user: { login: "runner" },
    head: { ref: "feature/pr-files", sha: "abc123" },
    base: { ref: "master", sha: "def456" },
    additions: 12,
    deletions: 3,
    changed_files: 2,
    commits: 1,
    created_at: "2026-05-23T00:00:00Z",
    updated_at: "2026-05-23T00:10:00Z",
  };
}

function prFilesFixture(): JsonRecord[] {
  return [
    {
      sha: "aaa",
      filename: "scripts/src/gh.ts",
      status: "modified",
      additions: 10,
      deletions: 2,
      changes: 12,
      blob_url: "https://github.example/blob/scripts/src/gh.ts",
      raw_url: "https://github.example/raw/scripts/src/gh.ts",
      contents_url: "https://api.github.example/contents/scripts/src/gh.ts",
      patch: "@@ raw diff must not be returned @@",
    },
    {
      sha: "bbb",
      filename: "scripts/gh-cli-pr-files-contract-test.ts",
      status: "added",
      additions: 2,
      deletions: 1,
      changes: 3,
      blob_url: "https://github.example/blob/scripts/gh-cli-pr-files-contract-test.ts",
      raw_url: "https://github.example/raw/scripts/gh-cli-pr-files-contract-test.ts",
      contents_url: "https://api.github.example/contents/scripts/gh-cli-pr-files-contract-test.ts",
      patch: "@@ raw diff must not be returned either @@",
    },
  ];
}

async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockRequest[]; close: () => Promise<void> }> {
  const requests: MockRequest[] = [];
  const server = createServer(async (req, res) => {
    const body = await collectBody(req);
    requests.push({ method: req.method ?? "", url: req.url ?? "", body });
    const url = new URL(req.url ?? "/", "http://localhost");
    if (req.method === "GET" && url.pathname === "/repos/pikasTech/unidesk/pulls/42") {
      sendJson(res, 200, prFixture());
      return;
    }
    if (req.method === "GET" && url.pathname === "/repos/pikasTech/unidesk/pulls/42/files") {
      const perPage = Number(url.searchParams.get("per_page") ?? "30");
      const page = Number(url.searchParams.get("page") ?? "1");
      const offset = (page - 1) * perPage;
      sendJson(res, 200, prFilesFixture().slice(offset, offset + perPage));
      return;
    }
    sendJson(res, 404, { message: `unexpected ${req.method} ${req.url}` });
  });
  await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
  const address = server.address();
  assertCondition(typeof address === "object" && address !== null, "mock server should expose address");
  const port = (address as AddressInfo).port;
  assertCondition(typeof port === "number", "mock server should expose port");
  return {
    baseUrl: `http://127.0.0.1:${port}`,
    requests,
    close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())),
  };
}

function dataOf(response: JsonRecord): JsonRecord {
  assertCondition(response.ok === true, "CLI command should succeed", response);
  assertCondition(typeof response.data === "object" && response.data !== null && !Array.isArray(response.data), "response data should be object", response);
  return response.data as JsonRecord;
}

function failedDataOf(response: JsonRecord): JsonRecord {
  assertCondition(response.ok === false, "CLI command should fail", response);
  assertCondition(typeof response.data === "object" && response.data !== null && !Array.isArray(response.data), "failure data should be object", response);
  return response.data as JsonRecord;
}

export async function runGhCliPrFilesContract(): Promise<JsonRecord> {
  const help = await runCli(["gh", "help"]);
  assertCondition(help.status === 0, "gh help should succeed", help.json ?? { stdout: help.stdout });
  const helpData = dataOf(help.json ?? {});
  const usage = Array.isArray(helpData.usage) ? helpData.usage.map((value) => String(value)) : [];
  const notes = Array.isArray(helpData.notes) ? helpData.notes.map((value) => String(value)) : [];
  assertCondition(usage.some((line) => line.includes("gh pr files <number>")), "help should document gh pr files", { usage });
  assertCondition(usage.some((line) => line.includes("gh pr diff <number> --stat")), "help should document gh pr diff --stat", { usage });
  assertCondition(notes.some((line) => line.includes("PR files is the canonical compact changed-file/stat summary")), "help should document PR files disclosure boundary", { notes });

  const mock = await startMockGitHub();
  const env = {
    GH_TOKEN: "contract-token",
    UNIDESK_GITHUB_API_URL: mock.baseUrl,
  };
  const checks: string[] = ["gh help documents pr files and pr diff --stat"];
  try {
    mock.requests.length = 0;
    const files = await runCli(["gh", "pr", "files", "42", "--repo", "pikasTech/unidesk", "--limit", "1"], env);
    assertCondition(files.status === 0, "gh pr files should succeed", files.json ?? { stdout: files.stdout, stderr: files.stderr });
    const filesData = dataOf(files.json ?? {});
    assertCondition(filesData.command === "pr files", "command name should be pr files", filesData);
    assertCondition(filesData.rawDiffIncluded === false, "rawDiffIncluded must be false", filesData);
    const summary = filesData.summary as JsonRecord;
    assertCondition(summary.files === 2, "summary must include total changed files", summary);
    assertCondition(summary.additions === 12, "summary additions should come from PR REST", summary);
    assertCondition(summary.deletions === 3, "summary deletions should come from PR REST", summary);
    assertCondition(summary.changes === 15, "summary changes should include additions plus deletions", summary);
    assertCondition(filesData.filesReturned === 1, "limit 1 should return one file", filesData);
    const fileRows = filesData.files as JsonRecord[];
    assertCondition(Array.isArray(fileRows) && fileRows.length === 1, "files list should be bounded", filesData);
    assertCondition(fileRows[0]?.filename === "scripts/src/gh.ts", "first file filename mismatch", fileRows[0]);
    assertCondition(fileRows[0]?.patch === undefined, "raw patch must not be emitted", fileRows[0]);
    assertCondition(fileRows[0]?.raw_url === undefined, "raw_url field casing must not leak", fileRows[0]);
    const truncation = filesData.truncation as JsonRecord;
    assertCondition(truncation.truncated === true, "limit 1 should mark truncation", truncation);
    assertCondition(truncation.totalFiles === 2, "truncation should expose totalFiles", truncation);
    const next = filesData.next as JsonRecord;
    assertCondition(String(next.command).includes("--limit 2"), "next command should request the bounded full file count", next);
    assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls/42/files?per_page=1&page=1"), "files endpoint should use bounded per_page", mock.requests);
    checks.push("gh pr files returns bounded REST file/stat JSON without raw patches");

    mock.requests.length = 0;
    const filesOptionsFirst = await runCli(["gh", "pr", "files", "--repo", "pikasTech/unidesk", "--limit", "1", "42"], env);
    assertCondition(filesOptionsFirst.status === 0, "gh pr files should accept PR number after options", filesOptionsFirst.json ?? { stdout: filesOptionsFirst.stdout, stderr: filesOptionsFirst.stderr });
    const filesOptionsFirstData = dataOf(filesOptionsFirst.json ?? {});
    assertCondition(filesOptionsFirstData.command === "pr files" && filesOptionsFirstData.filesReturned === 1, "options-first pr files should return compact file summary", filesOptionsFirstData);
    assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls/42/files?per_page=1&page=1"), "options-first pr files should query the requested PR number", mock.requests);
    checks.push("gh pr files accepts the PR number after options");

    mock.requests.length = 0;
    const stat = await runCli(["gh", "pr", "diff", "42", "--stat", "--repo", "pikasTech/unidesk", "--limit", "2"], env);
    assertCondition(stat.status === 0, "gh pr diff --stat should succeed", stat.json ?? { stdout: stat.stdout, stderr: stat.stderr });
    const statData = dataOf(stat.json ?? {});
    assertCondition(statData.command === "pr diff --stat", "diff --stat should report alias command name", statData);
    assertCondition(statData.rawDiffIncluded === false, "diff --stat must not include raw diff", statData);
    assertCondition(statData.filesReturned === 2, "diff --stat should return requested files", statData);
    const statRows = statData.files as JsonRecord[];
    assertCondition(statRows.every((file) => file.patch === undefined), "diff --stat file rows must not include patch", statRows);
    checks.push("gh pr diff --stat is a compact summary alias");

    mock.requests.length = 0;
    const statOptionsFirst = await runCli(["gh", "pr", "diff", "--repo", "pikasTech/unidesk", "--stat", "--limit", "2", "42"], env);
    assertCondition(statOptionsFirst.status === 0, "gh pr diff --stat should accept PR number after options", statOptionsFirst.json ?? { stdout: statOptionsFirst.stdout, stderr: statOptionsFirst.stderr });
    const statOptionsFirstData = dataOf(statOptionsFirst.json ?? {});
    assertCondition(statOptionsFirstData.command === "pr diff --stat" && statOptionsFirstData.filesReturned === 2, "options-first pr diff --stat should return compact file summary", statOptionsFirstData);
    assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls/42/files?per_page=2&page=1"), "options-first pr diff --stat should query the requested PR number", mock.requests);
    checks.push("gh pr diff --stat accepts the PR number after options");

    const rawDiff = await runCli(["gh", "pr", "diff", "42", "--repo", "pikasTech/unidesk"], env);
    assertCondition(rawDiff.status !== 0, "gh pr diff without --stat should fail closed", rawDiff.json ?? { stdout: rawDiff.stdout, stderr: rawDiff.stderr });
    const rawData = failedDataOf(rawDiff.json ?? {});
    assertCondition(rawData.degradedReason === "unsupported-command", "raw diff should fail as unsupported-command", rawData);
    assertCondition(rawData.rawDiffIncluded === false, "raw diff failure should state rawDiffIncluded=false", rawData);
    checks.push("gh pr diff without --stat fails closed without raw diff output");

    return { ok: true, checks };
  } finally {
    await mock.close();
  }
}

if (import.meta.main) {
  runGhCliPrFilesContract()
    .then((result) => console.log(JSON.stringify(result, null, 2)))
    .catch((error) => {
      console.error(error instanceof Error ? error.stack ?? error.message : String(error));
      process.exitCode = 1;
    });
}
