/**
 * upstream-bridge.test.mjs — closure-aware idempotency.
 *
 * Validates that bridgeUpstreamFeedback respects:
 *   - currently-OPEN rollup kinds (existing behavior)
 *   - rollup kinds CLOSED within the last 30 days (new behavior addressing
 *     sf-mp4rp6xn-hpag5h: bridge re-filed every closed cluster on every
 *     session_start because it only checked open state)
 *   - allows re-file when the same kind was closed OUTSIDE the lookback
 */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { markResolved, recordSelfFeedback } from "../self-feedback.js";
import {
	closeDatabase,
	listSelfFeedbackEntries,
	openDatabase,
} from "../sf-db.js";
import { bridgeUpstreamFeedback } from "../upstream-bridge.js";

const tmpDirs = [];
const ORIGINAL_SF_HOME = process.env.SF_HOME;

beforeEach(() => {
	delete process.env.SF_HOME;
});

afterEach(() => {
	closeDatabase();
	if (ORIGINAL_SF_HOME === undefined) {
		delete process.env.SF_HOME;
	} else {
		process.env.SF_HOME = ORIGINAL_SF_HOME;
	}
	while (tmpDirs.length > 0) {
		const dir = tmpDirs.pop();
		if (dir) rmSync(dir, { recursive: true, force: true });
	}
});

function makeForgeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-bridge-"));
	tmpDirs.push(dir);
	mkdirSync(join(dir, ".sf"), { recursive: true });
	writeFileSync(
		join(dir, "package.json"),
		JSON.stringify({ name: "singularity-forge" }),
	);
	openDatabase(join(dir, ".sf", "sf.db"));
	return dir;
}

function makeSfHome(upstreamEntries) {
	const dir = mkdtempSync(join(tmpdir(), "sf-home-"));
	tmpDirs.push(dir);
	mkdirSync(join(dir, "agent"), { recursive: true });
	const path = join(dir, "agent", "upstream-feedback.jsonl");
	writeFileSync(
		path,
		upstreamEntries.map((e) => JSON.stringify(e)).join("\n") + "\n",
	);
	process.env.SF_HOME = dir;
	return dir;
}

function makeUpstreamEntry(overrides = {}) {
	return {
		schemaVersion: 1,
		id: `sf-upstream-${Math.random().toString(36).slice(2, 8)}`,
		ts: new Date().toISOString(),
		kind: "runaway-guard-hard-pause",
		severity: "medium",
		blocking: false,
		summary: "synthetic upstream observation",
		evidence: "tmpdir test",
		repoIdentity: "external",
		sfVersion: "test",
		basePath: `/some/external/repo-${Math.random().toString(36).slice(2, 6)}`,
		...overrides,
	};
}

describe("bridgeUpstreamFeedback", () => {
	test("files a new rollup when threshold met and no prior matching kind", () => {
		const project = makeForgeProject();
		// 3 entries of same kind, 2 distinct repos (meets THRESHOLD_COUNT=3, THRESHOLD_REPOS=2)
		makeSfHome([
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/b" }),
		]);

		const filed = bridgeUpstreamFeedback(project);
		expect(filed).toBe(1);
		const entries = listSelfFeedbackEntries();
		const rollup = entries.find((e) => e.kind?.startsWith("upstream-rollup:"));
		expect(rollup).toBeTruthy();
		expect(rollup.kind).toBe("upstream-rollup:runaway-guard-hard-pause");
	});

	test("skips when an OPEN rollup of the same kind already exists", () => {
		const project = makeForgeProject();
		makeSfHome([
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/b" }),
		]);

		expect(bridgeUpstreamFeedback(project)).toBe(1);
		expect(bridgeUpstreamFeedback(project)).toBe(0);
	});

	test("skips when a same-kind rollup was RESOLVED within the lookback window (sf-mp4rp6xn-hpag5h)", async () => {
		const project = makeForgeProject();
		makeSfHome([
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/b" }),
		]);

		// File once, then close with human-clear (today)
		expect(bridgeUpstreamFeedback(project)).toBe(1);
		const filed = listSelfFeedbackEntries().find((e) =>
			e.kind?.startsWith("upstream-rollup:"),
		);
		const ok = markResolved(
			filed.id,
			{
				reason: "out of scope for inline-fix",
				evidence: { kind: "human-clear" },
			},
			project,
		);
		expect(ok).toBe(true);

		// Re-running the bridge MUST NOT re-file. This is the regression guard.
		const refiled = bridgeUpstreamFeedback(project);
		expect(refiled).toBe(0);
	});

	test("allows re-file when same-kind rollup was resolved BEFORE lookback (>30d)", async () => {
		const project = makeForgeProject();
		makeSfHome([
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/b" }),
		]);

		expect(bridgeUpstreamFeedback(project)).toBe(1);
		const filed = listSelfFeedbackEntries().find((e) =>
			e.kind?.startsWith("upstream-rollup:"),
		);
		markResolved(
			filed.id,
			{ reason: "ancient close", evidence: { kind: "human-clear" } },
			project,
		);
		// Back-date the resolution to 60 days ago via direct DB update
		const oldTs = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
		const { _getAdapter } = await import("../sf-db/sf-db-core.js");
		_getAdapter()
			.prepare("UPDATE self_feedback SET resolved_at = :ts WHERE id = :id")
			.run({ ":ts": oldTs, ":id": filed.id });

		// Bridge should re-file because the closure is now stale (>30 days)
		const refiled = bridgeUpstreamFeedback(project);
		expect(refiled).toBe(1);
	});

	test("doesn't fire below threshold (2 entries, 2 repos < 3-count threshold)", () => {
		const project = makeForgeProject();
		makeSfHome([
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/b" }),
		]);
		expect(bridgeUpstreamFeedback(project)).toBe(0);
	});

	test("doesn't fire below repo-distinct threshold (3 entries, 1 repo)", () => {
		const project = makeForgeProject();
		makeSfHome([
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/a" }),
		]);
		expect(bridgeUpstreamFeedback(project)).toBe(0);
	});

	test("returns 0 when basePath isn't the forge repo", () => {
		const dir = mkdtempSync(join(tmpdir(), "sf-non-forge-"));
		tmpDirs.push(dir);
		writeFileSync(
			join(dir, "package.json"),
			JSON.stringify({ name: "some-other-app" }),
		);
		makeSfHome([
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/a" }),
			makeUpstreamEntry({ basePath: "/b" }),
		]);
		expect(bridgeUpstreamFeedback(dir)).toBe(0);
	});
});
