/**
 * canonical-id-mapping.test.mjs
 *
 * Verify that canonicalIdFor() maps every production route key that has
 * been observed in the journal to a non-null canonical id, and that
 * backfillModelPerformanceFromJournal() produces non-_unmapped buckets
 * when given a mock journal.
 */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

// ── canonicalIdFor tests ──────────────────────────────────────────────────────

describe("canonicalIdFor — production route keys", async () => {
	const { canonicalIdFor } = await import("../model-registry.js");

	// Route keys observed in .sf/journal/* via model_auto_resolved events.
	const expectedMappings = [
		// kimi-coding
		["kimi-coding/kimi-k2.6", "kimi-k2.6"],
		["kimi-coding/kimi-for-coding", "kimi-k2.6"], // alias → same canonical
		// minimax
		["minimax/MiniMax-M2.7-highspeed", "MiniMax-M2.7-highspeed"],
		["minimax/MiniMax-M2.7", "MiniMax-M2.7"],
		["minimax/MiniMax-M2.1", "minimax-m2.1"],
		// zai (direct provider, not via openrouter)
		["zai/glm-4.5", "glm-4.5"],
		// opencode-go
		["opencode-go/kimi-k2.5", "kimi-k2.5"],
		// mistral (direct provider, not via openrouter)
		["mistral/codestral-latest", "codestral-latest"],
		// google-gemini-cli
		["google-gemini-cli/gemini-3-flash-preview", "gemini-3-flash-preview"],
		["google-gemini-cli/gemini-3.1-pro-preview", "gemini-3.1-pro-preview"],
		["google-gemini-cli/gemini-2.5-pro", "gemini-2.5-pro"],
	];

	for (const [routeKey, expectedCanonical] of expectedMappings) {
		it(`maps "${routeKey}" → "${expectedCanonical}"`, () => {
			const result = canonicalIdFor(routeKey);
			expect(result).not.toBeNull();
			expect(result).toBe(expectedCanonical);
		});
	}

	it("returns null for nonexistent/foo (no false positives)", () => {
		expect(canonicalIdFor("nonexistent/foo")).toBeNull();
	});

	it("bare canonical id (no provider prefix) resolves via _ENTRY_BY_ROUTE fallback to itself", () => {
		// "kimi-k2.6" is a wire_id under kimi-coding — if it happened to be a
		// provider-less route key it has no _ENTRY_BY_ROUTE entry, so returns null.
		// This test documents the expected behaviour: bare ids WITHOUT a provider
		// are not looked up (they never appear as route keys in practice).
		// Bare ids used as canonicalId strings are never passed to canonicalIdFor.
		expect(canonicalIdFor("kimi-k2.6")).toBeNull();
	});
});

// ── backfillModelPerformanceFromJournal tests ─────────────────────────────────

describe("backfillModelPerformanceFromJournal — mock journal", async () => {
	const { backfillModelPerformanceFromJournal, setRegistryResolver } =
		await import("../model-learner.js");

	let tmpDir;

	beforeEach(() => {
		tmpDir = mkdtempSync(join(tmpdir(), "sf-backfill-test-"));
		mkdirSync(join(tmpDir, ".sf", "journal"), { recursive: true });

		// Inject a stub resolver so tests don't need the full @singularity-forge/ai stack.
		setRegistryResolver((rk) => {
			const mappings = {
				"kimi-coding/kimi-k2.6": "kimi-k2.6",
				"minimax/MiniMax-M2.7-highspeed": "MiniMax-M2.7-highspeed",
				"zai/glm-4.5": "glm-4.5",
				"mistral/codestral-latest": "codestral-latest",
				"opencode-go/kimi-k2.5": "kimi-k2.5",
			};
			return mappings[rk] ?? null;
		});
	});

	afterEach(() => {
		rmSync(tmpDir, { recursive: true, force: true });
		// Reset to null so other tests use the real registry.
		setRegistryResolver(null);
	});

	function writeJournalLine(date, event) {
		const file = join(tmpDir, ".sf", "journal", `${date}.jsonl`);
		writeFileSync(file, JSON.stringify(event) + "\n", { flag: "a" });
	}

	it("maps dispatched routes to non-_unmapped canonical buckets", async () => {
		// Dispatch: kimi-coding/kimi-k2.6 on traceId aaa
		writeJournalLine("2026-05-15", {
			schemaVersion: 1,
			eventType: "model_auto_resolved",
			data: {
				eventType: "model_auto_resolved",
				unitType: "execute-task",
				traceId: "aaa",
				data: {
					resolvedProvider: "kimi-coding",
					resolvedModel: "kimi-k2.6",
					routing: { tier: "standard", modelDowngraded: false },
				},
			},
		});

		// Completion for traceId aaa
		writeJournalLine("2026-05-15", {
			schemaVersion: 1,
			eventType: "unit_completed",
			data: {
				eventType: "unit_completed",
				traceId: "aaa",
				data: {
					status: "completed",
					tokens: 1000,
					cost_usd: 0.01,
				},
			},
		});

		// Dispatch: zai/glm-4.5 on traceId bbb (no completion = assume success)
		writeJournalLine("2026-05-15", {
			schemaVersion: 1,
			eventType: "model_auto_resolved",
			data: {
				eventType: "model_auto_resolved",
				unitType: "research-slice",
				traceId: "bbb",
				data: {
					resolvedProvider: "zai",
					resolvedModel: "glm-4.5",
					routing: { tier: "standard", modelDowngraded: false },
				},
			},
		});

		const result = await backfillModelPerformanceFromJournal(tmpDir);

		expect(result.written).toBe(2);
		expect(result.unmapped).toBe(0);

		// Verify the written file has the correct structure.
		const { readFileSync } = await import("node:fs");
		const perfPath = join(tmpDir, ".sf", "model-performance.json");
		const data = JSON.parse(readFileSync(perfPath, "utf-8"));

		// execute-task → kimi-k2.6 bucket
		expect(data["execute-task"]).toBeDefined();
		expect(data["execute-task"]["kimi-k2.6"]).toBeDefined();
		expect(data["execute-task"]["kimi-k2.6"].aggregate.successes).toBe(1);
		expect(data["execute-task"]["_unmapped"]).toBeUndefined();

		// research-slice → glm-4.5 bucket
		expect(data["research-slice"]["glm-4.5"]).toBeDefined();
		expect(data["research-slice"]["glm-4.5"].aggregate.successes).toBe(1);
	});

	it("puts unknown routes into _unmapped (no false positives)", async () => {
		writeJournalLine("2026-05-15", {
			schemaVersion: 1,
			eventType: "model_auto_resolved",
			data: {
				eventType: "model_auto_resolved",
				unitType: "execute-task",
				traceId: "zzz",
				data: {
					resolvedProvider: "unknown-provider",
					resolvedModel: "mystery-model",
					routing: { tier: "standard", modelDowngraded: false },
				},
			},
		});

		const result = await backfillModelPerformanceFromJournal(tmpDir);
		expect(result.unmapped).toBe(1);
		expect(result.written).toBe(0);
	});

	it("backs up existing model-performance.json before overwriting", async () => {
		const perfPath = join(tmpDir, ".sf", "model-performance.json");
		writeFileSync(perfPath, JSON.stringify({ old: true }), "utf-8");

		// Write a minimal dispatch so the function has something to process.
		writeJournalLine("2026-05-15", {
			schemaVersion: 1,
			eventType: "model_auto_resolved",
			data: {
				eventType: "model_auto_resolved",
				unitType: "execute-task",
				traceId: "ccc",
				data: {
					resolvedProvider: "kimi-coding",
					resolvedModel: "kimi-k2.6",
				},
			},
		});

		const result = await backfillModelPerformanceFromJournal(tmpDir);
		expect(result.backupPath).not.toBeNull();
		expect(result.backupPath).toMatch(/model-performance\.json\.bak\.\d+$/);

		// Old content is in the backup.
		const { readFileSync } = await import("node:fs");
		const backup = JSON.parse(readFileSync(result.backupPath, "utf-8"));
		expect(backup.old).toBe(true);
	});
});
