/**
 * WP-07 Admission Policy — policy unit tests + old/new comparison matrix.
 */

import { describe, it } from "node:test";
import assert from "node:assert/strict";

import {
  canDispatchQA,
  qaGateToDispatchSkipReason,
} from "../../pm/qaDispatchGate.ts";
import type { ExecutionGateContext } from "../../pm/taskDispatchContext.ts";
import {
  evaluateDispatchEligibility,
  resolveExplicitDispatchHoldReason,
  type DispatchGateReportRef,
  type DispatchGateTaskRef,
} from "../../pm/taskDispatchGate.ts";
import {
  LegacyOpenDevTeamAdmissionPolicy,
  evaluateLegacyDispatcherSlice,
  evaluateLegacyOpenDevTeamGates,
  mapLegacyGateVerdictToAdmission,
  type LegacyOpenDevTeamAdmissionInput,
} from "../policy/index.ts";
import {
  LegacyOpenDevTeamAdmissionPolicy as FromPublic,
  type WorkAdmissionPolicy,
} from "../index.ts";

const THREAD = "panel-task-wp07";

function task(
  recipient: string,
  overrides: Partial<DispatchGateTaskRef> = {},
): DispatchGateTaskRef {
  const taskId = overrides.taskId ?? `TASK-20260805-001-PM-to-${recipient}`;
  return {
    taskId,
    filename: overrides.filename ?? `${taskId}.md`,
    recipient,
    sender: overrides.sender ?? "PM",
    threadKey: THREAD,
    lifecycleBucket: "inbox",
    fmState: "inbox",
    ...overrides,
  };
}

function report(
  reporter: string,
  status = "done",
  taskId?: string,
): DispatchGateReportRef {
  return {
    taskId: taskId ?? `TASK-20260805-001-PM-to-${reporter}`,
    reporter,
    status,
    threadKey: THREAD,
  };
}

function ctx(
  tasks: DispatchGateTaskRef[],
  reports: DispatchGateReportRef[],
  meta: Record<
    string,
    { artifactPath?: string; cancelled?: boolean; supersededBy?: string }
  > = {},
): ExecutionGateContext {
  return {
    tasks,
    reports,
    taskMeta: new Map(Object.entries(meta).map(([id, m]) => [id, { ...m }])),
    projectRoot: "/proj",
    artifactExists: (rel) => rel === "out/ok.txt",
  };
}

function admissionInput(
  target: DispatchGateTaskRef,
  threadTasks: DispatchGateTaskRef[],
  reports: DispatchGateReportRef[],
  extras: Partial<LegacyOpenDevTeamAdmissionInput> = {},
): LegacyOpenDevTeamAdmissionInput {
  return {
    target,
    threadTasks,
    reports,
    executionContext: ctx(threadTasks, reports),
    ...extras,
  };
}

describe("WP-07 WorkAdmissionPolicy exports", () => {
  it("re-exports LegacyOpenDevTeamAdmissionPolicy from switchboard barrel", () => {
    assert.equal(FromPublic, LegacyOpenDevTeamAdmissionPolicy);
    // Concrete policies are typed to their input shape (not the empty base).
    const policy: WorkAdmissionPolicy<LegacyOpenDevTeamAdmissionInput> =
      new LegacyOpenDevTeamAdmissionPolicy();
    assert.equal(typeof policy.inspect, "function");
  });
});

describe("WP-07 LegacyOpenDevTeamAdmissionPolicy", () => {
  it("allows trusted PM→DEV inbox task", async () => {
    const target = task("DEV");
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(admissionInput(target, [target], []));
    assert.equal(decision.decision, "allow");
    assert.equal(decision.reason, "allowed");
  });

  it("holds on product_brief_required", async () => {
    const target = task("DEV");
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(
      admissionInput(target, [target], [], {
        productGate: {
          allowed: false,
          reason: "product_brief_required",
          findings: ["brief_missing"],
        },
      }),
    );
    assert.equal(decision.decision, "hold");
    assert.equal(decision.reason, "product_brief_required");
  });

  it("rejects closed root product gate", async () => {
    const target = task("DEV");
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(
      admissionInput(target, [target], [], {
        productGate: {
          allowed: false,
          reason: "cancelled",
          code: "ROOT_TASK_CLOSED",
        },
      }),
    );
    assert.equal(decision.decision, "reject");
  });

  it("holds on dependency_pending", async () => {
    const target = task("QA", { dependsOn: ["TASK-20260805-001-PM-to-DEV"] });
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(
      admissionInput(target, [target], [], {
        dependencyGate: {
          allowed: false,
          reason: "waiting for done report: TASK-20260805-001-PM-to-DEV",
          dependencyTaskIds: ["TASK-20260805-001-PM-to-DEV"],
        },
      }),
    );
    assert.equal(decision.decision, "hold");
    assert.equal(decision.reason, "dependency_pending");
  });

  it("holds untrusted route via explicit hold", async () => {
    const target = task("EVAL", { sender: "PM" });
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(
      admissionInput(target, [target], [], {
        checkExplicitHold: true,
        explicitHoldInput: {
          sender: "PM",
          recipient: "EVAL",
          filename: target.filename,
          protocol: "fcop",
        },
      }),
    );
    assert.equal(decision.decision, "hold");
    assert.equal(decision.reason, "task_not_dispatched");
  });

  it("allows QA when DEV report done (open-dev-team rules stay in policy)", async () => {
    const dev = task("DEV", {
      lifecycleBucket: "done",
      taskId: "TASK-20260805-010-PM-to-DEV",
    });
    const qa = task("QA", {
      taskId: "TASK-20260805-011-PM-to-QA",
      dependsOn: ["TASK-20260805-010-PM-to-DEV"],
    });
    const thread = [dev, qa];
    const reports = [report("DEV", "done", "TASK-20260805-010-PM-to-DEV")];
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(
      admissionInput(qa, thread, reports, {
        executionContext: ctx(thread, reports),
      }),
    );
    assert.equal(decision.decision, "allow");
  });

  it("holds QA when DEV report pending", async () => {
    const dev = task("DEV", {
      lifecycleBucket: "active",
      taskId: "TASK-20260805-020-PM-to-DEV",
    });
    const qa = task("QA", {
      taskId: "TASK-20260805-021-PM-to-QA",
      dependsOn: ["TASK-20260805-020-PM-to-DEV"],
    });
    const thread = [dev, qa];
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(
      admissionInput(qa, thread, [], {
        executionContext: ctx(thread, []),
      }),
    );
    assert.equal(decision.decision, "hold");
    assert.equal(decision.reason, "waiting_dependency");
  });

  it("marks lifecycle_split as needs_human", async () => {
    const target = task("DEV", {
      lifecycleBucket: "inbox",
      fmState: "dispatched",
    });
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(admissionInput(target, [target], []));
    assert.equal(decision.decision, "needs_human");
    assert.equal(decision.reason, "lifecycle_split");
  });
});

describe("WP-07 old vs new comparison matrix", () => {
  type Case = {
    name: string;
    input: LegacyOpenDevTeamAdmissionInput;
  };

  const cases: Case[] = [];

  {
    const target = task("DEV");
    cases.push({
      name: "DEV inbox allow",
      input: admissionInput(target, [target], []),
    });
  }
  {
    const target = task("DEV", { lifecycleBucket: "done" });
    cases.push({
      name: "DEV already_done reject",
      input: admissionInput(target, [target], []),
    });
  }
  {
    const target = task("DEV", { lifecycleBucket: "active" });
    cases.push({
      name: "DEV already_active reject",
      input: admissionInput(target, [target], []),
    });
  }
  {
    const target = task("DEV", {
      lifecycleBucket: "inbox",
      fmState: "dispatched",
    });
    cases.push({
      name: "DEV lifecycle_split",
      input: admissionInput(target, [target], []),
    });
  }
  {
    const dep = task("DEV", {
      taskId: "TASK-20260805-030-PM-to-DEV",
      lifecycleBucket: "done",
    });
    const target = task("OPS", {
      taskId: "TASK-20260805-031-PM-to-OPS",
      dependsOn: ["TASK-20260805-030-PM-to-DEV"],
    });
    cases.push({
      name: "OPS unlocked by DEV done report",
      input: admissionInput(
        target,
        [dep, target],
        [report("DEV", "done", "TASK-20260805-030-PM-to-DEV")],
      ),
    });
  }
  {
    const dep = task("DEV", {
      taskId: "TASK-20260805-040-PM-to-DEV",
      lifecycleBucket: "active",
    });
    const target = task("OPS", {
      taskId: "TASK-20260805-041-PM-to-OPS",
      dependsOn: ["TASK-20260805-040-PM-to-DEV"],
    });
    cases.push({
      name: "OPS waiting_dependency (bypass when explicit depends_on)",
      input: admissionInput(target, [dep, target], []),
    });
  }
  {
    const dev = task("DEV", {
      lifecycleBucket: "done",
      taskId: "TASK-20260805-050-PM-to-DEV",
    });
    const qa = task("QA", {
      taskId: "TASK-20260805-051-PM-to-QA",
      dependsOn: ["TASK-20260805-050-PM-to-DEV"],
    });
    const thread = [dev, qa];
    const reports = [report("DEV", "done", "TASK-20260805-050-PM-to-DEV")];
    cases.push({
      name: "QA allow with DEV done",
      input: admissionInput(qa, thread, reports, {
        executionContext: ctx(thread, reports),
      }),
    });
  }
  {
    const dev = task("DEV", {
      lifecycleBucket: "active",
      taskId: "TASK-20260805-060-PM-to-DEV",
    });
    const qa = task("QA", {
      taskId: "TASK-20260805-061-PM-to-QA",
      dependsOn: ["TASK-20260805-060-PM-to-DEV"],
    });
    const thread = [dev, qa];
    cases.push({
      name: "QA hold DEV pending",
      input: admissionInput(qa, thread, [], {
        executionContext: ctx(thread, []),
      }),
    });
  }
  {
    const qa = task("QA", {
      taskId: "TASK-20260805-070-PM-to-QA",
      lifecycleBucket: "done",
    });
    cases.push({
      name: "QA already_done",
      input: admissionInput(qa, [qa], [], {
        executionContext: ctx([qa], []),
      }),
    });
  }

  for (const c of cases) {
    it(`parity: ${c.name}`, async () => {
      const legacySlice = evaluateLegacyDispatcherSlice(c.input);
      const verdict = evaluateLegacyOpenDevTeamGates(c.input);
      const policy = new LegacyOpenDevTeamAdmissionPolicy();
      const decision = await policy.inspect(c.input);

      assert.equal(
        verdict.allowed,
        legacySlice.allowed,
        `verdict.allowed vs legacySlice for ${c.name}`,
      );
      assert.equal(
        verdict.allowed ? "allowed" : verdict.reason,
        legacySlice.reason,
        `reason parity for ${c.name}`,
      );

      // Independent old-path call (same functions TaskDispatcher uses).
      const role = String(c.input.target.recipient).toUpperCase().split("-")[0];
      let oldAllowed: boolean;
      let oldReason: string;
      if (role === "QA") {
        const qa = canDispatchQA(
          c.input.target,
          c.input.executionContext!,
          c.input.threadTasks,
        );
        oldAllowed = qa.allowed;
        oldReason = qa.allowed
          ? "allowed"
          : qaGateToDispatchSkipReason(qa.reason);
      } else {
        const gate = evaluateDispatchEligibility(
          c.input.target,
          c.input.threadTasks,
          c.input.reports,
        );
        const explicitDeps = c.input.target.dependsOn ?? [];
        if (
          !gate.allowed &&
          gate.reason === "waiting_dependency" &&
          explicitDeps.length > 0
        ) {
          oldAllowed = true;
          oldReason = "allowed";
        } else {
          oldAllowed = gate.allowed;
          oldReason = gate.allowed ? "allowed" : gate.reason;
        }
      }

      assert.equal(verdict.allowed, oldAllowed, `oldAllowed for ${c.name}`);
      assert.equal(
        verdict.allowed ? "allowed" : verdict.reason,
        oldReason,
        `oldReason for ${c.name}`,
      );

      const mapped = mapLegacyGateVerdictToAdmission(verdict);
      assert.equal(decision.decision, mapped.decision);
      assert.equal(decision.reason, mapped.reason);
      if (oldAllowed) {
        assert.equal(decision.decision, "allow");
      } else {
        assert.notEqual(decision.decision, "allow");
      }
    });
  }

  it("explicit hold parity with resolveExplicitDispatchHoldReason", async () => {
    const target = task("EVAL", { sender: "PM" });
    const holdInput = {
      sender: "PM",
      recipient: "EVAL",
      filename: target.filename,
      protocol: "fcop" as const,
    };
    const oldHold = resolveExplicitDispatchHoldReason(holdInput);
    assert.ok(oldHold);
    const policy = new LegacyOpenDevTeamAdmissionPolicy();
    const decision = await policy.inspect(
      admissionInput(target, [target], [], {
        checkExplicitHold: true,
        explicitHoldInput: holdInput,
      }),
    );
    assert.equal(decision.decision, "hold");
    assert.equal(decision.extensions?.holdReason, oldHold);
  });
});

describe("WP-07 Core boundary", () => {
  it("generic policy module source does not hardcode role===QA outside Legacy policy", async () => {
    // Boundary check: WorkAdmissionPolicy.ts stays role-agnostic.
    const { readFile } = await import("node:fs/promises");
    const { fileURLToPath } = await import("node:url");
    const { dirname, join } = await import("node:path");
    const here = dirname(fileURLToPath(import.meta.url));
    const abstractPolicy = await readFile(
      join(here, "../policy/WorkAdmissionPolicy.ts"),
      "utf-8",
    );
    assert.equal(/role\s*===\s*["']QA["']/.test(abstractPolicy), false);
    assert.equal(/workerRole\s*===\s*["']QA["']/.test(abstractPolicy), false);
  });
});
