/**
 * purpose_anchor on self_feedback (sf-db v71, ADR-0000 restoration).
 *
 * SF is a purpose-to-software compiler — every self_feedback row should be
 * filable against the milestone vision or slice goal it's at risk against.
 * Without that link, triage can't prioritize work against purpose; it's
 * forced to treat every row as equally floating.
 *
 * This test covers the three slice contracts:
 *   1. `feedback add --purpose <fragment>` persists the anchor to SQLite.
 *   2. `feedback list --purpose <fragment>` filters rows by anchor substring.
 *   3. Help text documents `--purpose` for both add and list.
 *
 * Live-DB layer is exercised directly via insertSelfFeedbackEntry /
 * listSelfFeedbackEntries (same primitives the CLI uses) against an
 * in-memory database, mirroring the pattern used by self-feedback-db.test.mjs.
 */

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, test } from "vitest";
import {
	closeDatabase,
	insertSelfFeedbackEntry,
	listSelfFeedbackEntries,
	openDatabase,
} from "../resources/extensions/sf/sf-db.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
const handlerSrc = readFileSync(
	join(__dirname, "..", "headless-feedback.ts"),
	"utf-8",
);
const helpSrc = readFileSync(join(__dirname, "..", "help-text.ts"), "utf-8");

afterEach(() => {
	closeDatabase();
});

function baseEntry(
	overrides: Record<string, unknown>,
): Record<string, unknown> {
	return {
		id: `purpose-test-${Math.random().toString(36).slice(2, 8)}`,
		ts: new Date().toISOString(),
		kind: "improvement-idea",
		severity: "medium",
		blocking: false,
		repoIdentity: "test",
		sfVersion: "test",
		basePath: "/tmp/test",
		summary: "test entry",
		evidence: "",
		suggestedFix: "",
		schemaVersion: 1,
		...overrides,
	};
}

test("handler accepts --purpose flag on feedback add", () => {
	assert.match(
		handlerSrc,
		/readFlag\(options\.args, "--purpose"\)/,
		"handleAdd must read --purpose <fragment>",
	);
	assert.match(
		handlerSrc,
		/purposeAnchor/,
		"handleAdd must pass purposeAnchor through to the entry payload",
	);
});

test("handler accepts --purpose flag on feedback list and pushes it into the DB query", () => {
	// The list handler must read --purpose and forward it as { purpose: ... }
	// to listSelfFeedbackEntries so the LIKE filter runs in SQLite rather than
	// pulling the full table into memory.
	assert.match(
		handlerSrc,
		/listSelfFeedbackEntries\(\s*purposeFragment\s*\?\s*\{\s*purpose:\s*purposeFragment\s*\}\s*:\s*undefined\s*,?\s*\)/,
		"handleList must forward purposeFragment as { purpose } to the DB layer",
	);
});

test("help text documents --purpose for feedback add and list", () => {
	assert.match(
		helpSrc,
		/feedback add\s+File a self_feedback entry.*--purpose <fragment>/s,
		"help: feedback add must list --purpose <fragment>",
	);
	assert.match(
		helpSrc,
		/feedback list\s+List self_feedback entries.*--purpose <fragment>/s,
		"help: feedback list must list --purpose <fragment>",
	);
});

test("insertSelfFeedbackEntry persists purposeAnchor and rowToSelfFeedback round-trips it", () => {
	openDatabase(":memory:");

	const anchor = "M015 vision: machine-readable purpose contract";
	insertSelfFeedbackEntry(
		baseEntry({
			id: "purpose-roundtrip-1",
			summary: "anchored entry",
			purposeAnchor: anchor,
		}),
	);

	const rows = listSelfFeedbackEntries();
	const reloaded = rows.find((r) => r.id === "purpose-roundtrip-1");
	assert.ok(reloaded, "inserted row must come back from the list");
	assert.equal(
		reloaded.purposeAnchor,
		anchor,
		"purposeAnchor must round-trip through SQLite to the JS surface",
	);
});

test("insertSelfFeedbackEntry stores NULL when purposeAnchor is omitted (legacy compat)", () => {
	openDatabase(":memory:");

	insertSelfFeedbackEntry(
		baseEntry({ id: "purpose-omitted-1", summary: "legacy-style entry" }),
	);

	const rows = listSelfFeedbackEntries();
	const reloaded = rows.find((r) => r.id === "purpose-omitted-1");
	assert.ok(reloaded);
	// Legacy callers don't carry purposeAnchor — must come back as null/undefined,
	// not as the string "null" or an empty string.
	assert.ok(
		reloaded.purposeAnchor === null || reloaded.purposeAnchor === undefined,
		`expected null|undefined, got ${JSON.stringify(reloaded.purposeAnchor)}`,
	);
});

test("listSelfFeedbackEntries({ purpose }) scopes by substring match on purpose_anchor", () => {
	openDatabase(":memory:");

	insertSelfFeedbackEntry(
		baseEntry({
			id: "purpose-filter-m015",
			summary: "row tied to M015 vision",
			purposeAnchor: "M015 vision: triage prioritizes against purpose",
		}),
	);
	insertSelfFeedbackEntry(
		baseEntry({
			id: "purpose-filter-m019",
			summary: "row tied to a different milestone",
			purposeAnchor: "M019 goal: spec-first TDD reaches general availability",
		}),
	);
	insertSelfFeedbackEntry(
		baseEntry({
			id: "purpose-filter-none",
			summary: "free-floating legacy row",
		}),
	);

	const allRows = listSelfFeedbackEntries();
	assert.equal(
		allRows.length,
		3,
		"baseline: all three rows visible without filter",
	);

	const m015Rows = listSelfFeedbackEntries({ purpose: "M015" });
	assert.equal(
		m015Rows.length,
		1,
		"filter must scope to the single M015-anchored row",
	);
	assert.equal(m015Rows[0].id, "purpose-filter-m015");

	// Substring matching: a fragment from the anchor's sentence body still hits.
	const triageRows = listSelfFeedbackEntries({
		purpose: "triage prioritizes",
	});
	assert.equal(
		triageRows.length,
		1,
		"sentence-fragment match must hit (operators paste fragments, not full anchors)",
	);
	assert.equal(triageRows[0].id, "purpose-filter-m015");

	// Empty / whitespace purpose argument falls back to "no filter".
	const emptyRows = listSelfFeedbackEntries({ purpose: "   " });
	assert.equal(
		emptyRows.length,
		3,
		"empty/whitespace purpose must not filter (treat as no filter)",
	);
});
