/**
 * Test buildMemoryRetrievalQuery — the memory retrieval query
 * expansion at the execute-task site.
 *
 * Previously: getRelevantMemoriesRanked received only
 * `slice.title + task.title`. Two short strings are too sparse for
 * cosine ranking to do meaningful re-ranking against the static pool.
 *
 * Now: titles + slice.goal (front 600 chars) + touched-file paths
 * (top 20 from git diff/status). This test pins:
 *   - titles are always present
 *   - slice goal is appended when DB has one (we stub by passing
 *     through to the live import; if DB is closed we fall back to
 *     titles-only without throwing)
 *   - non-git directories don't crash the path
 *   - bounded output: ~600 char cap on goal, 20-file cap
 *
 * The unit tests run against a tmp dir so we don't need an
 * actual .sf/sf.db.
 */

import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import { buildMemoryRetrievalQuery } from "../auto-prompts.js";

const tempDirs = [];

afterEach(() => {
	for (const d of tempDirs) rmSync(d, { recursive: true, force: true });
	tempDirs.length = 0;
});

function mkdtemp() {
	const d = mkdtempSync(join(tmpdir(), "sf-memquery-"));
	tempDirs.push(d);
	return d;
}

test("titles always appear in the query", async () => {
	const dir = mkdtemp();
	const q = await buildMemoryRetrievalQuery(
		dir,
		"M01",
		"S01",
		"slice title",
		"task title",
	);
	assert.match(q, /slice title/);
	assert.match(q, /task title/);
});

test("non-git directory doesn't crash", async () => {
	const dir = mkdtemp();
	const q = await buildMemoryRetrievalQuery(dir, "M01", "S01", "A", "B");
	// Just titles, no goal (no DB), no files (no git) — still safe.
	assert.equal(typeof q, "string");
	assert.match(q, /A/);
	assert.match(q, /B/);
});

test("missing or empty inputs collapse cleanly", async () => {
	const dir = mkdtemp();
	const q = await buildMemoryRetrievalQuery(dir, "", "", "", "");
	// All-empty inputs → empty-string output (the consumer's `if
	// (!query.trim())` branch picks the static fallback).
	assert.equal(q.trim(), "");
});

test("nullish title args don't poison the query", async () => {
	const dir = mkdtemp();
	const q = await buildMemoryRetrievalQuery(
		dir,
		null,
		undefined,
		undefined,
		"real task",
	);
	// Only the present non-empty string survives.
	assert.match(q, /real task/);
	assert.ok(!q.includes("undefined"));
	assert.ok(!q.includes("null"));
});

test("git changed files added when repo is present", async () => {
	const { execFileSync } = await import("node:child_process");
	const dir = mkdtemp();
	execFileSync("git", ["init", "--quiet"], { cwd: dir });
	execFileSync("git", ["config", "user.email", "t@e.x"], { cwd: dir });
	execFileSync("git", ["config", "user.name", "t"], { cwd: dir });
	// Two committed files plus one untracked file in the working tree.
	writeFileSync(join(dir, "kept.txt"), "kept content");
	mkdirSync(join(dir, "sub"));
	writeFileSync(join(dir, "sub", "deep.txt"), "deep content");
	execFileSync("git", ["add", "."], { cwd: dir });
	execFileSync(
		"git",
		["commit", "--quiet", "--allow-empty-message", "-m", ""],
		{ cwd: dir },
	);
	writeFileSync(join(dir, "kept.txt"), "modified content");
	writeFileSync(join(dir, "new.txt"), "untracked");
	const q = await buildMemoryRetrievalQuery(dir, "M01", "S01", "title", "task");
	// At least one of the changed files surfaces in the query.
	const sawAny = ["kept.txt", "new.txt"].some((p) => q.includes(p));
	assert.ok(sawAny, `expected changed files in query, got: ${q}`);
});
