import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createGhciAdapter, type GhRunner } from "./ghci";

const FIXTURE_DIR = join(import.meta.dir, "../../test/fixtures/ghci");
const fixture = (name: string): string => readFileSync(join(FIXTURE_DIR, name), "utf8");

const HISTORY = fixture("snapshot-history.json");
const RUNNERS = fixture("snapshot-runners.json");
const OPEN = fixture("snapshot-open.json");
const MERGED = fixture("snapshot-merged.json");
const TRAIN_RUNS = fixture("snapshot-train-runs.json");
const JOBS = fixture("snapshot-jobs.json");
const TWO_TRAINS_OPEN = fixture("snapshot-open-two-trains.json");
const T2_JOBS = fixture("t2-jobs.json");
const T2_INFRA_LOG = fixture("t2-log-infra.txt");
const T2_CODE_LOG = fixture("t2-log-code.txt");
const T3_MERGED = fixture("t3-merged.json");
const T3_JOBS = fixture("t3-jobs.json");
const EMPTY_OPEN = JSON.stringify({
  data: {
    repository: {
      defaultBranchRef: { name: "main" },
      refs: { nodes: [], pageInfo: { hasNextPage: false } },
      pullRequests: { nodes: [], pageInfo: { hasNextPage: false } },
    },
  },
});

interface RunnerOptions {
  history?: string;
  open?: string;
  failRepo?: string;
  failMerged?: boolean;
  trains?: boolean;
  jobs?: boolean;
  failCall?: (args: string[]) => boolean;
}

function routingRunner(options: RunnerOptions = {}): { run: GhRunner; calls: string[][] } {
  const calls: string[][] = [];
  const run: GhRunner = async (args) => {
    calls.push(args);
    const joined = args.join(" ");
    if (options.failCall?.(args)) throw new Error("injected failure");
    if (options.failRepo && joined.includes(`/repos/${options.failRepo}/`)) throw new Error("boom");
    if (joined.includes("actions/runs?per_page=100&exclude_pull_requests=false")) {
      return options.history ?? HISTORY;
    }
    if (joined.includes("actions/runs?status=queued&per_page=1&exclude_pull_requests=false")) {
      return '{"total_count":2,"workflow_runs":[]}';
    }
    if (joined.includes("actions/runs?status=in_progress&per_page=1&exclude_pull_requests=false")) {
      return '{"total_count":1,"workflow_runs":[]}';
    }
    if (joined.includes("actions/runners?per_page=100")) return RUNNERS;
    if (joined.includes("actions/runs?branch=")) {
      if (options.trains === false) return '{"total_count":0,"workflow_runs":[]}';
      const oid = new URLSearchParams(joined.split("?")[1]).get("head_sha") ?? "oid-12";
      const parsed = JSON.parse(TRAIN_RUNS);
      parsed.workflow_runs[0].head_sha = oid;
      parsed.workflow_runs[0].id = oid.endsWith("9") ? 900 : 1200;
      return JSON.stringify(parsed);
    }
    if (joined.includes("/jobs?filter=latest&per_page=100")) return options.jobs === false ? '{"total_count":0,"jobs":[]}' : JOBS;
    if (joined.includes("/actions/jobs/") && joined.endsWith("/logs")) return "ENOTEMPTY";
    if (args[0] === "api" && args[1] === "graphql") {
      const query = args.find((arg) => arg.startsWith("query=")) ?? "";
      if (query.includes("CiMergedPrSnapshot")) {
        if (options.failMerged) throw new Error("merged failed");
        return MERGED;
      }
      if (query.includes("CiOpenRefsPage")) {
        return JSON.stringify({
          data: {
            repository: {
              refs: {
                nodes: [{ name: "integration/batch-train-99", target: { oid: "oid-99" } }],
                pageInfo: { hasNextPage: false, endCursor: null },
              },
            },
          },
        });
      }
      if (query.includes("CiOpenPrsPage")) {
        return JSON.stringify({
          data: {
            repository: {
              pullRequests: {
                nodes: [],
                pageInfo: { hasNextPage: false, endCursor: null },
              },
            },
          },
        });
      }
      return options.open ?? OPEN;
    }
    throw new Error(`unexpected gh call: ${joined}`);
  };
  return { run, calls };
}

function repos(result: Awaited<ReturnType<ReturnType<typeof createGhciAdapter>["poll"]>>) {
  return (result.panels[0]?.data as { repos: Array<Record<string, any>> }).repos;
}

function calledRepos(calls: string[][]): string[] {
  const found = new Set<string>();
  for (const args of calls) {
    const joined = args.join(" ");
    const rest = joined.match(/\/repos\/([^/]+\/[^/]+)\/actions\/runs\?per_page=100/);
    if (rest) found.add(rest[1]!);
  }
  return [...found];
}

function callRepo(args: string[]): string | null {
  const joined = args.join(" ");
  return joined.match(/\/repos\/([^/]+\/[^/]+)\/actions/)?.[1]
    ?? joined.match(/repo:([^ ]+) is:pr/)?.[1]
    ?? null;
}

describe("createGhciAdapter T1 snapshots", () => {
  test("uses pinned snapshot argv, OID identity, and independent completeness fields", async () => {
    const { run, calls } = routingRunner();
    const adapter = createGhciAdapter({ runGh: run, repos: ["acme/app"], now: () => Date.UTC(2026, 6, 19, 12) });
    const result = await adapter.poll();
    const row = repos(result)[0]!;

    expect(calls).toContainEqual(["api", "/repos/acme/app/actions/runs?per_page=100&exclude_pull_requests=false"]);
    expect(calls).toContainEqual(["api", "/repos/acme/app/actions/runs?status=queued&per_page=1&exclude_pull_requests=false"]);
    expect(calls).toContainEqual(["api", "/repos/acme/app/actions/runs?status=in_progress&per_page=1&exclude_pull_requests=false"]);
    expect(calls).toContainEqual(["api", "/repos/acme/app/actions/runners?per_page=100"]);
    expect(calls).toContainEqual(["api", "/repos/acme/app/actions/runs?branch=integration%2Fbatch-train-12&head_sha=oid-12&per_page=100&exclude_pull_requests=false"]);
    const openCall = calls.find((args) => args.some((arg) => arg.includes("CiOpenRepoSnapshot")))!;
    const query = openCall.find((arg) => arg.startsWith("query="))!;
    expect(query).toContain('refs(refPrefix:"refs/heads/", query:"integration/batch-train-", first:100, orderBy:{field:ALPHABETICAL,direction:ASC})');
    expect(query).toContain("target { oid }");
    expect(query).toContain("headRefOid");
    expect(calls.filter((args) => args.join(" ").includes("/git/refs"))).toHaveLength(0);
    expect(row.queueDepth).toBe(3);
    expect(row).toMatchObject({ queueComplete: true, historyComplete: true, trainRunsComplete: true, runnersComplete: true, refsComplete: true, prsComplete: true, trainsComplete: true });
    expect(row.prs[0].checks).toEqual([{ name: "lint", bucket: "pass" }, { name: "deploy", bucket: "pending" }]);
    expect(row.runs[0].headSha).toBe("history-sha");
  });

  test("keeps train evidence when GitHub returns a short partial history page", async () => {
    const history = JSON.stringify({ ...JSON.parse(HISTORY), total_count: 1217 });
    const { run } = routingRunner({ history });
    const row = repos(await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll())[0]!;
    expect(row).toMatchObject({ historyComplete: true, queueComplete: true, trainRunsComplete: true, refsComplete: true, prsComplete: true, trainsComplete: true });
    expect(row.degraded).not.toContain("pagination");
    expect(row.trains[0]).toMatchObject({ state: "green", actionEligible: true });
  });

  test("treats a full first history page as complete when total_count exceeds the page size", async () => {
    const template = JSON.parse(HISTORY).workflow_runs[0];
    const runs = Array.from({ length: 100 }, (_, index) => ({ ...template, id: 9000 + index }));
    const history = JSON.stringify({ total_count: 1282, workflow_runs: runs });
    const { run } = routingRunner({ history });
    const row = repos(await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll())[0]!;
    expect(row.historyComplete).toBe(true);
    expect(row.degraded).not.toContain("pagination");
  });

  test("serves persisted run history when the latest GitHub fetch fails", async () => {
    const store = {};
    const { run } = routingRunner({ history: HISTORY });
    const adapter = createGhciAdapter({ runGh: run, repos: ["acme/app"], runHistoryStore: store });
    expect(repos(await adapter.poll())[0]!.historyComplete).toBe(true);

    const failing: GhRunner = async (args) => {
      if (args.join(" ").includes("actions/runs?per_page=100&exclude_pull_requests=false")) {
        throw new Error("history failed");
      }
      return run(args);
    };
    const row = repos(await createGhciAdapter({
      runGh: failing,
      repos: ["acme/app"],
      runHistoryStore: store,
    }).poll())[0]!;
    expect(row.historyComplete).toBe(true);
    expect(row.runs[0]?.headSha).toBe("history-sha");
  });

  test("treats missing self-hosted runners as empty occupancy", async () => {
    const routed = routingRunner();
    const run: GhRunner = async (args) => {
      if (args.join(" ").includes("actions/runners?per_page=100")) {
        throw new Error("gh api failed (1): gh: Not Found (HTTP 404)");
      }
      return routed.run(args);
    };
    const row = repos(await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll())[0]!;
    expect(row).toMatchObject({ runnersComplete: true, runnersBusy: 0, runnersTotal: 0 });
    expect(row.degraded).not.toContain("pagination");
  });

  test("adds org-level runners to the fleet when repo runners are unavailable", async () => {
    const routed = routingRunner();
    const run: GhRunner = async (args) => {
      const joined = args.join(" ");
      if (joined.includes("/repos/acme/app/actions/runners")) {
        throw new Error("gh api failed (1): gh: Not Found (HTTP 404)");
      }
      if (joined.includes("/orgs/acme/actions/runners")) {
        routed.calls.push(args);
        return JSON.stringify({
          total_count: 1,
          runners: [{ id: 42, name: "platform-debian1", status: "online", busy: false }],
        });
      }
      return routed.run(args);
    };
    const result = await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll();
    const panel = result.panels[0]!.data as { runners: Array<{ id: number; name: string; scope: string }> };
    expect(panel.runners).toEqual([
      expect.objectContaining({ id: 42, name: "platform-debian1", scope: "org:acme" }),
    ]);
    expect(routed.calls.some((args) => args.join(" ").includes("/orgs/acme/actions/runners"))).toBe(true);
  });

  test("paginates train refs until the connection is complete", async () => {
    const open = JSON.parse(OPEN);
    open.data.repository.refs.pageInfo = { hasNextPage: true, endCursor: "refs-cursor-1" };
    const { run, calls } = routingRunner({ open: JSON.stringify(open) });
    const row = repos(await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll())[0]!;
    expect(row.refsComplete).toBe(true);
    expect(calls.some((args) => args.join(" ").includes("CiOpenRefsPage"))).toBe(true);
  });

  test("preserves simultaneous REST, ref, PR, and check pagination gaps", async () => {
    const open = JSON.parse(OPEN);
    open.data.repository.refs.pageInfo.hasNextPage = true;
    open.data.repository.pullRequests.pageInfo.hasNextPage = true;
    open.data.repository.pullRequests.nodes[0].commits.nodes[0].commit.statusCheckRollup.contexts.pageInfo.hasNextPage = true;
    const history = JSON.stringify({ ...JSON.parse(HISTORY), total_count: 1217 });
    const runners = JSON.parse(RUNNERS);
    runners.total_count = 101;
    const { run } = routingRunner({
      history,
      open: JSON.stringify(open),
      failCall: (args) => args.join(" ").includes("status=queued"),
    });
    const row = repos(await createGhciAdapter({
      runGh: async (args) => args.join(" ").includes("actions/runners") ? JSON.stringify(runners) : run(args),
      repos: ["acme/app"],
    }).poll())[0]!;

    expect(row).toMatchObject({
      queueDepth: null,
      queueComplete: false,
      historyComplete: true,
      runnersComplete: false,
      refsComplete: false,
      prsComplete: false,
      trainsComplete: false,
    });
    expect(row.prs[0].checksComplete).toBe(false);
    expect(row.degraded).toEqual(["pagination"]);
    expect(row.trains[0]).toMatchObject({ state: null, actionEligible: false });
  });

  test("keeps ref and PR completeness independent on a malformed sibling connection", async () => {
    const open = JSON.parse(OPEN);
    delete open.data.repository.pullRequests;
    const { run, calls } = routingRunner({ open: JSON.stringify(open) });
    const row = repos(await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll())[0]!;
    expect(row).toMatchObject({ refsComplete: false, prsComplete: false, trainRunsComplete: false, trainsComplete: false });
    expect(calls.some((args) => args.join(" ").includes("actions/runs?branch="))).toBe(false);
    expect(calls.some((args) => args.join(" ").includes("/jobs?"))).toBe(false);
  });

  test("rejects exact-run responses containing a different head SHA", async () => {
    const routed = routingRunner();
    const run: GhRunner = async (args) => {
      if (args.join(" ").includes("actions/runs?branch=")) {
        const response = JSON.parse(TRAIN_RUNS);
        response.workflow_runs[0].head_sha = "obsolete-oid";
        routed.calls.push(args);
        return JSON.stringify(response);
      }
      return routed.run(args);
    };
    const row = repos(await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll())[0]!;
    expect(row).toMatchObject({ trainRunsComplete: false, trainsComplete: false });
    expect(row.trains[0]).toMatchObject({ state: null, actionEligible: false });
  });

  test("serves cached snapshot for repos outside the admitted batch", async () => {
    const configured = ["a/one", "a/two", "a/three", "a/four", "a/five", "a/six", "a/seven"];
    const { run } = routingRunner({ open: EMPTY_OPEN, trains: false, jobs: false });
    const adapter = createGhciAdapter({ runGh: run, repos: configured });
    const first = repos(await adapter.poll());
    const second = repos(await adapter.poll());
    const deferred = first.find((row) => row.repo === "a/seven")!;
    expect(deferred.pollDeferred).toBe(true);
    expect(deferred.degraded).not.toContain("budget");
    expect(deferred.trains).toEqual(second.find((row) => row.repo === "a/seven")!.trains);
  });

  test("isolates early repo failures and advances rotation", async () => {
    const { run, calls } = routingRunner({ failRepo: "a/one" });
    const adapter = createGhciAdapter({ runGh: run, repos: ["a/one", "a/two", "a/three", "a/four", "a/five", "a/six"] });
    const first = repos(await adapter.poll());
    const firstCount = calls.length;
    const second = repos(await adapter.poll());
    expect(calledRepos(calls.slice(0, firstCount))).toEqual(["a/one", "a/two", "a/three", "a/four", "a/five", "a/six"]);
    expect(calledRepos(calls.slice(firstCount))).toEqual(["a/one", "a/two", "a/three", "a/four", "a/five", "a/six"]);
    expect(first.find((row) => row.repo === "a/two")?.queueComplete).toBe(true);
    expect(first.find((row) => row.repo === "a/six")?.pollDeferred).toBe(false);
    expect(second.find((row) => row.repo === "a/five")?.pollDeferred).toBe(false);
    expect(second.find((row) => row.repo === "a/five")?.runs.length).toBeGreaterThan(0);
    expect(second.find((row) => row.repo === "a/five")?.trains[0]).toMatchObject({ state: "green", actionEligible: true });
  });

  test.each([
    [10, [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 1, 2]]],
    [11, [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 1]]],
    [21, [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12], [13, 14, 15, 16, 17, 18], [19, 20, 21, 1, 2, 3], [4, 5, 6, 7, 8, 9]]],
  ] as const)("rotates %i configured repos in exact batches", async (count, expected) => {
    const configured = Array.from({ length: count }, (_, index) => `o/r${index + 1}`);
    const { run, calls } = routingRunner({ open: EMPTY_OPEN, trains: false, jobs: false });
    const adapter = createGhciAdapter({ runGh: run, repos: configured });
    let offset = 0;
    const admitted = new Set<string>();
    for (const batch of expected) {
      await adapter.poll();
      const pollCalls = calls.slice(offset);
      offset = calls.length;
      expect(pollCalls.length).toBeLessThanOrEqual(52);
      const actual = calledRepos(pollCalls);
      expect(actual).toEqual(batch.map((n) => `o/r${n}`));
      actual.forEach((repo) => admitted.add(repo));
    }
    expect(admitted.size).toBe(count);
  });

  test("latches the tenth successful poll and uses an inclusive UTC seven-day query", async () => {
    const { run, calls } = routingRunner({ open: EMPTY_OPEN, trains: false, jobs: false });
    const adapter = createGhciAdapter({ runGh: run, repos: ["acme/app"], now: () => Date.UTC(2026, 6, 19, 23, 59) });
    for (let i = 0; i < 10; i += 1) await adapter.poll();
    const merged = calls.filter((args) => args.some((arg) => arg.includes("CiMergedPrSnapshot")));
    expect(merged).toHaveLength(1);
    expect(merged[0]?.some((arg) => arg.includes('query:"repo:acme/app is:pr is:merged merged:2026-07-13..2026-07-19"'))).toBe(true);
  });

  test("retains a failed merged-fetch latch until a later successful admitted poll", async () => {
    let failMerged = true;
    const routed = routingRunner({ open: EMPTY_OPEN, trains: false, jobs: false });
    const run: GhRunner = async (args) => {
      if (args.some((arg) => arg.includes("CiMergedPrSnapshot")) && failMerged) throw new Error("merged failed");
      return routed.run(args);
    };
    const adapter = createGhciAdapter({ runGh: run, repos: ["acme/app"] });
    for (let i = 0; i < 10; i += 1) await adapter.poll();
    failMerged = false;
    await adapter.poll();
    expect(routed.calls.filter((args) => args.some((arg) => arg.includes("CiMergedPrSnapshot")))).toHaveLength(1);
  });

  test("finishes every fixed and exact query before jobs or logs and stays within the poll budget", async () => {
    const configured = ["o/r1", "o/r2", "o/r3", "o/r4", "o/r5"];
    const { run, calls } = routingRunner();
    await createGhciAdapter({ runGh: run, repos: configured }).poll();
    expect(calls.length).toBeLessThanOrEqual(52);
    const firstJob = calls.findIndex((args) => args.join(" ").includes("/jobs?"));
    const lastExact = calls.findLastIndex((args) => args.join(" ").includes("actions/runs?branch="));
    expect(firstJob).toBeGreaterThan(lastExact);
    for (const repo of configured) {
      expect(calls.filter((args) => args.join(" ").includes(`/repos/${repo}/actions/runs?`) && !args.join(" ").includes("branch="))).toHaveLength(3);
      expect(calls.some((args) => args.join(" ").includes(`/repos/${repo}/actions/runners?`))).toBe(true);
      expect(calls.some((args) => args.includes(`owner=${repo.split("/")[0]}`) && args.some((arg) => arg.includes("CiOpenRepoSnapshot")))).toBe(true);
      expect(calls.some((args) => args.join(" ").includes(`/repos/${repo}/actions/runs?branch=`))).toBe(true);
    }
  });

  test("pins ordinary, mixed-due, and all-due five-repo arithmetic", async () => {
    const configured = ["o/r1", "o/r2", "o/r3", "o/r4", "o/r5"];

    const ordinary = routingRunner({ open: TWO_TRAINS_OPEN });
    await createGhciAdapter({ runGh: ordinary.run, repos: configured }).poll();
    expect(ordinary.calls).toHaveLength(47);
    expect(ordinary.calls.filter((args) => args.join(" ").includes("actions/runs?branch="))).toHaveLength(10);
    expect(ordinary.calls.filter((args) => args.join(" ").includes("/jobs?"))).toHaveLength(10);
    expect(ordinary.calls.filter((args) => args.join(" ").endsWith("/logs"))).toHaveLength(1);

    let warmMixed = true;
    const mixed = routingRunner({
      open: TWO_TRAINS_OPEN,
      failCall: (args) => warmMixed && ["o/r2", "o/r4"].includes(callRepo(args) ?? "") && args.join(" ").includes("status=queued"),
    });
    const mixedAdapter = createGhciAdapter({ runGh: mixed.run, repos: configured });
    for (let poll = 0; poll < 9; poll += 1) await mixedAdapter.poll();
    warmMixed = false;
    const mixedStart = mixed.calls.length;
    await mixedAdapter.poll();
    const mixedPoll = mixed.calls.slice(mixedStart);
    expect(mixedPoll).toHaveLength(49);
    expect(mixedPoll.filter((args) => args.some((arg) => arg.includes("CiMergedPrSnapshot"))).map(callRepo)).toEqual(["o/r1", "o/r3", "o/r5"]);
    expect(mixedPoll.filter((args) => args.join(" ").includes("/jobs?"))).toHaveLength(10);

    const allDue = routingRunner({ open: TWO_TRAINS_OPEN });
    const allDueAdapter = createGhciAdapter({ runGh: allDue.run, repos: configured });
    for (let poll = 0; poll < 9; poll += 1) await allDueAdapter.poll();
    const allDueStart = allDue.calls.length;
    const allDueResult = await allDueAdapter.poll();
    const allDuePoll = allDue.calls.slice(allDueStart);
    expect(allDuePoll).toHaveLength(51);
    expect(allDuePoll.filter((args) => args.some((arg) => arg.includes("CiMergedPrSnapshot")))).toHaveLength(5);
    expect(allDuePoll.filter((args) => args.join(" ").includes("/jobs?"))).toHaveLength(10);
    expect(allDuePoll.filter((args) => args.join(" ").endsWith("/logs"))).toHaveLength(0);
    expect(repos(allDueResult).every((row) => row.trains.filter((train: any) => train.state !== "merged").every((train: any) => train.gateRun.jobsComplete === true))).toBe(true);
  });

  test("rotates constrained job admission after dispatch failure", async () => {
    const configured = ["o/r1", "o/r2", "o/r3", "o/r4", "o/r5"];
    let warm = true;
    const routed = routingRunner({
      open: TWO_TRAINS_OPEN,
      failCall: (args) => {
        const repo = callRepo(args);
        if (warm) return repo === "o/r5" && args.join(" ").includes("status=queued");
        if (args.some((arg) => arg.includes("CiMergedPrSnapshot"))) return repo !== "o/r5";
        return repo === "o/r1" && args.join(" ").includes("/jobs?");
      },
    });
    const adapter = createGhciAdapter({ runGh: routed.run, repos: configured });
    for (let poll = 0; poll < 9; poll += 1) await adapter.poll();
    warm = false;
    const start = routed.calls.length;
    await adapter.poll();
    const jobs = routed.calls.slice(start).filter((args) => args.join(" ").includes("/jobs?"));
    expect(jobs).toHaveLength(10);
    expect([...new Set(jobs.map((args) => callRepo(args)))].sort()).toEqual([...configured].sort());
  });

  test("rotates constrained log admission independently", async () => {
    const configured = ["o/r1", "o/r2", "o/r3", "o/r4", "o/r5"];
    let warm = true;
    let jobGeneration = 0;
    const routed = routingRunner({
      failCall: (args) => {
        const repo = callRepo(args);
        if (warm) return repo === "o/r5" && args.join(" ").includes("status=queued");
        return args.some((arg) => arg.includes("CiMergedPrSnapshot")) && repo !== "o/r5";
      },
    });
    const run: GhRunner = async (args) => {
      if (args.join(" ").includes("/jobs?")) {
        const response = JSON.parse(JOBS);
        response.jobs[0].id = ++jobGeneration;
        routed.calls.push(args);
        return JSON.stringify(response);
      }
      return routed.run(args);
    };
    const adapter = createGhciAdapter({ runGh: run, repos: configured });
    for (let poll = 0; poll < 9; poll += 1) await adapter.poll();
    warm = false;
    const start = routed.calls.length;
    await adapter.poll();
    const logs = routed.calls.slice(start).filter((args) => args.join(" ").endsWith("/logs"));
    expect(logs).toHaveLength(5);
    expect([...new Set(logs.map((args) => callRepo(args)))].sort()).toEqual([...configured].sort());
  });

  test("preserves stable run-failure item identity", async () => {
    const { run } = routingRunner();
    const result = await createGhciAdapter({ runGh: run, repos: ["acme/app"] }).poll();
    expect(result.items.map((item) => item.id)).toContain("ghci:acme/app:9001:1");
  });
});

interface T2Scenario {
  history?: string;
  refs?: Array<{ name: string; target: { oid: string } }>;
  prs?: Array<Record<string, unknown>>;
  runs?: Record<string, Record<string, unknown>[]>;
  merged?: unknown[];
  jobs?: Record<number, string>;
  logs?: Record<number, string>;
  runners?: Record<string, unknown>[];
  refsNext?: boolean;
  prsNext?: boolean;
  exactTotal?: number;
  mergedTotal?: number;
  mergedNext?: boolean;
  failMerged?: boolean;
  failLogs?: number[];
}

function trainPr(number: number, branch: string, oid: string, overrides: Record<string, unknown> = {}) {
  return {
    number,
    title: `Train ${number}`,
    body: "Train-Members: #8 #5\n- #5 legacy\n- #3 fallback",
    url: `https://github.com/acme/app/pull/${number}`,
    headRefName: branch,
    headRefOid: oid,
    baseRefName: "main",
    createdAt: `2026-07-19T${String(number).padStart(2, "0")}:00:00Z`,
    mergeable: "MERGEABLE",
    mergeStateStatus: "CLEAN",
    commits: { nodes: [{ commit: { statusCheckRollup: null } }] },
    ...overrides,
  };
}

function ghRun(id: number, branch: string, oid: string, status: string, conclusion: string | null, attempt = 1, createdAt = "2026-07-19T10:00:00Z", updatedAt = createdAt) {
  return {
    id,
    run_attempt: attempt,
    run_number: id,
    name: "train gate",
    head_branch: branch,
    head_sha: oid,
    display_title: branch,
    status,
    conclusion,
    workflow_id: 1,
    html_url: `https://github.com/acme/app/actions/runs/${id}`,
    created_at: createdAt,
    updated_at: updatedAt,
  };
}

function t2Runner(scenario: T2Scenario): { run: GhRunner; calls: string[][] } {
  const calls: string[][] = [];
  const run: GhRunner = async (args) => {
    calls.push(args);
    const joined = args.join(" ");
    if (joined.includes("actions/runs?per_page=100&exclude_pull_requests=false")) return scenario.history ?? '{"total_count":0,"workflow_runs":[]}';
    if (joined.includes("actions/runs?status=queued") || joined.includes("actions/runs?status=in_progress")) return '{"total_count":0,"workflow_runs":[]}';
    if (joined.includes("actions/runners?per_page=100")) return JSON.stringify({ total_count: scenario.runners?.length ?? 0, runners: scenario.runners ?? [] });
    if (joined.includes("actions/runs?branch=")) {
      const query = new URLSearchParams(joined.split("?")[1]);
      const branch = decodeURIComponent(query.get("branch") ?? "");
      const runs = scenario.runs?.[branch] ?? [];
      return JSON.stringify({ total_count: scenario.exactTotal ?? runs.length, workflow_runs: runs });
    }
    const jobsMatch = joined.match(/actions\/runs\/(\d+)\/jobs/);
    if (jobsMatch) return scenario.jobs?.[Number(jobsMatch[1])] ?? '{"total_count":0,"jobs":[]}';
    const logMatch = joined.match(/actions\/jobs\/(\d+)\/logs$/);
    if (logMatch) {
      const jobId = Number(logMatch[1]);
      if (scenario.failLogs?.includes(jobId)) throw new Error("log failed");
      return scenario.logs?.[jobId] ?? T2_CODE_LOG;
    }
    if (args[0] === "api" && args[1] === "graphql") {
      if (joined.includes("CiMergedPrSnapshot")) {
        if (scenario.failMerged) throw new Error("merged failed");
        const nodes = scenario.merged ?? [];
        return JSON.stringify({ data: { search: { issueCount: scenario.mergedTotal ?? nodes.length, pageInfo: { hasNextPage: scenario.mergedNext ?? false }, nodes } } });
      }
      return JSON.stringify({ data: { repository: {
        defaultBranchRef: { name: "main" },
        refs: { nodes: scenario.refs ?? [], pageInfo: { hasNextPage: scenario.refsNext ?? false } },
        pullRequests: { nodes: scenario.prs ?? [], pageInfo: { hasNextPage: scenario.prsNext ?? false } },
      } } });
    }
    throw new Error(`unexpected gh call: ${joined}`);
  };
  return { run, calls };
}

async function pollT2(scenario: T2Scenario, polls = 1) {
  const routed = t2Runner(scenario);
  const adapter = createGhciAdapter({ runGh: routed.run, repos: ["acme/app"], now: () => Date.UTC(2026, 6, 19, 12) });
  let result!: Awaited<ReturnType<typeof adapter.poll>>;
  for (let index = 0; index < polls; index += 1) result = await adapter.poll();
  return { row: repos(result)[0]!, calls: routed.calls, result };
}

describe("createGhciAdapter T2 train state", () => {
  test.each([
    ["queued", null, 1, "gating"],
    ["in_progress", null, 2, "rerunning"],
    ["completed", "failure", 1, "failed"],
    ["completed", "success", 1, "green"],
  ])("derives %s/%s attempt %i as %s", async (status, conclusion, attempt, expected) => {
    const branch = "integration/batch-train-12";
    const { row } = await pollT2({
      refs: [{ name: branch, target: { oid: "oid-12" } }],
      prs: [trainPr(12, branch, "oid-12")],
      runs: { [branch]: [ghRun(12, branch, "oid-12", status!, conclusion, attempt)] },
    });
    expect(row.trains[0]).toMatchObject({ state: expected, stateGap: null, createdAt: "2026-07-19T12:00:00Z", members: [8, 5, 3] });
  });

  test.each([
    ["queued", null],
    ["completed", "failure"],
    ["completed", "success"],
  ])("keeps a branch-only %s/%s run building", async (status, conclusion) => {
    const branch = "integration/batch-train-12";
    const { row } = await pollT2({
      refs: [{ name: branch, target: { oid: "oid-12" } }],
      runs: { [branch]: [ghRun(12, branch, "oid-12", status!, conclusion)] },
    });
    expect(row.trains[0]).toMatchObject({ state: "building", stateGap: null, prNumber: null, gateRun: null });
  });

  test("applies canonical total ordering and exact-query admission", async () => {
    const branches = ["integration/batch-train-9", "integration/batch-train-x", "integration/batch-train-12"];
    const { row, calls } = await pollT2({
      refs: branches.map((name) => ({ name, target: { oid: `oid-${name.at(-1)}` } })),
      runs: Object.fromEntries(branches.map((branch, index) => [branch, [ghRun(index + 1, branch, `oid-${branch.at(-1)}`, ["queued", "completed", "completed"][index]!, [null, "failure", "success"][index]!)]])),
    });
    expect(row.trains.map((train: any) => [train.branch, train.state, train.stateGap])).toEqual([
      ["integration/batch-train-12", null, "train runs incomplete"],
      ["integration/batch-train-9", null, "train runs incomplete"],
      ["integration/batch-train-x", null, "train runs incomplete"],
    ]);
    expect(calls.filter((args) => args.join(" ").includes("actions/runs?branch=")).map((args) => decodeURIComponent(args.join(" ").match(/branch=([^&]+)/)![1]!))).toEqual([
      "integration/batch-train-12",
      "integration/batch-train-9",
    ]);
  });

  test("orders invalid train suffixes by created time, PR number, then branch", async () => {
    const prs = [
      trainPr(2, "integration/batch-train-z", "z", { createdAt: "2026-07-19T10:00:00Z" }),
      trainPr(3, "integration/batch-train-y", "y", { createdAt: "2026-07-19T10:00:00Z" }),
      trainPr(1, "integration/batch-train-a", "a", { createdAt: "2026-07-19T11:00:00Z" }),
    ];
    const { row } = await pollT2({ prs });
    expect(row.trains.map((train: any) => train.branch)).toEqual([
      "integration/batch-train-a",
      "integration/batch-train-y",
      "integration/batch-train-z",
    ]);
  });

  test("selects latest exact-OID run by created time, attempt, then ID and exposes unsupported gaps", async () => {
    const branch = "integration/batch-train-12";
    const runs = [
      ghRun(1, branch, "oid", "completed", "failure", 1, "2026-07-19T10:00:00Z"),
      ghRun(2, branch, "oid", "completed", "success", 1, "2026-07-19T11:00:00Z"),
      ghRun(3, branch, "oid", "completed", "cancelled", 2, "2026-07-19T11:00:00Z"),
    ];
    const { row } = await pollT2({ refs: [{ name: branch, target: { oid: "oid" } }], prs: [trainPr(12, branch, "oid")], runs: { [branch]: runs } });
    expect(row.trains[0]).toMatchObject({ state: null, gateRun: { id: 3, attempt: 2 } });
    expect(row.trains[0].stateGap).toBeTruthy();
  });

  test("ignores obsolete open and merged PR identities on reused branches", async () => {
    const branch = "integration/batch-train-12";
    const staleRun = ghRun(1, branch, "old", "completed", "failure");
    const open = await pollT2({ refs: [{ name: branch, target: { oid: "new" } }], prs: [trainPr(12, branch, "old")], runs: { [branch]: [staleRun] } });
    expect(open.row.trains[0]).toMatchObject({ state: null, stateGap: "train runs incomplete", prNumber: null, gateRun: null, actionEligible: false });

    const mergedPr = { ...trainPr(11, branch, "old"), mergedAt: "2026-07-19T11:00:00Z" };
    const merged = await pollT2({ refs: [{ name: branch, target: { oid: "new" } }], merged: [mergedPr], runs: { [branch]: [] } }, 11);
    expect(merged.row.trains[0]).toMatchObject({ state: "building", prNumber: null });
  });

  test("retains cached merged train PRs and gives open PR lifecycle precedence", async () => {
    const branch = "integration/batch-train-11";
    const mergedPr = { ...trainPr(11, branch, "oid-11"), mergedAt: "2026-07-19T11:00:00Z" };
    const merged = await pollT2({ merged: [mergedPr] }, 11);
    expect(merged.row.trains[0]).toMatchObject({ state: "merged", prNumber: 11, prUrl: "https://github.com/acme/app/pull/11" });

    const open = await pollT2({ prs: [trainPr(12, branch, "oid-12")], merged: [mergedPr], runs: { [branch]: [ghRun(12, branch, "oid-12", "queued", null)] } }, 11);
    expect(open.row.trains[0]).toMatchObject({ state: "gating", prNumber: 12 });
  });

  test.each([
    [{ refsNext: true }, "train refs incomplete"],
    [{ prsNext: true }, "PRs incomplete"],
    [{ exactTotal: 2 }, "train runs incomplete"],
  ])("applies completeness guard %s", async (override, gap) => {
    const branch = "integration/batch-train-12";
    const { row } = await pollT2({ refs: [{ name: branch, target: { oid: "oid" } }], prs: [trainPr(12, branch, "oid")], runs: { [branch]: [ghRun(12, branch, "oid", "completed", "failure")] }, ...override });
    expect(row.trains[0]).toMatchObject({ state: null, stateGap: gap, gateRun: null, actionEligible: false });
    expect(row.trainsComplete).toBe(false);
  });

  test("derives PR queue metrics, staleVsMain, checks, and train membership", async () => {
    const train = trainPr(12, "integration/batch-train-12", "oid-12");
    const queued = trainPr(5, "feature/five", "oid-5", { createdAt: "2026-07-19T06:00:00Z", mergeStateStatus: "BEHIND" });
    const unknown = trainPr(8, "feature/eight", "oid-8", { baseRefName: "release", mergeStateStatus: "CLEAN" });
    const { row } = await pollT2({ prs: [train, unknown, queued], runs: { "integration/batch-train-12": [ghRun(12, "integration/batch-train-12", "oid-12", "queued", null)] } });
    expect(row).toMatchObject({ prQueueDepth: 3, oldestQueuedAgeH: 6 });
    expect(row.prs.find((pr: any) => pr.number === 5)).toMatchObject({ ageH: 6, staleVsMain: true, trainPr: 12 });
    expect(row.prs.find((pr: any) => pr.number === 8)).toMatchObject({ staleVsMain: null, trainPr: 12 });
    expect(row.prs.find((pr: any) => pr.number === 12).staleVsMain).toBe(false);
  });
});

describe("createGhciAdapter T2 jobs and runners", () => {
  test("classifies each unseen job ID once and reuses the cached result", async () => {
    const options: RunnerOptions = { open: TWO_TRAINS_OPEN };
    const routed = routingRunner(options);
    const configured = ["o/r1", "o/r2", "o/r3", "o/r4", "o/r5"];
    const adapter = createGhciAdapter({ runGh: routed.run, repos: configured });
    const first = repos(await adapter.poll());
    expect(routed.calls.filter((args) => args.join(" ").endsWith("/logs"))).toHaveLength(1);
    expect(
      first
        .flatMap((row) => row.trains)
        .flatMap((train: any) => train.gateRun?.jobs ?? [])
        .every((job: any) => job.infraFlag === true),
    ).toBe(true);

    options.open = OPEN;
    const start = routed.calls.length;
    const second = repos(await adapter.poll());
    expect(routed.calls.slice(start).filter((args) => args.join(" ").endsWith("/actions/jobs/501/logs"))).toHaveLength(0);
    expect(second.flatMap((row) => row.trains).flatMap((train: any) => train.gateRun?.jobs ?? []).filter((job: any) => job.id === 501).every((job: any) => job.infraFlag === true)).toBe(true);
  });

  test("classifies only the retained 64KiB log suffix and caches matches by job ID", async () => {
    const branch = "integration/batch-train-12";
    const jobs = JSON.parse(T2_JOBS);
    jobs.jobs = [jobs.jobs[1]];
    jobs.total_count = 1;
    const beforeTail = `ENOSPC${"x".repeat(65_536)}`;
    const scenario = { refs: [{ name: branch, target: { oid: "oid" } }], prs: [trainPr(12, branch, "oid")], runs: { [branch]: [ghRun(12, branch, "oid", "completed", "failure")] }, jobs: { 12: JSON.stringify(jobs) }, logs: { 702: beforeTail } };
    const first = await pollT2(scenario);
    expect(first.row.trains[0]).toMatchObject({ infraFlag: false, blockedBy: "integration lane — failure", gateRun: { jobs: [{ id: 702, infraFlag: false }] } });

    scenario.logs[702] = T2_INFRA_LOG;
    const routed = t2Runner(scenario);
    const adapter = createGhciAdapter({ runGh: routed.run, repos: ["acme/app"] });
    await adapter.poll();
    const second = repos(await adapter.poll())[0]!;
    expect(second.trains[0]).toMatchObject({ infraFlag: true, blockedBy: "integration lane — The self-hosted runner has lost communication (infra)" });
    expect(routed.calls.filter((args) => args.join(" ").endsWith("/actions/jobs/702/logs"))).toHaveLength(1);
  });

  test("derives deterministic blockers and runner occupancy joins", async () => {
    const branch = "integration/batch-train-12";
    const { row } = await pollT2({
      refs: [{ name: branch, target: { oid: "oid" } }],
      prs: [trainPr(12, branch, "oid")],
      runs: { [branch]: [ghRun(12, branch, "oid", "in_progress", null)] },
      jobs: { 12: T2_JOBS },
      runners: [
        { id: 7, name: "runner-7", status: "online", busy: true },
        { id: 8, name: "runner-8", status: "online", busy: true },
        { id: 9, name: "runner-9", status: "offline", busy: false },
      ],
    });
    expect(row.trains[0].blockedBy).toBe("active lane");
    expect(row.runners).toEqual([
      { id: 7, name: "runner-7", status: "online", busy: true, currentJob: "active lane", currentRepo: "acme/app", currentKnown: true },
      { id: 8, name: "runner-8", status: "online", busy: true, currentJob: null, currentRepo: null, currentKnown: false },
      { id: 9, name: "runner-9", status: "offline", busy: false, currentJob: null, currentRepo: null, currentKnown: true },
    ]);
    expect(row).toMatchObject({ runnersBusy: 2, runnersTotal: 2 });
    const online = row.runners.filter((runner: any) => runner.status === "online");
    expect(online.length > 0 && online.every((runner: any) => runner.busy)).toBe(true);
  });

  test("selects failed blockers by started time with null last, then job ID", async () => {
    const branch = "integration/batch-train-12";
    const failedJob = (id: number, name: string, startedAt: string | null) => ({
      id, name, status: "completed", conclusion: "failure", runner_id: null, runner_name: null,
      started_at: startedAt, completed_at: "2026-07-19T10:00:00Z",
    });
    const jobs = JSON.stringify({ total_count: 3, jobs: [
      failedJob(800, "missing time", null),
      failedJob(802, "later ID", "2026-07-19T09:00:00Z"),
      failedJob(801, "earliest", "2026-07-19T09:00:00Z"),
    ] });
    const { row } = await pollT2({
      refs: [{ name: branch, target: { oid: "oid" } }],
      prs: [trainPr(12, branch, "oid")],
      runs: { [branch]: [ghRun(12, branch, "oid", "completed", "failure")] },
      jobs: { 12: jobs },
    });
    expect(row.trains[0].blockedBy).toBe("earliest — failure");
  });

  test("marks truncated or budget-deferred jobs incomplete without inventing infra state", async () => {
    const branch = "integration/batch-train-12";
    const jobs = JSON.parse(T2_JOBS);
    jobs.total_count = 101;
    const { row } = await pollT2({ refs: [{ name: branch, target: { oid: "oid" } }], prs: [trainPr(12, branch, "oid")], runs: { [branch]: [ghRun(12, branch, "oid", "completed", "failure")] }, jobs: { 12: JSON.stringify(jobs) } });
    expect(row.degraded).toEqual(["pagination", "jobs"]);
    expect(row.trains[0]).toMatchObject({ infraFlag: null, gateRun: { jobsComplete: false } });
  });

  test("marks excess train job scope degraded while completeness guards suppress job calls", async () => {
    const prs = [9, 10, 11].map((number) => trainPr(number, `integration/batch-train-${number}`, `oid-${number}`));
    const refs = prs.map((pr) => ({ name: pr.headRefName as string, target: { oid: pr.headRefOid as string } }));
    const runs = Object.fromEntries(prs.map((pr) => [pr.headRefName, [ghRun(pr.number as number, pr.headRefName as string, pr.headRefOid as string, "queued", null)]]));
    const { row, calls } = await pollT2({ prs, refs, runs });
    expect(row.degraded).toContain("jobs");
    expect(row.trains.every((train: any) => train.gateRun === null && train.stateGap === "train runs incomplete")).toBe(true);
    expect(calls.some((args) => args.join(" ").includes("/jobs?"))).toBe(false);
  });

  test("does not report saturation for an empty online-runner set", async () => {
    const { row } = await pollT2({ runners: [{ id: 9, name: "offline", status: "offline", busy: true }] });
    const online = row.runners.filter((runner: any) => runner.status === "online");
    expect(online.length > 0 && online.every((runner: any) => runner.busy)).toBe(false);
    expect(row).toMatchObject({ runnersBusy: 0, runnersTotal: 0 });
  });
});

describe("createGhciAdapter T5 inbox scopes", () => {
  const branch = "integration/batch-train-12";
  const ref = { name: branch, target: { oid: "oid" } };

  test("emits scoped run failure, infra failure, green-unmerged, and queue-without-train items", async () => {
    const infra = await pollT2({
      refs: [ref],
      prs: [trainPr(12, branch, "oid")],
      runs: { [branch]: [ghRun(120, branch, "oid", "completed", "failure")] },
      jobs: { 120: T2_JOBS },
      logs: { 702: T2_INFRA_LOG },
    });
    expect(infra.result.items).toContainEqual(expect.objectContaining({
      id: "ghci:train:acme/app:12:infra-failed",
      reconciliationScope: "train:acme/app:infra-failed",
      actions: [{
        verb: "ci.rerunFailed",
        args: { repo: "acme/app", runId: "120" },
        label: "Rerun failed jobs",
        recommended: true,
      }],
    }));
    expect(infra.result.completeScopes).toContain("train:acme/app:infra-failed");

    const green = await pollT2({
      refs: [ref],
      prs: [trainPr(12, branch, "oid")],
      runs: {
        [branch]: [ghRun(121, branch, "oid", "completed", "success", 1, "2026-07-19T11:00:00Z", "2026-07-19T11:40:00Z")],
      },
    });
    expect(green.result.items).toContainEqual(expect.objectContaining({
      id: "ghci:train:acme/app:12:green-unmerged",
      reconciliationScope: "train:acme/app:green-unmerged",
      actions: [{
        verb: "open",
        args: { url: "https://github.com/acme/app/pull/12" },
        label: "Open pull request",
        recommended: true,
      }],
    }));
    expect(green.result.completeScopes).toContain("train:acme/app:green-unmerged");

    const queuedPrs = [1, 2, 3, 4].map((number) => trainPr(number, `feature/${number}`, `oid-${number}`));
    const queued = await pollT2({ prs: queuedPrs });
    expect(queued.result.items).toContainEqual(expect.objectContaining({
      id: "ghci:train:acme/app:queue-no-train",
      reconciliationScope: "train:acme/app:queue-no-train",
      severity: "warn",
    }));
    expect(queued.result.completeScopes).toContain("train:acme/app:queue-no-train");

    const history = JSON.stringify({ total_count: 1, workflow_runs: [ghRun(900, "main", "oid", "completed", "failure")] });
    const failure = await pollT2({ history });
    expect(failure.result.items).toContainEqual(expect.objectContaining({
      id: "ghci:acme/app:900:1",
      reconciliationScope: "run-failure:acme/app",
    }));
    expect(failure.result.completeScopes).toContain("run-failure:acme/app");
  });

  test("marks complete empty scopes while suppressing positive claims at exact incompleteness boundaries", async () => {
    const empty = await pollT2({});
    expect(empty.result.items).toEqual([]);
    expect(empty.result.completeScopes).toEqual([
      "train:acme/app:infra-failed",
      "train:acme/app:green-unmerged",
      "train:acme/app:queue-no-train",
    ]);

    const truncatedRefs = await pollT2({ refsNext: true });
    expect(truncatedRefs.result.completeScopes).toEqual([]);

    const partialFailure = ghRun(901, "main", "oid", "completed", "failure");
    const truncatedHistory = await pollT2({
      history: JSON.stringify({ total_count: 2, workflow_runs: [partialFailure] }),
    });
    expect(truncatedHistory.result.completeScopes).toContain("run-failure:acme/app");
    expect(truncatedHistory.result.items.some((item) => item.id === "ghci:acme/app:901:1")).toBe(true);
    expect(truncatedHistory.result.completeScopes).toContain("train:acme/app:green-unmerged");

    for (const incomplete of [
      await pollT2({ prsNext: true }),
      await pollT2({
        refs: [ref],
        prs: [trainPr(12, branch, "oid")],
        runs: { [branch]: [ghRun(120, branch, "oid", "completed", "success")] },
        exactTotal: 2,
      }),
    ]) {
      expect(incomplete.result.completeScopes).toEqual([]);
    }

    const jobs = JSON.parse(T2_JOBS);
    jobs.total_count = 99;
    const incompleteInfra = await pollT2({
      refs: [ref],
      prs: [trainPr(12, branch, "oid")],
      runs: { [branch]: [ghRun(120, branch, "oid", "completed", "failure")] },
      jobs: { 120: JSON.stringify(jobs) },
    });
    expect(incompleteInfra.result.items.some((item) => item.id.endsWith(":infra-failed"))).toBe(false);
    expect(incompleteInfra.result.completeScopes).not.toContain("train:acme/app:infra-failed");
    expect(incompleteInfra.result.completeScopes).toContain("train:acme/app:green-unmerged");

    const unclassified = await pollT2({
      refs: [ref],
      prs: [trainPr(12, branch, "oid")],
      runs: { [branch]: [ghRun(120, branch, "oid", "completed", "failure")] },
      jobs: { 120: T2_JOBS },
      failLogs: [702],
    });
    expect(unclassified.result.completeScopes).not.toContain("train:acme/app:infra-failed");
    expect(unclassified.result.items.some((item) => item.id.endsWith(":infra-failed"))).toBe(false);
  });

  test("budget-excluded repos expose no reconciliation scopes", async () => {
    const configured = ["o/r1", "o/r2", "o/r3", "o/r4", "o/r5", "o/r6", "o/r7"];
    const routed = routingRunner();
    const result = await createGhciAdapter({ runGh: routed.run, repos: configured }).poll();
    expect(result.completeScopes?.some((scope) => scope.includes("o/r7"))).toBe(false);
  });

  test("green-unmerged uses a strict ten-minute completion threshold and stable identity", async () => {
    const scenario: T2Scenario = {
      refs: [ref],
      prs: [trainPr(12, branch, "oid")],
      runs: {
        [branch]: [ghRun(121, branch, "oid", "completed", "success", 1, "2026-07-19T11:00:00Z", "2026-07-19T11:50:00Z")],
      },
    };
    const routed = t2Runner(scenario);
    let nowMs = Date.UTC(2026, 6, 19, 12);
    const adapter = createGhciAdapter({ runGh: routed.run, repos: ["acme/app"], now: () => nowMs });

    expect((await adapter.poll()).items.some((item) => item.id.endsWith(":green-unmerged"))).toBe(false);
    nowMs += 1;
    const first = await adapter.poll();
    const second = await adapter.poll();
    expect(first.items.filter((item) => item.id.endsWith(":green-unmerged"))).toHaveLength(1);
    expect(second.items.find((item) => item.id.endsWith(":green-unmerged"))?.id).toBe(
      first.items.find((item) => item.id.endsWith(":green-unmerged"))?.id,
    );
  });
});

describe("createGhciAdapter T3 lag history", () => {
  const branch = "integration/batch-train-12";
  const refs = [{ name: branch, target: { oid: "oid" } }];
  const prs = [trainPr(12, branch, "oid")];

  test("derives complete UTC merged metrics and leaves history completeness independent", async () => {
    const scenario: T2Scenario = {
      refs,
      prs,
      history: '{"total_count":1,"workflow_runs":[]}',
      merged: JSON.parse(T3_MERGED).data.search.nodes,
    };
    const routed = t2Runner(scenario);
    const adapter = createGhciAdapter({ runGh: routed.run, repos: ["acme/app"], now: () => Date.UTC(2026, 6, 19, 12) });

    let row = repos(await adapter.poll())[0]!;
    expect(row.lag).toEqual({ landsToday: null, medianOpenToMergeH7d: null, gates: [], warmedAt: null });
    for (let poll = 1; poll < 10; poll += 1) row = repos(await adapter.poll())[0]!;

    expect(row.historyComplete).toBe(false);
    expect(row.lag).toEqual({ landsToday: 2, medianOpenToMergeH7d: 36, gates: [], warmedAt: "2026-07-19T12:00:00.000Z" });
    expect(routed.calls.find((args) => args.some((arg) => arg.includes("CiMergedPrSnapshot")))?.join(" ")).toContain("merged:2026-07-13..2026-07-19");
  });

  test.each([
    ["truncated count", { merged: JSON.parse(T3_MERGED).data.search.nodes, mergedTotal: 99 }],
    ["next page", { merged: JSON.parse(T3_MERGED).data.search.nodes, mergedNext: true }],
    ["invalid timestamp", { merged: [{ ...JSON.parse(T3_MERGED).data.search.nodes[0], createdAt: "invalid" }] }],
    ["null node", { merged: [null] }],
    ["query failure", { failMerged: true }],
  ])("nulls both merged metrics for %s", async (_label, override) => {
    const { row } = await pollT2({ refs, prs, ...override }, 10);
    expect(row.lag).toMatchObject({ landsToday: null, medianOpenToMergeH7d: null, warmedAt: null });
    expect(row.degraded).toContain("pagination");
  });

  test("requires three valid seven-day samples without hiding today's count", async () => {
    const nodes = JSON.parse(T3_MERGED).data.search.nodes.slice(0, 2);
    const { row } = await pollT2({ refs, prs, merged: nodes }, 10);
    expect(row.lag).toMatchObject({ landsToday: 0, medianOpenToMergeH7d: null, warmedAt: "2026-07-19T12:00:00.000Z" });
  });

  test("records only completed runs with complete, valid job timing and resets on restart", async () => {
    const scenario: T2Scenario = { refs, prs, runs: { [branch]: [] }, jobs: {} };
    const routed = t2Runner(scenario);
    const adapter = createGhciAdapter({ runGh: routed.run, repos: ["acme/app"], now: () => Date.UTC(2026, 6, 19, 12) });

    for (const [id, status, jobs] of [
      [1, "queued", T3_JOBS],
      [2, "in_progress", '{"total_count":0,"jobs":[]}'],
      [3, "completed", '{"total_count":2,"jobs":[]}'],
      [4, "completed", JSON.stringify({ total_count: 1, jobs: [{ ...JSON.parse(T3_JOBS).jobs[0], completed_at: null }] })],
      [45, "completed", JSON.stringify({ total_count: 1, jobs: [{ ...JSON.parse(T3_JOBS).jobs[0], started_at: "2026-07-19T10:10:00Z", completed_at: "2026-07-19T10:00:00Z" }] })],
      [5, "completed", T3_JOBS],
    ] as const) {
      scenario.runs![branch] = [ghRun(id, branch, "oid", status, status === "completed" ? "success" : null, 1, "2026-07-19T10:00:00Z", "2026-07-19T11:00:00Z")];
      scenario.jobs![id] = jobs;
      await adapter.poll();
    }

    const row = repos(await adapter.poll())[0]!;
    expect(row.lag.gates).toEqual([{ runId: 5, computeMin: 30, wallMin: 60 }]);
    scenario.runs![branch] = [ghRun(6, branch, "oid", "queued", null)];
    const restarted = createGhciAdapter({ runGh: routed.run, repos: ["acme/app"], now: () => Date.UTC(2026, 6, 19, 12) });
    expect(repos(await restarted.poll())[0]!.lag).toEqual({ landsToday: null, medianOpenToMergeH7d: null, gates: [], warmedAt: null });
  });

  test("dedupes, orders by completion and run ID, and retains newest 50 after sorting", async () => {
    const scenario: T2Scenario = { refs, prs, runs: { [branch]: [] }, jobs: {} };
    const routed = t2Runner(scenario);
    const adapter = createGhciAdapter({ runGh: routed.run, repos: ["acme/app"] });
    const arrivals = [
      ...Array.from({ length: 51 }, (_, index) => ({ id: index + 1, updated: `2026-07-19T11:${String(index).padStart(2, "0")}:00Z` })),
      { id: 500, updated: "2026-07-19T09:00:00Z" },
      { id: 51, updated: "2026-07-19T11:50:00Z" },
      { id: 700, updated: "2026-07-19T11:50:00Z" },
    ];
    let row!: Record<string, any>;
    for (const arrival of arrivals) {
      scenario.runs![branch] = [ghRun(arrival.id, branch, "oid", "completed", "success", 1, "2026-07-19T08:00:00Z", arrival.updated)];
      scenario.jobs![arrival.id] = T3_JOBS;
      row = repos(await adapter.poll())[0]!;
    }

    expect(row.lag.gates).toHaveLength(50);
    expect(row.lag.gates.slice(0, 3).map((gate: any) => gate.runId)).toEqual([700, 51, 50]);
    expect(row.lag.gates.filter((gate: any) => gate.runId === 51)).toHaveLength(1);
    expect(row.lag.gates.some((gate: any) => gate.runId === 500 || gate.runId === 1 || gate.runId === 2)).toBe(false);
  });

  test.each([
    ["invalid created time", "invalid", "2026-07-19T11:00:00Z"],
    ["invalid updated time", "2026-07-19T10:00:00Z", "invalid"],
    ["reversed run time", "2026-07-19T12:00:00Z", "2026-07-19T11:00:00Z"],
  ])("filters %s from amplification", async (_label, createdAt, updatedAt) => {
    const { row } = await pollT2({
      refs,
      prs,
      runs: { [branch]: [ghRun(8, branch, "oid", "completed", "success", 1, createdAt, updatedAt)] },
      jobs: { 8: T3_JOBS },
    });
    expect(row.lag.gates).toEqual([]);
  });
});
