/**
 * memory-extraction-lifecycle.test.mjs — memory closeout observability.
 *
 * Purpose: prove autonomous memory extraction records skips, completions, and
 * prompt reuse in DB-backed state instead of leaving operators to infer health
 * from logs.
 */
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, vi } from "vitest";
import {
	_resetExtractionState,
	extractMemoriesFromUnit,
} from "../memory-extractor.js";
import {
	createMemory,
	formatMemoriesForPrompt,
	getActiveMemoriesRanked,
	getRecentMemoryExtractionAttempts,
	getRelevantMemoriesRanked,
} from "../memory-store.js";
import {
	closeDatabase,
	getDatabase,
	getRuntimeCounter,
	openDatabase,
} from "../sf-db.js";

const tmpDirs = [];

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

function makeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-memory-lifecycle-"));
	tmpDirs.push(dir);
	mkdirSync(join(dir, ".sf"), { recursive: true });
	assert.equal(openDatabase(join(dir, ".sf", "sf.db")), true);
	return dir;
}

function writeActivity(dir, name, text) {
	const activityFile = join(dir, name);
	writeFileSync(
		activityFile,
		`${JSON.stringify({
			type: "custom_message",
			customType: "sf-auto",
			text,
		})}\n`,
		"utf8",
	);
	return activityFile;
}

test("extractMemoriesFromUnit_when_activity_too_small_records_skip_attempt", async () => {
	const project = makeProject();
	const activityFile = writeActivity(project, "small.jsonl", "short");

	await extractMemoriesFromUnit(
		activityFile,
		"execute-task",
		"M001/S01/T01",
		async () => "[]",
	);

	const attempts = getRecentMemoryExtractionAttempts(5);
	assert.equal(attempts.length, 1);
	assert.equal(attempts[0].status, "skipped");
	assert.equal(attempts[0].reason, "file-too-small");
	assert.equal(attempts[0].unit_key, "execute-task/M001/S01/T01");
	const processed = getDatabase()
		.prepare("SELECT count(*) AS cnt FROM memory_processed_units")
		.get();
	assert.equal(processed.cnt, 0);
});

test("extractMemoriesFromUnit_when_successful_records_processed_unit_and_attempt", async () => {
	const project = makeProject();
	const activityFile = writeActivity(
		project,
		"large.jsonl",
		"Autonomous memory extraction should persist useful project knowledge. ".repeat(
			40,
		),
	);

	await extractMemoriesFromUnit(
		activityFile,
		"execute-task",
		"M001/S01/T02",
		async () =>
			JSON.stringify([
				{
					action: "add",
					category: "knowledge",
					content:
						"Memory extraction attempts are stored in SQLite for diagnostics.",
					confidence: 0.9,
				},
			]),
	);

	const latestAttempt = getRecentMemoryExtractionAttempts(1)[0];
	assert.equal(latestAttempt.status, "processed");
	assert.equal(latestAttempt.reason, "actions-applied");
	const processed = getDatabase()
		.prepare("SELECT count(*) AS cnt FROM memory_processed_units")
		.get();
	assert.equal(processed.cnt, 1);
	const memories = getActiveMemoriesRanked(10);
	assert.equal(memories.length, 1);
	assert.match(memories[0].content, /stored in SQLite/);
});

test("extractMemoriesFromUnit_when_response_invalid_json_records_parse_failure", async () => {
	const project = makeProject();
	const activityFile = writeActivity(
		project,
		"invalid-json.jsonl",
		"Memory extraction should classify malformed model output. ".repeat(40),
	);
	const llmCall = vi.fn(async () => "{not valid json");

	await extractMemoriesFromUnit(
		activityFile,
		"execute-task",
		"M001/S01/T03",
		llmCall,
	);

	assert.equal(llmCall.mock.calls.length, 1);
	const latestAttempt = getRecentMemoryExtractionAttempts(1)[0];
	assert.equal(latestAttempt.status, "error");
	assert.equal(latestAttempt.failure_class, "parse");
	assert.equal(latestAttempt.reason, "invalid-json");
	const processed = getDatabase()
		.prepare("SELECT count(*) AS cnt FROM memory_processed_units")
		.get();
	assert.equal(processed.cnt, 0);
});

test("extractMemoriesFromUnit_when_transient_failure_retries_once", async () => {
	const project = makeProject();
	const activityFile = writeActivity(
		project,
		"transient.jsonl",
		"Memory extraction should retry only transient provider failures. ".repeat(
			40,
		),
	);
	const llmCall = vi
		.fn()
		.mockRejectedValueOnce(new Error("503 service unavailable"))
		.mockResolvedValueOnce("[]");

	await extractMemoriesFromUnit(
		activityFile,
		"execute-task",
		"M001/S01/T04",
		llmCall,
	);

	assert.equal(llmCall.mock.calls.length, 2);
	const attempts = getRecentMemoryExtractionAttempts(5);
	assert.equal(attempts[0].status, "processed");
	assert.equal(attempts[0].reason, "retry-success");
	const transientAttempt = attempts.find((row) => row.status === "error");
	assert.equal(transientAttempt.failure_class, "transient");
	assert.equal(transientAttempt.reason, "transient-failure");
});

test("formatMemoriesForPrompt_when_injected_increments_hit_count", () => {
	makeProject();
	const id = createMemory({
		category: "knowledge",
		content: "Injected memories should count as used.",
		confidence: 0.8,
	});
	const memories = getActiveMemoriesRanked(10);

	const rendered = formatMemoriesForPrompt(memories);

	assert.match(rendered, /Injected memories/);
	const row = getDatabase()
		.prepare("SELECT hit_count FROM memories WHERE id = :id")
		.get({ ":id": id });
	assert.equal(row.hit_count, 1);
	assert.equal(getRuntimeCounter("memory_inject_count"), 1);
	assert.equal(getRuntimeCounter("memory_inject_chars_total"), rendered.length);
});

test("formatMemoriesForPrompt_when_budget_truncates_skips_hidden_hit_count", () => {
	makeProject();
	const renderedId = createMemory({
		category: "knowledge",
		content: "short",
		confidence: 0.8,
	});
	const hiddenId = createMemory({
		category: "knowledge",
		content:
			"This memory is intentionally long enough that the small prompt budget excludes it.",
		confidence: 0.8,
	});

	formatMemoriesForPrompt(
		[
			{ id: renderedId, category: "knowledge", content: "short" },
			{
				id: hiddenId,
				category: "knowledge",
				content:
					"This memory is intentionally long enough that the small prompt budget excludes it.",
			},
		],
		15,
		true,
	);

	const rows = getDatabase()
		.prepare("SELECT id, hit_count FROM memories ORDER BY id")
		.all();
	assert.deepEqual(
		rows.map((row) => [row.id, row.hit_count]),
		[
			[renderedId, 1],
			[hiddenId, 0],
		],
	);
});

test("getRelevantMemoriesRanked_when_embeddings_unavailable_uses_query_terms", async () => {
	makeProject();
	createMemory({
		category: "knowledge",
		content: "Generic unrelated implementation note.",
		confidence: 0.95,
	});
	const relevantId = createMemory({
		category: "knowledge",
		content:
			"OpenCode provider routing must fall back from paid model failures to another free model.",
		confidence: 0.6,
	});

	const [top] = await getRelevantMemoriesRanked(
		"opencode paid model triage fallback",
		1,
	);

	assert.equal(top.id, relevantId);
});
