import { describe, expect, test } from "bun:test";
import { computeHookFireStats, type DispatcherRegistry } from "./hook-fire-stats";

const NOW = new Date("2026-08-15T12:00:00.000Z");
const NOW_MS = NOW.getTime();

const REGISTRY: DispatcherRegistry = {
  PreToolUse: [
    { id: "curl-timeout-gate", matcher: "Bash", timeoutMs: 5000 },
    { id: "deny-gate", matcher: "Bash", timeoutMs: 5000 },
  ],
  PostToolUse: [{ id: "edit-inspector", matcher: "Edit|Write", timeoutMs: 2000 }],
};

function iso(offsetMs: number): string {
  return new Date(NOW_MS - offsetMs).toISOString();
}

describe("computeHookFireStats", () => {
  test("absent jsonl returns manifest rows all at zero, status absent, observing", async () => {
    const result = await computeHookFireStats({
      jsonlPath: "/nowhere/hook-fires.jsonl",
      existsSyncImpl: () => false,
      importRegistryImpl: async () => ({ REGISTRY }),
      now: () => NOW,
    });

    expect(result.status).toBe("absent");
    expect(result.daysObserved).toBe(0);
    expect(result.earliestObservedAt).toBeNull();
    expect(result.modules).toHaveLength(3);
    for (const row of result.modules) {
      expect(row.fires24h).toBe(0);
      expect(row.fires7d).toBe(0);
      expect(row.fires30d).toBe(0);
      expect(row.lastFiredAt).toBeNull();
      expect(row.status).toBe("OBSERVING");
      expect(row.inManifest).toBe(true);
    }
  });

  test("a module firing in the last 7d is ACTIVE even mid-observation", async () => {
    const DAY = 24 * 60 * 60 * 1000;
    const fixture = [
      JSON.stringify({ ts: iso(0), event: "PreToolUse", module: "curl-timeout-gate", action: "act" }),
      JSON.stringify({ ts: iso(DAY), event: "PreToolUse", module: "curl-timeout-gate", action: "act" }),
    ].join("\n");

    const result = await computeHookFireStats({
      jsonlPath: "/tmp/hook-fires.jsonl",
      existsSyncImpl: () => true,
      readFileImpl: () => fixture,
      importRegistryImpl: async () => ({ REGISTRY }),
      now: () => NOW,
    });

    expect(result.status).toBe("ok");
    expect(result.daysObserved).toBe(1);
    const row = result.modules.find((m) => m.module === "curl-timeout-gate");
    expect(row?.fires24h).toBe(2);
    expect(row?.fires7d).toBe(2);
    expect(row?.fires30d).toBe(2);
    expect(row?.lastFiredAt).toBe(iso(0));
    expect(row?.status).toBe("ACTIVE");

    const neverFired = result.modules.find((m) => m.module === "deny-gate");
    expect(neverFired?.status).toBe("OBSERVING");
  });

  test("zero fires in >=7 observed days is SUSPECT; >=30 with zero in 30d is REMOVAL_CANDIDATE", async () => {
    const DAY = 24 * 60 * 60 * 1000;
    const fixtureSuspect = [
      JSON.stringify({ ts: iso(9 * DAY), event: "PreToolUse", module: "deny-gate", action: "deny" }),
    ].join("\n");

    const suspect = await computeHookFireStats({
      jsonlPath: "/tmp/hook-fires.jsonl",
      existsSyncImpl: () => true,
      readFileImpl: () => fixtureSuspect,
      importRegistryImpl: async () => ({ REGISTRY }),
      now: () => NOW,
    });
    expect(suspect.daysObserved).toBe(9);
    const row = suspect.modules.find((m) => m.module === "deny-gate");
    expect(row?.fires7d).toBe(0);
    expect(row?.status).toBe("SUSPECT");

    const fixtureRemoval = [
      JSON.stringify({ ts: iso(31 * DAY), event: "PreToolUse", module: "deny-gate", action: "deny" }),
    ].join("\n");
    const removal = await computeHookFireStats({
      jsonlPath: "/tmp/hook-fires.jsonl",
      existsSyncImpl: () => true,
      readFileImpl: () => fixtureRemoval,
      importRegistryImpl: async () => ({ REGISTRY }),
      now: () => NOW,
    });
    expect(removal.daysObserved).toBe(31);
    const removalRow = removal.modules.find((m) => m.module === "deny-gate");
    expect(removalRow?.fires7d).toBe(0);
    expect(removalRow?.fires30d).toBe(0);
    expect(removalRow?.status).toBe("REMOVAL_CANDIDATE");
  });

  test("a module observed in telemetry but absent from the manifest still gets a row", async () => {
    const fixture = JSON.stringify({ ts: iso(0), event: "Stop", module: "session-summary", action: "advise" });
    const result = await computeHookFireStats({
      jsonlPath: "/tmp/hook-fires.jsonl",
      existsSyncImpl: () => true,
      readFileImpl: () => fixture,
      importRegistryImpl: async () => ({ REGISTRY }),
      now: () => NOW,
    });
    const row = result.modules.find((m) => m.module === "session-summary");
    expect(row?.inManifest).toBe(false);
    expect(row?.event).toBe("Stop");
    expect(row?.matcher).toBeNull();
    expect(row?.status).toBe("ACTIVE");
  });

  test("corrupt lines are ignored without throwing", async () => {
    const fixture = ["{ not json", JSON.stringify({ event: "PreToolUse", module: "x", action: "act" })].join("\n");
    const result = await computeHookFireStats({
      jsonlPath: "/tmp/hook-fires.jsonl",
      existsSyncImpl: () => true,
      readFileImpl: () => fixture,
      importRegistryImpl: async () => ({ REGISTRY }),
      now: () => NOW,
    });
    expect(result.status).toBe("ok");
    expect(result.modules.every((m) => m.fires24h === 0)).toBe(true);
  });
});
