import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, test } from "vitest";

import { runTriageApply } from "../headless-triage.js";

const tempDirs: string[] = [];
let originalAgentDir: string | undefined;
const deciderPlan = [
	"```yaml",
	"decisions:",
	"  - id: sf-test-1",
	"    outcome: close",
	"    evidence_kind: human-clear",
	"    reason: stale",
	"```",
	"Self-feedback triage complete.",
].join("\n");

function makeProject(): string {
	const dir = mkdtempSync(join(tmpdir(), "sf-headless-triage-"));
	tempDirs.push(dir);
	mkdirSync(join(dir, ".sf"), { recursive: true });
	return dir;
}

afterEach(() => {
	if (originalAgentDir === undefined) delete process.env.SF_CODING_AGENT_DIR;
	else process.env.SF_CODING_AGENT_DIR = originalAgentDir;
	while (tempDirs.length > 0) {
		rmSync(tempDirs.pop()!, { recursive: true, force: true });
	}
});

beforeEach(() => {
	originalAgentDir = process.env.SF_CODING_AGENT_DIR;
	const dir = mkdtempSync(join(tmpdir(), "sf-headless-agent-dir-"));
	tempDirs.push(dir);
	process.env.SF_CODING_AGENT_DIR = dir;
});

test("runTriageApply_when_review_code_agrees_runs_both_agents_in_order", async () => {
	const project = makeProject();
	const calls: string[] = [];
	const result = await runTriageApply(project, "triage prompt", {
		candidateCount: 1,
		allowUntrustedRunner: true,
		agentRunner: async (agent) => {
			calls.push(agent.name);
			if (agent.name === "triage-decider") {
				return {
					ok: true,
					output: deciderPlan,
					exitCode: 0,
				};
			}
			return { ok: true, output: "review-code: agree", exitCode: 0 };
		},
	});

	// Orchestration contract: review-code agreed, so the apply phase ran.
	// The actual mutation on sf-test-1 fails because the test project has
	// no real ledger entry — that's the rejectedIds path, not a contract
	// violation. ok=false on apply failures is itself the contract
	// (codex review 2026-05-14 follow-up).
	assert.equal(result.agreed, true, "review-code must agree");
	assert.deepEqual(
		calls,
		["triage-decider", "review-code"],
		"decider must run before review-code",
	);
	assert.equal(result.ok, false, "apply should fail on missing ledger entry");
	assert.deepEqual(result.rejectedIds, ["sf-test-1"]);
	assert.deepEqual(result.resolvedIds, []);
});

test("runTriageApply_when_custom_runner_without_trust_flag_refuses", async () => {
	// Defense-in-depth: an injected agentRunner can side-channel-mutate
	// the ledger despite the read-only tool override. Only test paths
	// that explicitly set allowUntrustedRunner can pass a custom runner.
	const project = makeProject();
	let calls = 0;
	const result = await runTriageApply(project, "triage prompt", {
		// NOTE: allowUntrustedRunner intentionally omitted.
		agentRunner: async () => {
			calls += 1;
			return { ok: true, output: deciderPlan, exitCode: 0 };
		},
	});

	assert.equal(result.ok, false);
	assert.equal(
		calls,
		0,
		"agentRunner must not be invoked when trust flag is absent",
	);
	assert.match(result.error ?? "", /allowUntrustedRunner|custom agentRunner/i);
});

test("runTriageApply_when_review_code_disagrees_blocks_completion", async () => {
	const project = makeProject();
	const result = await runTriageApply(project, "triage prompt", {
		allowUntrustedRunner: true,
		agentRunner: async (agent) => {
			if (agent.name === "triage-decider") {
				return {
					ok: true,
					output: deciderPlan,
					exitCode: 0,
				};
			}
			return {
				ok: true,
				output: "## Concern 1:\nThe cited closure is not supported.",
				exitCode: 0,
			};
		},
	});

	assert.equal(result.ok, false);
	assert.equal(result.agreed, false);
	assert.equal(
		result.error,
		"review-code disagreed — pausing for operator review",
	);
});

test("runTriageApply_when_decider_fails_skips_review_code", async () => {
	const project = makeProject();
	const calls: string[] = [];
	const result = await runTriageApply(project, "triage prompt", {
		allowUntrustedRunner: true,
		agentRunner: async (agent) => {
			calls.push(agent.name);
			return {
				ok: false,
				output: "",
				stderr: "model failed",
				exitCode: 1,
			};
		},
	});

	assert.equal(result.ok, false);
	assert.equal(result.error, "model failed");
	assert.deepEqual(calls, ["triage-decider"]);
});

test("runTriageApply_when_decider_outputs_unknown_id_rejects_plan_before_review", async () => {
	const project = makeProject();
	const calls: string[] = [];
	const result = await runTriageApply(project, "triage prompt", {
		candidateCount: 1,
		expectedIds: ["sf-expected-1"],
		allowUntrustedRunner: true,
		agentRunner: async (agent) => {
			calls.push(agent.name);
			return {
				ok: true,
				output: deciderPlan,
				exitCode: 0,
			};
		},
	});

	assert.equal(result.ok, false);
	assert.match(result.error ?? "", /not in the candidate set/);
	assert.deepEqual(calls, ["triage-decider"]);
});
