/**
 * requirement-status-sweeper.test.mjs — auto-transition sweeper for R-entries.
 *
 * Tests all three rules:
 *  1. operational-evidence-drained (source IDs all resolved → 'resolved')
 *  2. supporting-slices-complete   (all slices complete → 'validated')
 *  3. supersedes-resolved          (superseded_by target is terminal → 'superseded')
 *
 * Uses in-memory SQLite fixtures — never touches the real DB.
 */

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import {
	closeDatabase,
	getRequirementById,
	insertMilestone,
	insertSelfFeedbackEntry,
	insertSlice,
	openDatabase,
	resolveSelfFeedbackEntry,
	upsertRequirement,
} from "../sf-db.js";
import {
	requirementStatusSweeperGate,
	sweepRequirementStatuses,
} from "../sf-db/requirement-status-sweeper.js";

// ─── Fixtures ─────────────────────────────────────────────────────────────────

const tmpDirs = [];

afterEach(() => {
	closeDatabase();
	while (tmpDirs.length > 0) {
		const dir = tmpDirs.pop();
		if (dir) rmSync(dir, { recursive: true, force: true });
	}
});

/** Open an in-memory DB (no file I/O). */
function openMemoryDb() {
	openDatabase(":memory:");
}

/** Insert a minimal self_feedback row with given id and resolved state. */
function makeSelfFeedbackRow(id, { resolved = false } = {}) {
	const ts = new Date().toISOString();
	insertSelfFeedbackEntry({
		id,
		ts,
		kind: "test:sweeper",
		severity: "low",
		blocking: false,
		repoIdentity: "forge",
		sfVersion: "0.0.0",
		basePath: "/tmp/test",
		summary: `test entry ${id}`,
		evidence: "",
		suggestedFix: "",
	});
	if (resolved) {
		resolveSelfFeedbackEntry(id, {
			reason: "resolved-in-test",
			resolvedAt: new Date().toISOString(),
		});
	}
}

/** Insert an active requirement with the given fields. */
function makeRequirement(id, fields = {}) {
	upsertRequirement({
		id,
		class: fields.class ?? "operational",
		status: fields.status ?? "active",
		description: fields.description ?? "",
		why: fields.why ?? "test",
		source: fields.source ?? "test",
		primary_owner: fields.primary_owner ?? null,
		supporting_slices: fields.supporting_slices ?? "[]",
		validation: fields.validation ?? "",
		notes: fields.notes ?? "",
		full_content: fields.full_content ?? "",
		superseded_by: fields.superseded_by ?? null,
	});
}

/** Insert a milestone row (required for slice FK). */
function makeMilestone(milestoneId) {
	insertMilestone({
		id: milestoneId,
		title: `test milestone ${milestoneId}`,
		status: "active",
	});
}

/** Insert a slice row with the given status. */
function makeSlice(milestoneId, sliceId, status = "complete") {
	makeMilestone(milestoneId);
	insertSlice({
		milestoneId,
		id: sliceId,
		title: `test slice ${sliceId}`,
		status,
		risk: "low",
		depends: [],
		demo: "",
		sequence: 0,
	});
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe("sweepRequirementStatuses — rule 1: operational-evidence-drained", () => {
	test("transitions to 'resolved' when all 3 source IDs are resolved", async () => {
		openMemoryDb();

		makeSelfFeedbackRow("sf-aaa1", { resolved: true });
		makeSelfFeedbackRow("sf-bbb2", { resolved: true });
		makeSelfFeedbackRow("sf-ccc3", { resolved: true });

		makeRequirement("R900", {
			class: "operational",
			description:
				"Some recurring issue\n\nSource IDs: sf-aaa1, sf-bbb2, sf-ccc3",
		});

		const result = await sweepRequirementStatuses("/tmp/test");

		expect(result.totalChecked).toBe(1);
		expect(result.transitioned).toHaveLength(1);
		expect(result.transitioned[0]).toMatchObject({
			id: "R900",
			fromStatus: "active",
			toStatus: "resolved",
			rule: "operational-evidence-drained",
		});

		const updated = getRequirementById("R900");
		expect(updated.status).toBe("resolved");
	});

	test("stays 'active' when 1 of 3 source IDs is still open", async () => {
		openMemoryDb();

		makeSelfFeedbackRow("sf-aaa1", { resolved: true });
		makeSelfFeedbackRow("sf-bbb2", { resolved: true });
		makeSelfFeedbackRow("sf-ccc3", { resolved: false }); // still open

		makeRequirement("R901", {
			class: "operational",
			description: "Source IDs: sf-aaa1, sf-bbb2, sf-ccc3",
		});

		const result = await sweepRequirementStatuses("/tmp/test");

		expect(result.transitioned).toHaveLength(0);
		const row = getRequirementById("R901");
		expect(row.status).toBe("active");
	});
});

describe("sweepRequirementStatuses — rule 2: supporting-slices-complete", () => {
	test("transitions to 'validated' when slice M999/S99 is complete", async () => {
		openMemoryDb();
		makeSlice("M999", "S99", "complete");
		makeRequirement("R902", {
			class: "capability",
			supporting_slices: '["M999/S99"]',
		});

		const result = await sweepRequirementStatuses("/tmp/test");

		expect(result.transitioned).toHaveLength(1);
		expect(result.transitioned[0]).toMatchObject({
			id: "R902",
			toStatus: "validated",
			rule: "supporting-slices-complete",
		});

		const updated = getRequirementById("R902");
		expect(updated.status).toBe("validated");
	});

	test("stays 'active' when slice M999/S99 is in_progress", async () => {
		openMemoryDb();
		makeSlice("M999", "S99", "in_progress");
		makeRequirement("R903", {
			class: "capability",
			supporting_slices: '["M999/S99"]',
		});

		const result = await sweepRequirementStatuses("/tmp/test");

		expect(result.transitioned).toHaveLength(0);
		const row = getRequirementById("R903");
		expect(row.status).toBe("active");
	});

	test("stays 'active' when supporting_slices is empty array", async () => {
		openMemoryDb();
		makeRequirement("R904", {
			class: "capability",
			supporting_slices: "[]",
		});

		const result = await sweepRequirementStatuses("/tmp/test");

		expect(result.transitioned).toHaveLength(0);
		const row = getRequirementById("R904");
		expect(row.status).toBe("active");
	});
});

describe("sweepRequirementStatuses — rule 3: supersedes-resolved", () => {
	test("transitions to 'superseded' when supersedes_by target is resolved", async () => {
		openMemoryDb();

		// Target R200 is already resolved
		makeRequirement("R200", { status: "resolved" });
		// R905 supersedes R200
		makeRequirement("R905", { superseded_by: "R200" });

		const result = await sweepRequirementStatuses("/tmp/test");

		// Only R905 is active (R200 is resolved, not active)
		expect(result.transitioned).toHaveLength(1);
		expect(result.transitioned[0]).toMatchObject({
			id: "R905",
			toStatus: "superseded",
			rule: "supersedes-resolved",
		});

		const updated = getRequirementById("R905");
		expect(updated.status).toBe("superseded");
	});

	test("stays 'active' when supersedes_by target does not exist", async () => {
		openMemoryDb();

		makeRequirement("R906", { superseded_by: "R999" });

		const result = await sweepRequirementStatuses("/tmp/test");

		expect(result.transitioned).toHaveLength(0);
		const row = getRequirementById("R906");
		expect(row.status).toBe("active");
	});
});

describe("sweepRequirementStatuses — dryRun option", () => {
	test("returns transitions but does not write to DB", async () => {
		openMemoryDb();

		makeSelfFeedbackRow("sf-dry1", { resolved: true });
		makeSelfFeedbackRow("sf-dry2", { resolved: true });
		makeSelfFeedbackRow("sf-dry3", { resolved: true });

		makeRequirement("R907", {
			class: "operational",
			description: "Source IDs: sf-dry1, sf-dry2, sf-dry3",
		});

		const result = await sweepRequirementStatuses("/tmp/test", {
			dryRun: true,
		});

		expect(result.transitioned).toHaveLength(1);
		expect(result.transitioned[0].toStatus).toBe("resolved");

		// DB must NOT have been written
		const row = getRequirementById("R907");
		expect(row.status).toBe("active");
	});
});

describe("requirementStatusSweeperGate — UokGate contract", () => {
	test("returns GateResult pass when no transitions needed", async () => {
		openMemoryDb();
		makeRequirement("R908", { class: "capability", supporting_slices: "[]" });

		const result = await requirementStatusSweeperGate.execute({
			basePath: "/tmp/test",
		});

		expect(result.outcome).toBe("pass");
		expect(result.failureClass).toBeNull();
		expect(typeof result.rationale).toBe("string");
		expect(result.rationale).toContain("0 of");
	});

	test("returns GateResult manual-attention when transitions occurred", async () => {
		openMemoryDb();

		makeSelfFeedbackRow("sf-gate1", { resolved: true });
		makeRequirement("R909", {
			class: "operational",
			description: "Source IDs: sf-gate1",
		});

		const result = await requirementStatusSweeperGate.execute({
			basePath: "/tmp/test",
		});

		expect(result.outcome).toBe("manual-attention");
		expect(result.failureClass).toBe("verification");
		expect(result.findings).toHaveLength(1);
		expect(result.findings[0].id).toBe("R909");
	});
});

describe("sweepRequirementStatuses — includeRules option", () => {
	test("only rule 3 fires when includeRules=['supersedes-resolved']", async () => {
		openMemoryDb();

		// Set up a row that would trigger rule 1 (all source IDs resolved)
		makeSelfFeedbackRow("sf-r1a", { resolved: true });
		makeRequirement("R910", {
			class: "operational",
			description: "Source IDs: sf-r1a",
		});

		// Set up a row that would trigger rule 3
		makeRequirement("R911-target", { status: "resolved" });
		makeRequirement("R912", { superseded_by: "R911-target" });

		const result = await sweepRequirementStatuses("/tmp/test", {
			includeRules: ["supersedes-resolved"],
		});

		// Only rule 3 should have fired — R910 should remain active
		const r910 = getRequirementById("R910");
		expect(r910.status).toBe("active");

		// R912 should have transitioned
		const r912Updated = getRequirementById("R912");
		expect(r912Updated.status).toBe("superseded");

		const rules = result.transitioned.map((t) => t.rule);
		expect(rules.every((r) => r === "supersedes-resolved")).toBe(true);
		expect(rules).toHaveLength(1);
	});
});
