/**
 * requirements-sift-projection.test.mjs — unit tests for the DB-to-sift
 * requirements corpus projection module.
 *
 * Purpose: verify buildRequirementsProjection writes a correct markdown file
 * from a tmp DB fixture without touching the real .sf/sf.db.
 */
import assert from "node:assert/strict";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import { closeDatabase, insertRequirement, openDatabase } from "../sf-db.js";
import {
	buildRequirementsProjection,
	getRequirementsProjectionPath,
} from "../sf-db/requirements-sift-projection.js";

const tmpRoots = [];

afterEach(() => {
	closeDatabase();
	for (const root of tmpRoots.splice(0)) {
		rmSync(root, { recursive: true, force: true });
	}
});

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-req-sift-proj-"));
	mkdirSync(join(root, ".sf"), { recursive: true });
	tmpRoots.push(root);
	return root;
}

function openDb(root) {
	openDatabase(join(root, ".sf", "sf.db"));
}

function seedRow(overrides = {}) {
	return {
		id: "R001",
		class: "functional",
		status: "active",
		description: "Test requirement",
		why: "Because tests matter",
		source: "operator",
		primary_owner: "S01",
		supporting_slices: "",
		validation: "smoke",
		notes: "",
		full_content: "",
		superseded_by: null,
		...overrides,
	};
}

// ── Test 1: 3 rows of varying status → all present, grouped by status ────────

test("buildRequirementsProjection_with_three_rows_writes_all_grouped_by_status", async () => {
	const root = makeProject();
	openDb(root);

	insertRequirement(
		seedRow({ id: "R001", status: "active", description: "Active req" }),
	);
	insertRequirement(
		seedRow({ id: "R002", status: "validated", description: "Validated req" }),
	);
	insertRequirement(
		seedRow({ id: "R003", status: "deferred", description: "Deferred req" }),
	);

	const result = await buildRequirementsProjection(root);

	assert.equal(result.written, true);
	assert.equal(result.rows, 3);
	assert.ok(result.bytes > 0, "bytes should be positive");
	assert.ok(
		result.projectionPath === getRequirementsProjectionPath(root),
		"projectionPath should match helper",
	);

	assert.ok(existsSync(result.projectionPath), "projection file should exist");
	const content = readFileSync(result.projectionPath, "utf-8");

	// All three IDs appear
	assert.match(content, /## R001/);
	assert.match(content, /## R002/);
	assert.match(content, /## R003/);

	// Grouping by status: active before validated before deferred
	const posActive = content.indexOf("## R001");
	const posValidated = content.indexOf("## R002");
	const posDeferred = content.indexOf("## R003");
	assert.ok(posActive < posValidated, "active should come before validated");
	assert.ok(
		posValidated < posDeferred,
		"validated should come before deferred",
	);
});

// ── Test 2: NULL notes → no "**Notes:**" section and no "null" literal ────────

test("buildRequirementsProjection_with_null_notes_omits_notes_section", async () => {
	const root = makeProject();
	openDb(root);

	insertRequirement(
		seedRow({ id: "R010", notes: "", description: "No notes req" }),
	);

	const result = await buildRequirementsProjection(root);
	assert.equal(result.written, true);

	const content = readFileSync(result.projectionPath, "utf-8");
	assert.doesNotMatch(
		content,
		/\*\*Notes:\*\*/,
		"should not emit Notes section for empty notes",
	);
	assert.doesNotMatch(content, /\bnull\b/, "should not emit literal null");
});

// ── Test 3: Special chars (newlines, backticks) → escaped for MD safety ───────

test("buildRequirementsProjection_escapes_backticks_and_collapses_newlines", async () => {
	const root = makeProject();
	openDb(root);

	insertRequirement(
		seedRow({
			id: "R020",
			description: "Use `node:sqlite`\nAnd another line",
			why: "Backtick `escape` needed",
			notes: "multi\nline\nnote",
		}),
	);

	const result = await buildRequirementsProjection(root);
	assert.equal(result.written, true);

	const content = readFileSync(result.projectionPath, "utf-8");

	// Backticks in description should be escaped
	assert.match(content, /\\`node:sqlite\\`/, "backticks should be escaped");

	// Newline in description should be collapsed to space in heading
	assert.doesNotMatch(
		content,
		/## R020 — .*\n.*And another line/,
		"heading should be single-line",
	);

	// Notes newlines should be collapsed
	assert.doesNotMatch(
		content,
		/\*\*Notes:\*\*.*\nline/,
		"notes should be on one line",
	);
});

// ── Test 4: Returns correct shape {rows, bytes, projectionPath} ───────────────

test("buildRequirementsProjection_returns_correct_shape", async () => {
	const root = makeProject();
	openDb(root);

	insertRequirement(seedRow({ id: "R031" }));
	insertRequirement(seedRow({ id: "R032" }));
	insertRequirement(seedRow({ id: "R033" }));

	const result = await buildRequirementsProjection(root);

	assert.equal(result.written, true);
	assert.equal(result.rows, 3);
	assert.ok(typeof result.bytes === "number" && result.bytes > 0, "bytes > 0");
	assert.ok(
		typeof result.projectionPath === "string" &&
			result.projectionPath.includes("requirements-projected.md"),
		"projectionPath contains expected filename",
	);
});

// ── Test 5: Empty DB → header written, zero R-sections ────────────────────────

test("buildRequirementsProjection_with_empty_db_writes_header_only", async () => {
	const root = makeProject();
	openDb(root);

	const result = await buildRequirementsProjection(root);

	assert.equal(result.written, true);
	assert.equal(result.rows, 0);
	assert.ok(result.bytes > 0, "header itself has bytes");

	const content = readFileSync(result.projectionPath, "utf-8");
	assert.match(content, /# SF Requirements Corpus/, "header should be present");
	assert.doesNotMatch(content, /## R/, "should have no R-sections");
});
