import { describe, expect, it, vi } from "vitest";

// Stub `~/db.server` before importing anything that transitively imports it.
// The real module eagerly calls `prisma.$connect()` at singleton construction
// (db.server.ts), so loading it under vitest tries to reach localhost:5432
// and surfaces as an unhandled rejection that fails the whole shard — even
// though no test in this file actually uses the default prisma client.
vi.mock("~/db.server", () => ({
  prisma: {},
  $replica: {},
}));

import {
  evaluateGate,
  makeResolveMollifierFlag,
  type GateDependencies,
  type GateInputs,
  type TripDecision,
} from "~/v3/mollifier/mollifierGate.server";
import type { DecisionOutcome, DecisionReason } from "~/v3/mollifier/mollifierTelemetry.server";

// We deliberately don't use vi.fn here. Per repo policy tests shouldn't lean on
// mock frameworks for behaviours that are pure functions of the inputs — the
// gate is pure decision logic, so a hand-rolled "deps + spy log" wired with
// plain closures gives exactly the assertions we need without the indirection.
type Spies = {
  evaluatorCalls: number;
  logShadowCalls: Array<{ inputs: GateInputs; decision: Extract<TripDecision, { divert: true }> }>;
  logMollifiedCalls: Array<{
    inputs: GateInputs;
    decision: Extract<TripDecision, { divert: true }>;
  }>;
  recordDecisionCalls: Array<{
    outcome: DecisionOutcome;
    reason?: DecisionReason;
    enrolled?: boolean;
    orgId?: string;
  }>;
};

type Toggles = {
  enabled: boolean;
  shadow: boolean;
  flag: boolean;
  decision: TripDecision;
};

function makeDeps(toggles: Toggles): { deps: GateDependencies; spies: Spies } {
  const spies: Spies = {
    evaluatorCalls: 0,
    logShadowCalls: [],
    logMollifiedCalls: [],
    recordDecisionCalls: [],
  };
  const deps: GateDependencies = {
    isMollifierEnabled: () => toggles.enabled,
    isShadowModeOn: () => toggles.shadow,
    resolveOrgFlag: async () => toggles.flag,
    evaluator: async () => {
      spies.evaluatorCalls += 1;
      return toggles.decision;
    },
    logShadow: (inputs, decision) => {
      spies.logShadowCalls.push({ inputs, decision });
    },
    logMollified: (inputs, decision) => {
      spies.logMollifiedCalls.push({ inputs, decision });
    },
    recordDecision: (outcome, opts) => {
      spies.recordDecisionCalls.push({
        outcome,
        reason: opts.reason,
        enrolled: opts.enrolled,
        orgId: opts.orgId,
      });
    },
  };
  return { deps, spies };
}

const trippedDecision = {
  divert: true as const,
  reason: "per_env_rate" as const,
  count: 150,
  threshold: 100,
  windowMs: 200,
  holdMs: 500,
};

const passDecision: TripDecision = { divert: false };

const inputs: GateInputs = {
  envId: "e1",
  orgId: "o1",
  taskId: "t1",
  orgFeatureFlags: null,
};

// Cascade truth table. Every combination of (enabled, shadow, flag, divert) is
// enumerated. `evaluatorCalls` is the expected count, not arbitrary: the gate
// short-circuits before the evaluator if `!enabled` or (`!flag && !shadow`).
// `expectedReason` is the optional second arg to `recordDecision` — only
// divert-true paths attach a reason.
type Row = {
  id: number;
  enabled: boolean;
  shadow: boolean;
  flag: boolean;
  divert: boolean;
  expected: {
    action: "pass_through" | "shadow_log" | "mollify";
    evaluatorCalls: 0 | 1;
    logShadowCalls: 0 | 1;
    logMollifiedCalls: 0 | 1;
    recordedOutcome: "pass_through" | "shadow_log" | "mollify";
    expectedReason: "per_env_rate" | undefined;
  };
};

// 16 rows = 2^4 input combinations. Comment column shows which gate branch
// each row exercises so reviewers can map row → code at a glance.
const cascade: Row[] = [
  // enabled=F → kill-switch wins; evaluator+flag never consulted (rows 1-8)
  {
    id: 1,
    enabled: false,
    shadow: false,
    flag: false,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 2,
    enabled: false,
    shadow: false,
    flag: false,
    divert: true,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 3,
    enabled: false,
    shadow: false,
    flag: true,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 4,
    enabled: false,
    shadow: false,
    flag: true,
    divert: true,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 5,
    enabled: false,
    shadow: true,
    flag: false,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 6,
    enabled: false,
    shadow: true,
    flag: false,
    divert: true,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 7,
    enabled: false,
    shadow: true,
    flag: true,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 8,
    enabled: false,
    shadow: true,
    flag: true,
    divert: true,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  // enabled=T, flag=F, shadow=F → both opt-ins off; evaluator never called (rows 9-10)
  {
    id: 9,
    enabled: true,
    shadow: false,
    flag: false,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 10,
    enabled: true,
    shadow: false,
    flag: false,
    divert: true,
    expected: {
      action: "pass_through",
      evaluatorCalls: 0,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  // enabled=T, flag=F, shadow=T → shadow path; divert routes outcome (rows 11-12)
  {
    id: 11,
    enabled: true,
    shadow: true,
    flag: false,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 1,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 12,
    enabled: true,
    shadow: true,
    flag: false,
    divert: true,
    expected: {
      action: "shadow_log",
      evaluatorCalls: 1,
      logShadowCalls: 1,
      logMollifiedCalls: 0,
      recordedOutcome: "shadow_log",
      expectedReason: "per_env_rate",
    },
  },
  // enabled=T, flag=T, shadow=F → mollify path (rows 13-14)
  {
    id: 13,
    enabled: true,
    shadow: false,
    flag: true,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 1,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 14,
    enabled: true,
    shadow: false,
    flag: true,
    divert: true,
    expected: {
      action: "mollify",
      evaluatorCalls: 1,
      logShadowCalls: 0,
      logMollifiedCalls: 1,
      recordedOutcome: "mollify",
      expectedReason: "per_env_rate",
    },
  },
  // enabled=T, flag=T, shadow=T → flag wins over shadow (rows 15-16)
  {
    id: 15,
    enabled: true,
    shadow: true,
    flag: true,
    divert: false,
    expected: {
      action: "pass_through",
      evaluatorCalls: 1,
      logShadowCalls: 0,
      logMollifiedCalls: 0,
      recordedOutcome: "pass_through",
      expectedReason: undefined,
    },
  },
  {
    id: 16,
    enabled: true,
    shadow: true,
    flag: true,
    divert: true,
    expected: {
      action: "mollify",
      evaluatorCalls: 1,
      logShadowCalls: 0,
      logMollifiedCalls: 1,
      recordedOutcome: "mollify",
      expectedReason: "per_env_rate",
    },
  },
];

describe("evaluateGate cascade — exhaustive truth table", () => {
  it.each(cascade)(
    "row $id: enabled=$enabled shadow=$shadow flag=$flag divert=$divert → action=$expected.action",
    async (row) => {
      const { deps, spies } = makeDeps({
        enabled: row.enabled,
        shadow: row.shadow,
        flag: row.flag,
        decision: row.divert ? trippedDecision : passDecision,
      });

      const outcome = await evaluateGate(inputs, deps);

      expect(outcome.action).toBe(row.expected.action);
      expect(spies.evaluatorCalls).toBe(row.expected.evaluatorCalls);
      expect(spies.logShadowCalls).toHaveLength(row.expected.logShadowCalls);
      expect(spies.logMollifiedCalls).toHaveLength(row.expected.logMollifiedCalls);

      // Every evaluation records exactly one decision.
      expect(spies.recordDecisionCalls).toHaveLength(1);
      expect(spies.recordDecisionCalls[0].outcome).toBe(row.expected.recordedOutcome);
      expect(spies.recordDecisionCalls[0].reason).toBe(row.expected.expectedReason);
      // enrolled label = the resolved per-org flag, now hoisted above the
      // bypasses so it's set on every decision. orgId is always passed by the
      // gate; the telemetry layer drops it for non-enrolled (covered in
      // mollifierDecisionLabels.test.ts).
      expect(spies.recordDecisionCalls[0].enrolled).toBe(row.flag);
      expect(spies.recordDecisionCalls[0].orgId).toBe(inputs.orgId);
    }
  );

  it("divert log carries the full decision (envId, orgId, taskId, reason, count, threshold, windowMs, holdMs)", async () => {
    const { deps, spies } = makeDeps({
      enabled: true,
      shadow: true,
      flag: false,
      decision: trippedDecision,
    });

    await evaluateGate(inputs, deps);

    expect(spies.logShadowCalls).toEqual([{ inputs, decision: trippedDecision }]);
  });

  it("mollify log carries the full decision (mirrors shadow log)", async () => {
    const { deps, spies } = makeDeps({
      enabled: true,
      shadow: false,
      flag: true,
      decision: trippedDecision,
    });

    await evaluateGate(inputs, deps);

    expect(spies.logMollifiedCalls).toEqual([{ inputs, decision: trippedDecision }]);
  });
});

// Hot-path guard: `triggerTask.server.ts` calls `evaluateGate` on every
// trigger when `TRIGGER_MOLLIFIER_ENABLED=1`. The per-org override path must resolve
// without a Prisma round-trip — otherwise the gate adds a DB query to the
// highest-throughput code path in the system (see apps/webapp/CLAUDE.md).
describe("resolveMollifierFlag — hot path", () => {
  it("returns the per-org override when it's set", async () => {
    const resolve = makeResolveMollifierFlag();

    const enabled = await resolve({
      envId: "e",
      orgId: "o",
      taskId: "t",
      orgFeatureFlags: { mollifierEnabled: true },
    });
    const disabled = await resolve({
      envId: "e",
      orgId: "o",
      taskId: "t",
      orgFeatureFlags: { mollifierEnabled: false },
    });

    expect(enabled).toBe(true);
    expect(disabled).toBe(false);
  });

  it("returns false when the org has no override for the key — no DB query, ever", async () => {
    // Regression intent: the resolver MUST NOT call `flag()` (which would
    // query `FeatureFlag` via Prisma) on the trigger hot path. Per-org
    // rollout via `Organization.featureFlags` JSON is the only enable
    // path; the fleet-wide kill switch is `TRIGGER_MOLLIFIER_ENABLED`.
    const resolve = makeResolveMollifierFlag();

    const fromNull = await resolve({
      envId: "e",
      orgId: "o",
      taskId: "t",
      orgFeatureFlags: null,
    });
    const fromUnrelatedKeys = await resolve({
      envId: "e",
      orgId: "o",
      taskId: "t",
      orgFeatureFlags: { hasAiAccess: true },
    });

    expect(fromNull).toBe(false);
    expect(fromUnrelatedKeys).toBe(false);
  });
});

describe("evaluateGate — fail open on evaluator error", () => {
  it("treats a throwing evaluator as no-divert (pass_through), and never blocks the trigger", async () => {
    const spies: Spies = {
      evaluatorCalls: 0,
      logShadowCalls: [],
      logMollifiedCalls: [],
      recordDecisionCalls: [],
    };
    const deps: Partial<GateDependencies> = {
      isMollifierEnabled: () => true,
      isShadowModeOn: () => false,
      resolveOrgFlag: async () => true,
      evaluator: async () => {
        spies.evaluatorCalls += 1;
        throw new Error("simulated evaluator failure");
      },
      logShadow: (inputs, decision) => {
        spies.logShadowCalls.push({ inputs, decision });
      },
      logMollified: (inputs, decision) => {
        spies.logMollifiedCalls.push({ inputs, decision });
      },
      recordDecision: (outcome, opts) => {
        spies.recordDecisionCalls.push({
          outcome,
          reason: opts.reason,
          enrolled: opts.enrolled,
          orgId: opts.orgId,
        });
      },
    };

    const outcome = await evaluateGate(inputs, deps);

    expect(outcome.action).toBe("pass_through");
    expect(spies.evaluatorCalls).toBe(1);
    expect(spies.logMollifiedCalls).toHaveLength(0);
    expect(spies.logShadowCalls).toHaveLength(0);
    expect(spies.recordDecisionCalls).toHaveLength(1);
    expect(spies.recordDecisionCalls[0]).toMatchObject({
      outcome: "pass_through",
      reason: undefined,
      enrolled: true,
      orgId: inputs.orgId,
    });
  });
});

describe("evaluateGate — fail open on resolveOrgFlag error", () => {
  it("treats org flag as false when resolveOrgFlag throws, and does not block triggers", async () => {
    const spies: Spies = {
      evaluatorCalls: 0,
      logShadowCalls: [],
      logMollifiedCalls: [],
      recordDecisionCalls: [],
    };
    const deps: Partial<GateDependencies> = {
      isMollifierEnabled: () => true,
      isShadowModeOn: () => false,
      resolveOrgFlag: async () => {
        throw new Error("simulated prisma timeout");
      },
      evaluator: async () => {
        spies.evaluatorCalls += 1;
        return trippedDecision;
      },
      logShadow: (inputs, decision) => {
        spies.logShadowCalls.push({ inputs, decision });
      },
      logMollified: (inputs, decision) => {
        spies.logMollifiedCalls.push({ inputs, decision });
      },
      recordDecision: (outcome, opts) => {
        spies.recordDecisionCalls.push({
          outcome,
          reason: opts.reason,
          enrolled: opts.enrolled,
          orgId: opts.orgId,
        });
      },
    };

    const outcome = await evaluateGate(inputs, deps);

    expect(outcome.action).toBe("pass_through");
    expect(spies.evaluatorCalls).toBe(0);
    expect(spies.recordDecisionCalls).toHaveLength(1);
    expect(spies.recordDecisionCalls[0]).toMatchObject({
      outcome: "pass_through",
      reason: undefined,
      enrolled: false,
      orgId: inputs.orgId,
    });
  });
});

describe("evaluateGate — per-org isolation via Organization.featureFlags", () => {
  function makeIsolationDeps(resolveOrgFlag: GateDependencies["resolveOrgFlag"]): {
    deps: Partial<GateDependencies>;
    spies: Spies;
  } {
    const spies: Spies = {
      evaluatorCalls: 0,
      logShadowCalls: [],
      logMollifiedCalls: [],
      recordDecisionCalls: [],
    };
    // Override lifecycle bits and inject the production resolveOrgFlag.
    // Evaluator returns a fixed tripped decision so the outcome is purely a
    // function of the flag resolution (which is what we're isolating on).
    const deps: Partial<GateDependencies> = {
      isMollifierEnabled: () => true,
      isShadowModeOn: () => false,
      resolveOrgFlag,
      evaluator: async () => {
        spies.evaluatorCalls += 1;
        return trippedDecision;
      },
      logShadow: (inputs, decision) => {
        spies.logShadowCalls.push({ inputs, decision });
      },
      logMollified: (inputs, decision) => {
        spies.logMollifiedCalls.push({ inputs, decision });
      },
      recordDecision: (outcome, opts) => {
        spies.recordDecisionCalls.push({
          outcome,
          reason: opts.reason,
          enrolled: opts.enrolled,
          orgId: opts.orgId,
        });
      },
    };
    return { deps, spies };
  }

  // The production resolver — purely in-memory, no Prisma. Mirrors
  // `defaultGateDependencies.resolveOrgFlag` exactly.
  const resolve = makeResolveMollifierFlag();

  it("opts in only the org whose featureFlags has mollifierEnabled=true", async () => {
    const orgA = { ...inputs, orgId: "org_a", orgFeatureFlags: { mollifierEnabled: true } };
    const orgB = { ...inputs, orgId: "org_b", orgFeatureFlags: { mollifierEnabled: false } };
    const orgC = { ...inputs, orgId: "org_c", orgFeatureFlags: null };

    const a = makeIsolationDeps(resolve);
    const b = makeIsolationDeps(resolve);
    const c = makeIsolationDeps(resolve);

    const [outcomeA, outcomeB, outcomeC] = await Promise.all([
      evaluateGate(orgA, a.deps),
      evaluateGate(orgB, b.deps),
      evaluateGate(orgC, c.deps),
    ]);

    // Only org A's flag is on → only org A mollifies. Orgs B and C never
    // reach the evaluator because both flag and shadow-mode are off.
    expect(outcomeA.action).toBe("mollify");
    expect(outcomeB.action).toBe("pass_through");
    expect(outcomeC.action).toBe("pass_through");

    expect(a.spies.evaluatorCalls).toBe(1);
    expect(b.spies.evaluatorCalls).toBe(0);
    expect(c.spies.evaluatorCalls).toBe(0);

    expect(a.spies.logMollifiedCalls).toHaveLength(1);
    expect(b.spies.logMollifiedCalls).toHaveLength(0);
    expect(c.spies.logMollifiedCalls).toHaveLength(0);
  });

  it("another org's beta flags must not opt them into mollifier", async () => {
    // Org A has mollifier on (plus an unrelated beta).
    const orgA = {
      ...inputs,
      orgId: "org_a",
      orgFeatureFlags: { mollifierEnabled: true, hasComputeAccess: true },
    };
    // Org B has *other* betas on but mollifier remains off — keys that gate
    // compute/AI/query must not bleed across into the mollifier decision.
    const orgB = {
      ...inputs,
      orgId: "org_b",
      orgFeatureFlags: { hasComputeAccess: true, hasAiAccess: true },
    };

    const a = makeIsolationDeps(resolve);
    const b = makeIsolationDeps(resolve);

    const outcomeA = await evaluateGate(orgA, a.deps);
    const outcomeB = await evaluateGate(orgB, b.deps);

    expect(outcomeA.action).toBe("mollify");
    expect(outcomeB.action).toBe("pass_through");
  });

  it("orgs without an explicit override stay off — no global FeatureFlag fallback", async () => {
    // Regression intent: the resolver MUST NOT consult the global
    // `FeatureFlag` table on the hot path. An org with `orgFeatureFlags`
    // unset (the default for almost every org during rollout) gets
    // pass_through, period. The fleet-wide kill switch lives in
    // `TRIGGER_MOLLIFIER_ENABLED`, not the FeatureFlag table.
    const orgInherits = { ...inputs, orgId: "org_inherits", orgFeatureFlags: null };
    const orgEmpty = { ...inputs, orgId: "org_empty", orgFeatureFlags: {} };
    const orgUnrelated = {
      ...inputs,
      orgId: "org_unrelated",
      orgFeatureFlags: { hasAiAccess: true },
    };

    const inheritsDeps = makeIsolationDeps(resolve);
    const emptyDeps = makeIsolationDeps(resolve);
    const unrelatedDeps = makeIsolationDeps(resolve);

    const [outInherits, outEmpty, outUnrelated] = await Promise.all([
      evaluateGate(orgInherits, inheritsDeps.deps),
      evaluateGate(orgEmpty, emptyDeps.deps),
      evaluateGate(orgUnrelated, unrelatedDeps.deps),
    ]);

    expect(outInherits.action).toBe("pass_through");
    expect(outEmpty.action).toBe("pass_through");
    expect(outUnrelated.action).toBe("pass_through");
    // None of these reached the evaluator (flag off, shadow off).
    expect(inheritsDeps.spies.evaluatorCalls).toBe(0);
    expect(emptyDeps.spies.evaluatorCalls).toBe(0);
    expect(unrelatedDeps.spies.evaluatorCalls).toBe(0);
  });
});

// Bypasses: the three categories of trigger that the mollifier never
// intercepts, regardless of the per-org flag or the trip-evaluator decision.
describe("evaluateGate — debounce / OTU / triggerAndWait bypasses", () => {
  it("debounce triggers pass through without invoking the evaluator", async () => {
    const { deps, spies } = makeDeps({
      enabled: true,
      shadow: false,
      flag: true,
      decision: trippedDecision,
    });
    const outcome = await evaluateGate({ ...inputs, options: { debounce: { key: "k" } } }, deps);
    expect(outcome).toEqual({ action: "pass_through" });
    expect(spies.evaluatorCalls).toBe(0);
  });

  it("oneTimeUseToken triggers pass through without invoking the evaluator", async () => {
    const { deps, spies } = makeDeps({
      enabled: true,
      shadow: false,
      flag: true,
      decision: trippedDecision,
    });
    const outcome = await evaluateGate(
      { ...inputs, options: { oneTimeUseToken: "jwt-otu" } },
      deps
    );
    expect(outcome).toEqual({ action: "pass_through" });
    expect(spies.evaluatorCalls).toBe(0);
  });

  it("single triggerAndWait (parentTaskRunId + resumeParentOnCompletion) passes through", async () => {
    const { deps, spies } = makeDeps({
      enabled: true,
      shadow: false,
      flag: true,
      decision: trippedDecision,
    });
    const outcome = await evaluateGate(
      {
        ...inputs,
        options: { parentTaskRunId: "run_parent", resumeParentOnCompletion: true },
      },
      deps
    );
    expect(outcome).toEqual({ action: "pass_through" });
    expect(spies.evaluatorCalls).toBe(0);
  });

  it("parentTaskRunId alone (no resumeParentOnCompletion) does NOT bypass — must be both", async () => {
    const { deps, spies } = makeDeps({
      enabled: true,
      shadow: false,
      flag: true,
      decision: trippedDecision,
    });
    const outcome = await evaluateGate(
      { ...inputs, options: { parentTaskRunId: "run_parent" } },
      deps
    );
    expect(outcome.action).toBe("mollify");
    expect(spies.evaluatorCalls).toBe(1);
  });

  it("bypass records pass_through decision (so observability counters stay accurate)", async () => {
    const { deps, spies } = makeDeps({
      enabled: true,
      shadow: false,
      flag: true,
      decision: trippedDecision,
    });
    await evaluateGate({ ...inputs, options: { debounce: { key: "k" } } }, deps);
    expect(spies.recordDecisionCalls).toHaveLength(1);
    expect(spies.recordDecisionCalls[0].outcome).toBe("pass_through");
  });
});
