import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { AuditWriteError, ControllerStore, DeliveryEvidenceAdmissionService, StaleRevisionError } from "./store";
import { definitionDigest, type FeatureDefinition } from "./delivery/definitions";
import type { GateReceiptIdentity } from "./delivery/gate-receipt";

setDefaultTimeout(20_000);

describe("ControllerStore", () => {
  let dir: string;
  let dbPath: string;
  let store: ControllerStore;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), "controller-store-"));
    dbPath = join(dir, "state.sqlite");
    store = new ControllerStore(dbPath);
  });

  afterEach(() => {
    store.close();
    rmSync(dir, { recursive: true, force: true });
  });

  test("expands an old jobs schema without changing legacy rows", () => {
    store.close();
    rmSync(dbPath, { force: true });
    const legacy = new Database(dbPath);
    legacy.exec(`
      CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
      CREATE TABLE jobs (
        id TEXT PRIMARY KEY, repo TEXT NOT NULL, host TEXT NOT NULL, snapshot TEXT NOT NULL,
        stage TEXT NOT NULL, attempt INTEGER NOT NULL DEFAULT 1, rc INTEGER,
        infra_failure INTEGER NOT NULL DEFAULT 0
      );
      INSERT INTO jobs (id, repo, host, snapshot, stage) VALUES ('legacy-job', 'repo', 'host', 'sha', 'queued');
    `);
    legacy.close();

    store = new ControllerStore(dbPath);
    expect(store.getJob("legacy-job")).toEqual({
      id: "legacy-job", repo: "repo", host: "host", snapshot: "sha", stage: "queued", attempt: 1,
      rc: null, infraFailure: false, publication: { state: "none" },
    });
    const expanded = new Database(dbPath, { readonly: true });
    const columns = expanded.query<{ name: string; type: string; notnull: number; dflt_value: string | null; pk: number }, []>(`SELECT name, type, "notnull", dflt_value, pk FROM pragma_table_info('jobs')`).all();
    expanded.close();
    expect(columns.slice(0, 8)).toEqual([
      { name: "id", type: "TEXT", notnull: 0, dflt_value: null, pk: 1 },
      { name: "repo", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
      { name: "host", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
      { name: "snapshot", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
      { name: "stage", type: "TEXT", notnull: 1, dflt_value: null, pk: 0 },
      { name: "attempt", type: "INTEGER", notnull: 1, dflt_value: "1", pk: 0 },
      { name: "rc", type: "INTEGER", notnull: 0, dflt_value: null, pk: 0 },
      { name: "infra_failure", type: "INTEGER", notnull: 1, dflt_value: "0", pk: 0 },
    ]);
    expect(columns.slice(8)).toEqual([
      { name: "publication_state", type: "TEXT", notnull: 1, dflt_value: "'none'", pk: 0 },
      { name: "publication_reason", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
      { name: "job_key", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
      { name: "mirror", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
      { name: "started_at", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
      { name: "finished_at", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
      { name: "last_report_at", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
      { name: "timeout_sec", type: "INTEGER", notnull: 0, dflt_value: null, pk: 0 },
    ]);
  });

  test("opens with revision 0 and empty fleet", () => {
    expect(store.getRevision()).toBe(0);
    expect(store.listHosts()).toEqual([]);
    expect(store.getLease().active).toBe(false);
  });

  test("records ordered land conduct health and resets consecutive failures on success", () => {
    store.recordLandConductResult("/repo/z", "2026-08-14T00:00:00.000Z", false, "rc=1");
    store.recordLandConductResult("/repo/a", "2026-08-14T00:01:00.000Z", false, "rc=2");
    store.recordLandConductResult("/repo/z", "2026-08-14T00:02:00.000Z", false, "rc=3");

    expect(store.listLandConductHealth()).toEqual([
      {
        root: "/repo/a", lastPassAt: "2026-08-14T00:01:00.000Z", lastOk: false,
        lastDetail: "rc=2", consecutiveFailures: 1,
      },
      {
        root: "/repo/z", lastPassAt: "2026-08-14T00:02:00.000Z", lastOk: false,
        lastDetail: "rc=3", consecutiveFailures: 2,
      },
    ]);

    store.recordLandConductResult("/repo/z", "2026-08-14T00:03:00.000Z", true, "");
    expect(store.listLandConductHealth()[1]).toEqual({
      root: "/repo/z", lastPassAt: "2026-08-14T00:03:00.000Z", lastOk: true,
      lastDetail: "", consecutiveFailures: 0,
    });
  });

  test("drops wrapper-named breakers on open and keeps executable ones", () => {
    for (const command of ["bash", "env", "playwright"]) {
      store.setCapabilityBreaker({
        hostname: "debian3",
        command,
        state: "half-open",
        failureCount: 2,
        missingEventEmitted: true,
      });
    }
    store.close();
    store = new ControllerStore(dbPath);

    expect(store.getCapabilityBreaker("debian3", "bash")).toBeNull();
    expect(store.getCapabilityBreaker("debian3", "env")).toBeNull();
    expect(store.getCapabilityBreaker("debian3", "playwright")?.state).toBe("half-open");
  });

  test("enforces exact receipt authority at the feature state boundary", () => {
    const definition: FeatureDefinition = { id: "dark-delivery", changeClass: "behavior-change", owner: "owner", fallback: "off", defaultState: "OFF", readinessChecks: ["check", "extra"], observability: ["ledger"], reviewDate: "2026-09-01", cleanupTask: "#5", rollbackWindowDays: 7, removalCondition: "remove", introduced: { task: "#5", sha: "f".repeat(40) }, activationPolicy: "auto-after-receipts" };
    const digest = definitionDigest(definition);
    const sha = "a".repeat(40);
    const tree = "b".repeat(40);
    const artifact = "c".repeat(64);
    const deployment = { deploymentId: "deploy", targetId: "controller", deployedSha: sha, deployedTree: tree, artifactDigest: artifact, status: "ACTIVE" as const, observedAt: "2026-08-11T00:00:00.000Z", evidenceReference: "deploy" };
    const base = { featureId: "dark-delivery", definitionDigest: digest, checkId: "check", candidateSha: sha, candidateTree: tree, deploymentId: "deploy", targetId: "controller", deployedSha: sha, deployedTree: tree, artifactDigest: artifact, result: "PASSED" as const, observedAt: "2026-08-11T00:00:00.000Z", evidenceReference: "evidence" };
    new DeliveryEvidenceAdmissionService(store).admit(deployment, [{ ...base, receiptId: "acceptance", kind: "ACCEPTANCE" }, { ...base, receiptId: "smoke-good", kind: "SMOKE" }], "test-attestation");
    const unauthorizedStore = store as unknown as { transitionDeliveryFeatureStateAuthorized(capability: symbol, input: unknown): void };
    expect(() => unauthorizedStore.transitionDeliveryFeatureStateAuthorized(Symbol("forged"), { definition: { definition, digest, path: "test.json" }, state: "INTERNAL", targetId: "controller", acceptanceReceiptId: "acceptance", smokeReceiptId: "smoke-good", expectedRevision: 0 })).toThrow(/delivery store capability required/);
    expect(store.getDeliveryFeatureState("dark-delivery")).toBeNull();
  });

  test("persists immutable installed entrypoint proof bound to deployment identity", () => {
    const sha = "a".repeat(40);
    const tree = "b".repeat(40);
    const artifact = "c".repeat(64);
    const deployment = { deploymentId: "deploy-installed", targetId: "controller", deployedSha: sha, deployedTree: tree, artifactDigest: artifact, status: "ACTIVE" as const, observedAt: "2026-08-14T00:00:00.000Z", evidenceReference: "deploy-evidence" };
    const admission = new DeliveryEvidenceAdmissionService(store);
    const proof = {
      proofId: "installed-proof", deploymentId: deployment.deploymentId, targetId: deployment.targetId,
      deployedSha: sha, deployedTree: tree, artifactDigest: artifact, entrypoint: "overdeck-controller.service",
      result: "PASSED" as const, observedAt: "2026-08-14T00:01:00.000Z", evidenceReference: "proof-evidence",
    };
    admission.admit(deployment, [{
      receiptId: "installed-acceptance", featureId: "dark-delivery", definitionDigest: "d".repeat(64),
      kind: "ACCEPTANCE", checkId: "installed-check", candidateSha: sha, candidateTree: tree,
      deploymentId: deployment.deploymentId, targetId: deployment.targetId, deployedSha: sha,
      deployedTree: tree, artifactDigest: artifact, result: "PASSED",
      observedAt: "2026-08-14T00:00:00.000Z", evidenceReference: "receipt-evidence",
    }], "installed-attestation", proof);

    expect(store.getDeliveryInstalledProof("installed-proof")).toMatchObject({
      deploymentId: "deploy-installed", entrypoint: "overdeck-controller.service", result: "PASSED",
    });
    expect(() => admission.admit(deployment, [{
      receiptId: "installed-acceptance", featureId: "dark-delivery", definitionDigest: "d".repeat(64),
      kind: "ACCEPTANCE", checkId: "installed-check", candidateSha: sha, candidateTree: tree,
      deploymentId: deployment.deploymentId, targetId: deployment.targetId, deployedSha: sha,
      deployedTree: tree, artifactDigest: artifact, result: "PASSED",
      observedAt: "2026-08-14T00:00:00.000Z", evidenceReference: "receipt-evidence",
    }], "installed-attestation-2", { ...proof, result: "FAILED" })).toThrow(/immutable installed proof ID payload mismatch/);
  });

  test("rejects installed proof that does not match its deployment", () => {
    const sha = "a".repeat(40);
    const tree = "b".repeat(40);
    const artifact = "c".repeat(64);
    const deployment = { deploymentId: "deploy-installed", targetId: "controller", deployedSha: sha, deployedTree: tree, artifactDigest: artifact, status: "ACTIVE" as const, observedAt: "2026-08-14T00:00:00.000Z", evidenceReference: "deploy-evidence" };
    const admission = new DeliveryEvidenceAdmissionService(store);
    expect(() => admission.admit(deployment, [{
      receiptId: "installed-acceptance", featureId: "dark-delivery", definitionDigest: "d".repeat(64),
      kind: "ACCEPTANCE", checkId: "installed-check", candidateSha: sha, candidateTree: tree,
      deploymentId: deployment.deploymentId, targetId: deployment.targetId, deployedSha: sha,
      deployedTree: tree, artifactDigest: artifact, result: "PASSED",
      observedAt: "2026-08-14T00:00:00.000Z", evidenceReference: "receipt-evidence",
    }], "orphan-attestation", {
      proofId: "orphan-proof", deploymentId: "missing", targetId: "controller",
      deployedSha: sha, deployedTree: tree, artifactDigest: artifact, entrypoint: "overdeck-controller.service",
      result: "PASSED", observedAt: "2026-08-14T00:01:00.000Z", evidenceReference: "proof-evidence",
    })).toThrow(/exact deployment required/);
  });

  test("reuses only an exact successful gate receipt and reports origin age", () => {
    const identity: GateReceiptIdentity = {
      candidateTree: "a".repeat(40), gateGraphDigest: "b".repeat(64),
      declaredInputsDigest: "c".repeat(64), lockfileDigest: "d".repeat(64),
      toolchainDigest: "e".repeat(64), fixtureSchemaDigest: "f".repeat(64),
      commandDigest: "1".repeat(64), warningDigest: "2".repeat(64),
      logDigest: "3".repeat(64), artifactDigest: "4".repeat(64),
    };
    store.recordGateReceipt({ receiptId: "gate-1", identity, result: "PASSED", recordedAt: "2026-08-14T00:00:00.000Z", origin: "debian1" });

    expect(store.findReusableGateReceipt(identity, Date.parse("2026-08-14T00:02:00.000Z"))).toMatchObject({
      receiptId: "gate-1", origin: "debian1", ageMs: 120_000,
    });
    expect(store.findReusableGateReceipt({ ...identity, lockfileDigest: "9".repeat(64) }, Date.parse("2026-08-14T00:02:00.000Z"))).toBeNull();
  });

  test("never reuses a failed gate receipt", () => {
    const identity: GateReceiptIdentity = {
      candidateTree: "a".repeat(40), gateGraphDigest: "b".repeat(64),
      declaredInputsDigest: "c".repeat(64), lockfileDigest: "d".repeat(64),
      toolchainDigest: "e".repeat(64), fixtureSchemaDigest: "f".repeat(64),
      commandDigest: "1".repeat(64), warningDigest: "2".repeat(64),
      logDigest: "3".repeat(64), artifactDigest: "4".repeat(64),
    };
    store.recordGateReceipt({ receiptId: "gate-failed", identity, result: "FAILED", recordedAt: "2026-08-14T00:00:00.000Z", origin: "debian1" });
    expect(store.findReusableGateReceipt(identity, Date.parse("2026-08-14T00:02:00.000Z"))).toBeNull();
  });

  test("rejects non-canonical and future gate receipt timestamps", () => {
    const identity: GateReceiptIdentity = {
      candidateTree: "a".repeat(40), gateGraphDigest: "b".repeat(64),
      declaredInputsDigest: "c".repeat(64), lockfileDigest: "d".repeat(64),
      toolchainDigest: "e".repeat(64), fixtureSchemaDigest: "f".repeat(64),
      commandDigest: "1".repeat(64), warningDigest: "2".repeat(64),
      logDigest: "3".repeat(64), artifactDigest: "4".repeat(64),
    };
    expect(() => store.recordGateReceipt({ receiptId: "offset", identity, result: "PASSED", recordedAt: "2026-08-14T01:00:00+01:00", origin: "debian1" })).toThrow(/invalid gate receipt/);
    store.recordGateReceipt({ receiptId: "future", identity, result: "PASSED", recordedAt: "2026-08-14T00:03:00.000Z", origin: "debian1" });
    expect(store.findReusableGateReceipt(identity, Date.parse("2026-08-14T00:02:00.000Z"))).toBeNull();
  });

  test("persists revision across reopen", () => {
    store.commitTransition({
      journalId: store.journalIntent({
        verb: "admission-reconcile",
        idempotencyKey: "seed-1",
        args: {},
        expectedRevision: 0,
      }),
      idempotencyKey: "seed-1",
      result: { reconciled: true },
      apply: () => {},
    });
    expect(store.getRevision()).toBe(1);
    store.close();

    const reopened = new ControllerStore(dbPath);
    expect(reopened.getRevision()).toBe(1);
    reopened.close();
  });

  test("rejects permanent fallback lease", () => {
    expect(() =>
      store.setLease({ active: true, expiresAt: "", host: "laptop", reason: "spill" }),
    ).toThrow(/must always expire/);
  });

  test("expires fallback lease after ttl", () => {
    let now = Date.now();
    const timed = new ControllerStore(dbPath, { now: () => now });
    timed.setLease({
      active: true,
      expiresAt: new Date(now + 1000).toISOString(),
      host: "laptop",
      reason: "spill",
    });
    expect(timed.getLease().active).toBe(true);
    now += 2000;
    expect(timed.getLease().active).toBe(false);
    timed.close();
  });

  test("idempotency replay survives reopen within 24h", () => {
    const key = "idem-1";
    store.commitTransition({
      journalId: store.journalIntent({
        verb: "admission-reconcile",
        idempotencyKey: key,
        args: {},
        expectedRevision: 0,
      }),
      idempotencyKey: key,
      result: { reconciled: true, advanced: [] },
      apply: () => {},
    });
    store.close();

    const reopened = new ControllerStore(dbPath);
    const replay = reopened.getIdempotencyResult(key);
    expect(replay?.revision).toBe(1);
    expect(replay?.result).toEqual({ reconciled: true, advanced: [] });
    reopened.close();
  });

  test("audit write failure throws AuditWriteError", () => {
    const failing = new ControllerStore(dbPath, { auditWriteFails: true });
    expect(() =>
      failing.journalIntent({
        verb: "box-drain",
        idempotencyKey: "audit-fail",
        args: { host: "debian1" },
        expectedRevision: 0,
      }),
    ).toThrow(AuditWriteError);
    failing.close();
  });

  test("mutations are transactional", () => {
    store.upsertHost({ hostname: "debian1", state: "available" });
    expect(store.getHost("debian1")?.state).toBe("available");
    store.setHostState("debian1", "draining");
    expect(store.getHost("debian1")?.state).toBe("draining");
  });

  test("nested mutations share one transaction and return values", () => {
    const value = store.mutate(() => {
      store.upsertHost({ hostname: "nested", state: "maintenance" });
      return store.getHost("nested")?.hostname;
    });
    expect(value).toBe("nested");
  });

  test("persists report job fields and succeeded releases reservations", () => {
    store.upsertJob({
      id: "report-job",
      key: "key-1",
      mirror: "mirror-1",
      repo: "owner/repo",
      host: "remote",
      snapshot: "42",
      stage: "succeeded",
      attempt: 2,
      rc: 0,
      infraFailure: false,
      startedAt: "2026-07-20T00:00:00.000Z",
      finishedAt: "2026-07-20T00:00:05.000Z",
      lastReportAt: "2026-07-20T00:00:06.000Z",
      timeoutSec: 30,
    });
    expect(store.getJob("report-job")).toMatchObject({
      key: "key-1",
      mirror: "mirror-1",
      stage: "succeeded",
      startedAt: "2026-07-20T00:00:00.000Z",
      finishedAt: "2026-07-20T00:00:05.000Z",
      lastReportAt: "2026-07-20T00:00:06.000Z",
      timeoutSec: 30,
    });
  });

  test("stores only the latest spine config hash per path", () => {
    expect(store.getSpineConfigState("/etc/overdeck.json")).toBeNull();
    store.setSpineConfigState({
      configPath: "/etc/overdeck.json",
      lastAppliedBodyHash: "abc",
    });
    expect(store.getSpineConfigState("/etc/overdeck.json")).toEqual({
      configPath: "/etc/overdeck.json",
      lastAppliedBodyHash: "abc",
    });
  });

  test("host eligibility evaluates guards in contract order without reservations", () => {
    expect(store.hostEligible("missing", "node")).toEqual({
      eligible: false,
      reason: "unknown-host",
    });
    store.upsertHost({
      hostname: "builder",
      state: "available",
      capabilityOk: true,
      slotsTotal: 1,
    });
    expect(store.hostEligible("builder", "node")).toEqual({
      eligible: true,
      reason: "eligible",
    });
    store.setCapabilityBreaker({
      hostname: "builder",
      command: "node",
      state: "open",
      failureCount: 2,
      missingEventEmitted: true,
    });
    expect(store.hostEligible("builder", "node")).toEqual({
      eligible: false,
      reason: "command-quarantined",
    });
    expect(store.hostEligible("builder", "bun")).toEqual({
      eligible: true,
      reason: "eligible",
    });
    expect(store.listHostSlotReservations()).toEqual([]);
  });

  test("transition commit CAS cancels stale journal without state writes", () => {
    const journalId = store.journalIntent({
      verb: "admission-reconcile",
      idempotencyKey: "cas-stale",
      args: {},
      expectedRevision: 0,
    });
    store.appendEvent({
      ts: "2026-07-20T00:00:00.000Z",
      job: "",
      repo: "",
      host: "",
      snapshot: "",
      attempt: 0,
      stage: "external",
      reason: "external",
      rc: null,
      durationSeconds: 0,
    });
    expect(() => store.commitTransition({
      journalId,
      idempotencyKey: "cas-stale",
      acceptedRevision: 0,
      result: {},
      apply: () => store.upsertHost({ hostname: "should-not-exist" }),
    })).toThrow(StaleRevisionError);
    expect(store.getHost("should-not-exist")).toBeNull();
    expect(store.listPendingJournals()).toEqual([]);
    expect(store.getIdempotencyResult("cas-stale")).toBeNull();
  });

  test("clears spine-config degradation whose config file no longer exists", () => {
    const vanished = join(dir, "vanished", "invalid.json");
    const present = join(dir, "state.sqlite");
    store.setSpineConfigDegraded(vanished);
    store.upsertIncident({
      key: `spine-config:${vanished}`,
      lastSeen: "2026-07-21T02:56:26.319Z",
      affectedJobs: [],
      remediation: "fix config",
      cooldownUntil: null,
      autoResolveCondition: `valid report for ${vanished}`,
      state: "open",
    });
    store.upsertIncident({
      key: `spine-config:${present}`,
      lastSeen: "2026-07-21T02:56:26.319Z",
      affectedJobs: [],
      remediation: "fix config",
      cooldownUntil: null,
      autoResolveCondition: `valid report for ${present}`,
      state: "open",
    });

    expect(store.clearVanishedSpineConfigDegradation("2026-08-08T00:00:00.000Z")).toEqual([
      vanished,
    ]);
    expect(store.getMetaSnapshot().dispatchState).toBe("healthy");
    expect(store.getIncident(`spine-config:${vanished}`)?.state).toBe("resolved");
    expect(store.getIncident(`spine-config:${present}`)?.state).toBe("open");
  });

  test("emits only one config incident", () => {
    expect(store.emitConfigIncident("config-invalid-shape", "bad")).toBe(true);
    expect(store.emitConfigIncident("config-invalid-shape", "bad")).toBe(false);
    expect(store.hasConfigIncident()).toBe(true);
    expect(store.getMetaSnapshot().observed).toBe("degraded");
  });
});
