/**
 * requirement-promoter.test.mjs — clusters self-feedback by domain:family
 * prefix and promotes recurring clusters to requirement rows.
 *
 * Covers AC4 of sf-mp4rxkwt-sfthez (kind taxonomy enforcement, commit
 * 83c28b756): the threshold-promoter MUST cluster by domain:family prefix
 * (not exact kind) so 5 entries with the same family but different
 * specific suffixes can fire the COUNT_THRESHOLD=5 trigger.
 */
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 { promoteFeedbackToRequirements } from "../requirement-promoter.js";
import { recordSelfFeedback } from "../self-feedback.js";
import {
	closeDatabase,
	getActiveRequirements,
	listSelfFeedbackEntries,
	openDatabase,
} from "../sf-db.js";

const tmpDirs = [];

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

function makeForgeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-req-promoter-"));
	tmpDirs.push(dir);
	mkdirSync(join(dir, ".sf"), { recursive: true });
	writeFileSync(
		join(dir, "package.json"),
		JSON.stringify({ name: "singularity-forge" }),
	);
	openDatabase(join(dir, ".sf", "sf.db"));
	return dir;
}

describe("promoteFeedbackToRequirements", () => {
	test("clusters by domain:family prefix and promotes when count >= 5", () => {
		const project = makeForgeProject();

		// 5 entries sharing the same domain:family (gap:routing) but with
		// distinct :specific suffixes — exact-string clustering would see
		// these as 5 separate kinds and never fire. domain:family clustering
		// groups them as one "gap:routing" cluster of size 5.
		const specifics = [
			"tiebreak-cost-only",
			"agentic-axis-partial-coverage",
			"adr-0079-step-4-escalation",
			"upstream-bridge-no-closure-memory",
			"missing-fallback-policy",
		];
		for (const specific of specifics) {
			const filed = recordSelfFeedback(
				{
					kind: `gap:routing:${specific}`,
					severity: "medium",
					summary: `synthetic ${specific}`,
				},
				project,
			);
			expect(filed).toBeTruthy();
		}

		const result = promoteFeedbackToRequirements(project);
		expect(result.promoted).toBe(1);
		expect(result.requirementIds).toHaveLength(1);

		// The requirement row was created. NOTE: requirements schema has no
		// `title` column — the promoter's title computation is lost at the
		// upsert layer (separate bug worth filing). We assert source +
		// description (which carries the source IDs list).
		const reqs = getActiveRequirements();
		const promoted = reqs.find((r) => result.requirementIds.includes(r.id));
		expect(promoted).toBeTruthy();
		expect(promoted.source).toBe("sf-promoter");
		expect(promoted.description).toContain("Source IDs:");

		// Each contributing entry should be marked resolved with
		// promoted-to-requirement evidence
		const entries = listSelfFeedbackEntries();
		const promotedEntries = entries.filter((e) =>
			specifics.some((s) => e.kind === `gap:routing:${s}`),
		);
		expect(promotedEntries).toHaveLength(5);
		for (const e of promotedEntries) {
			expect(e.resolvedAt).toBeTruthy();
			expect(e.resolvedEvidence?.kind).toBe("promoted-to-requirement");
		}
	});

	test("does NOT cluster across different families even within the same domain", () => {
		const project = makeForgeProject();

		// Mixed: 3 gap:routing + 3 gap:reflection — neither family has 5,
		// neither hits MILESTONE_THRESHOLD=3 since occurredIn.milestone is
		// undefined. Total is 6 but split across families = no promotion.
		for (const f of ["routing", "reflection"]) {
			for (let i = 0; i < 3; i++) {
				recordSelfFeedback(
					{
						kind: `gap:${f}:specific-${i}`,
						severity: "medium",
						summary: `synthetic ${f} ${i}`,
					},
					project,
				);
			}
		}

		const result = promoteFeedbackToRequirements(project);
		expect(result.promoted).toBe(0);
	});

	test("legacy single-segment kinds cluster as themselves (no prefix to extract)", () => {
		const project = makeForgeProject();

		// 5 single-segment legacy kinds — each entry has kind="runaway-loop"
		// with no specific suffix. These cluster as "runaway-loop" itself.
		for (let i = 0; i < 5; i++) {
			recordSelfFeedback(
				{
					kind: "runaway-loop",
					severity: "medium",
					summary: `legacy entry ${i}`,
				},
				project,
			);
		}

		const result = promoteFeedbackToRequirements(project);
		expect(result.promoted).toBe(1);
	});

	test("returns 0 when below count threshold and no milestone-distinct trigger", () => {
		const project = makeForgeProject();
		for (let i = 0; i < 4; i++) {
			recordSelfFeedback(
				{
					kind: `gap:routing:s${i}`,
					severity: "low",
					summary: `synthetic ${i}`,
				},
				project,
			);
		}
		expect(promoteFeedbackToRequirements(project).promoted).toBe(0);
	});

	test("returns 0 when basePath isn't the forge repo", () => {
		const dir = mkdtempSync(join(tmpdir(), "sf-non-forge-"));
		tmpDirs.push(dir);
		writeFileSync(
			join(dir, "package.json"),
			JSON.stringify({ name: "some-other-app" }),
		);
		expect(promoteFeedbackToRequirements(dir).promoted).toBe(0);
	});
});
