import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createGatesAdapter, type GitRunner } from "./gates";

const FIXTURE_DIR = join(import.meta.dir, "../../test/fixtures/gates");
const CONFIRMED_PATH = join(FIXTURE_DIR, "confirmed.json");
const PRECISION_PATH = join(FIXTURE_DIR, "precision_records.json");
const REPO_ALPHA = join(FIXTURE_DIR, "repo-alpha");
const REPO_BETA = join(FIXTURE_DIR, "repo-beta");
const BASELINE_REL = "baseline.json";

const FIXED_NOW_MS = Date.parse("2026-07-17T00:00:00.000Z");
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const CUTOFF_ISO = new Date(FIXED_NOW_MS - SEVEN_DAYS_MS).toISOString();

const BASELINE_7D_ALPHA = readFileSync(join(FIXTURE_DIR, "baseline-7d-ago-alpha.json"), "utf8");
const BASELINE_7D_BETA = readFileSync(join(FIXTURE_DIR, "baseline-7d-ago-beta.json"), "utf8");

function diskRead(path: string): string {
  return readFileSync(path, "utf8");
}

function fakeReadFile(files: Record<string, string>): (path: string) => string {
  return (path: string) => {
    const content = files[path];
    if (content === undefined) {
      throw new Error(`ENOENT: no such file, open '${path}'`);
    }
    return content;
  };
}

function keyedGit(responses: Record<string, string>): GitRunner {
  return async (args, cwd) => {
    const key = `${cwd}::${args.join(" ")}`;
    const response = responses[key];
    if (response === undefined) {
      throw new Error(`unmocked git call: ${key}`);
    }
    return response;
  };
}

function baseAdapterOpts(runGit: GitRunner) {
  return {
    confirmedPath: CONFIRMED_PATH,
    precisionRecordsPath: PRECISION_PATH,
    repos: [REPO_ALPHA, REPO_BETA],
    slopgateBaselineRel: BASELINE_REL,
    readFileImpl: diskRead,
    runGit,
    now: () => FIXED_NOW_MS,
  };
}

describe("createGatesAdapter", () => {
  test("id defaults to gates with a sane default interval", () => {
    const adapter = createGatesAdapter({
      confirmedPath: CONFIRMED_PATH,
      precisionRecordsPath: PRECISION_PATH,
      repos: [],
      readFileImpl: diskRead,
      runGit: async () => "",
    });
    expect(adapter.id).toBe("gates");
    expect(adapter.interval).toBeGreaterThan(0);
  });

  test("emits gates panel with prevent open count, slopgate debt total and 7-day trend", async () => {
    const git = keyedGit({
      [`${REPO_ALPHA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "alpha7d\n",
      [`${REPO_ALPHA}::show alpha7d:${BASELINE_REL}`]: BASELINE_7D_ALPHA,
      [`${REPO_BETA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "beta7d\n",
      [`${REPO_BETA}::show beta7d:${BASELINE_REL}`]: BASELINE_7D_BETA,
    });
    const adapter = createGatesAdapter(baseAdapterOpts(git));
    const result = await adapter.poll();

    expect(result.panels).toHaveLength(1);
    expect(result.panels[0]?.id).toBe("gates");
    const data = result.panels[0]?.data as {
      preventOpen: number;
      slopgateDebtTotal: number;
      slopgateDebtTrend7d: number;
      slopgateRepos: Array<{ repo: string; debt: number; trend7d: number }>;
      warnignorePending: number;
      warnignoreAdds7d: number;
      precisionRecords: Array<{ cellId: string; rate: number }>;
    };

    expect(data.preventOpen).toBe(2);
    expect(data.slopgateDebtTotal).toBe(8); // alpha 5 + beta 3
    expect(data.slopgateRepos).toEqual([
      { repo: "repo-alpha", debt: 5, trend7d: 2 }, // 5 - 3
      { repo: "repo-beta", debt: 3, trend7d: -1 }, // 3 - 4
    ]);
    expect(data.slopgateDebtTrend7d).toBe(1);
    expect(data.warnignorePending).toBe(1);
    expect(data.warnignoreAdds7d).toBe(1);
    expect(data.precisionRecords).toEqual([
      expect.objectContaining({ cellId: "S2-team-owner-invariant", rate: 1, k: 3 }),
    ]);
  });

  test("emits gate items for each confirmed prevent-band finding", async () => {
    const git = keyedGit({
      [`${REPO_ALPHA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
      [`${REPO_BETA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
    });
    const adapter = createGatesAdapter(baseAdapterOpts(git));
    const result = await adapter.poll();

    const preventItems = result.items.filter((item) => item.id.startsWith("gates:prevent:"));
    expect(preventItems).toHaveLength(2);
    expect(preventItems[0]).toMatchObject({
      kind: "gate",
      severity: "act",
      project: "security-gate",
      title: "Prevent-band [S9]: apps/web/src/server/referrals/service.ts",
    });
    expect(preventItems[1]?.id).toBe("gates:prevent:S2:packages/auth/src/guard.ts:membership");
  });

  test("emits gate decision items for warnignore additions awaiting sign-off", async () => {
    const git = keyedGit({
      [`${REPO_ALPHA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
      [`${REPO_BETA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
    });
    const adapter = createGatesAdapter(baseAdapterOpts(git));
    const result = await adapter.poll();

    const warnItems = result.items.filter((item) => item.id.startsWith("gates:warnignore:"));
    expect(warnItems).toHaveLength(1);
    expect(warnItems[0]).toMatchObject({
      kind: "gate",
      severity: "warn",
      project: "repo-alpha",
      title: '.warnignore addition requested: esbuild "import.meta" warning from vite 6 dep',
      decision: expect.objectContaining({
        question: expect.stringContaining("repo-alpha/.warnignore"),
        options: [
          { label: "Approve suppression (upstream)", recommended: true },
          { label: "Reject — try dep bump first" },
        ],
        freeText: true,
        waitingSince: "2026-07-17T11:00:00.000Z",
      }),
    });
  });

  test("successful poll omits an item when its condition no longer holds (snapshot contract)", async () => {
    const git = keyedGit({
      [`${REPO_ALPHA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
      [`${REPO_BETA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
    });
    const adapter = createGatesAdapter({
      ...baseAdapterOpts(git),
      confirmedPath: join(FIXTURE_DIR, "confirmed-empty.json"),
    });
    const result = await adapter.poll();
    expect(result.items.filter((item) => item.id.startsWith("gates:prevent:"))).toHaveLength(0);
    const data = result.panels[0]?.data as { preventOpen: number };
    expect(data.preventOpen).toBe(0);
  });

  test("throws when confirmed.json cannot be read so prior state is retained", async () => {
    const adapter = createGatesAdapter({
      confirmedPath: join(FIXTURE_DIR, "missing-confirmed.json"),
      precisionRecordsPath: PRECISION_PATH,
      repos: [],
      readFileImpl: fakeReadFile({
        [PRECISION_PATH]: readFileSync(PRECISION_PATH, "utf8"),
      }),
      runGit: async () => "",
      now: () => FIXED_NOW_MS,
    });
    await expect(adapter.poll()).rejects.toThrow(/cannot read prevent confirmed\.json/);
  });

  test("throws when precision records cannot be read", async () => {
    const adapter = createGatesAdapter({
      confirmedPath: CONFIRMED_PATH,
      precisionRecordsPath: join(FIXTURE_DIR, "missing-precision.json"),
      repos: [],
      readFileImpl: diskRead,
      runGit: async () => "",
      now: () => FIXED_NOW_MS,
    });
    await expect(adapter.poll()).rejects.toThrow(/cannot read precision records/);
  });

  test("throws when a slopgate baseline exists but is unreadable", async () => {
    const adapter = createGatesAdapter({
      confirmedPath: CONFIRMED_PATH,
      precisionRecordsPath: PRECISION_PATH,
      repos: [REPO_ALPHA],
      slopgateBaselineRel: BASELINE_REL,
      readFileImpl: fakeReadFile({
        [CONFIRMED_PATH]: readFileSync(CONFIRMED_PATH, "utf8"),
        [PRECISION_PATH]: readFileSync(PRECISION_PATH, "utf8"),
        [join(REPO_ALPHA, BASELINE_REL)]: "{not json",
      }),
      runGit: async () => "",
      now: () => FIXED_NOW_MS,
    });
    await expect(adapter.poll()).rejects.toThrow(/cannot read slopgate baseline/);
  });

  test("throws when a configured slopgate baseline is missing", async () => {
    const repo = join(FIXTURE_DIR, "repo-no-baseline");
    const git = keyedGit({
      [`${repo}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
    });
    const adapter = createGatesAdapter({
      confirmedPath: join(FIXTURE_DIR, "confirmed-empty.json"),
      precisionRecordsPath: PRECISION_PATH,
      repos: [repo],
      slopgateBaselineRel: BASELINE_REL,
      readFileImpl: diskRead,
      runGit: git,
      now: () => FIXED_NOW_MS,
    });
    await expect(adapter.poll()).rejects.toThrow(/cannot read slopgate baseline/);
  });

  test("throws when baseline git history cannot be read", async () => {
    const adapter = createGatesAdapter({
      ...baseAdapterOpts(async () => {
        throw new Error("git history unavailable");
      }),
      repos: [REPO_ALPHA],
    });

    await expect(adapter.poll()).rejects.toThrow(/git history unavailable/);
  });

  test("uses argv-array git invocation, never a shell string", async () => {
    const calls: Array<{ args: string[]; cwd: string }> = [];
    const runGit: GitRunner = async (args, cwd) => {
      calls.push({ args, cwd });
      const key = `${cwd}::${args.join(" ")}`;
      const responses: Record<string, string> = {
        [`${REPO_ALPHA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "alpha7d\n",
        [`${REPO_ALPHA}::show alpha7d:${BASELINE_REL}`]: BASELINE_7D_ALPHA,
        [`${REPO_BETA}::log -1 --format=%H --until=${CUTOFF_ISO} -- ${BASELINE_REL}`]: "",
      };
      return responses[key] ?? "";
    };
    const adapter = createGatesAdapter(baseAdapterOpts(runGit));
    await adapter.poll();

    expect(calls.length).toBeGreaterThan(0);
    for (const call of calls) {
      expect(Array.isArray(call.args)).toBe(true);
      for (const arg of call.args) {
        expect(arg.includes("&&")).toBe(false);
        expect(arg.includes(";")).toBe(false);
      }
    }
  });
});
