import assert from "node:assert/strict";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, test } from "vitest";
import {
	type PersistedSelfFeedbackEntry,
	markResolved,
	readAllSelfFeedback,
} from "../self-feedback.ts";
import { bridgeUpstreamFeedback } from "../upstream-bridge.ts";

// ─── Test scaffolding ─────────────────────────────────────────────────────────

let tmpBase: string;
let forgeDir: string;
let sfHomeDir: string;
let upstreamLog: string;
let originalSfHome: string | undefined;

beforeEach(() => {
	tmpBase = mkdtempSync(join(tmpdir(), "sf-upstream-bridge-"));
	forgeDir = join(tmpBase, "forge");
	sfHomeDir = join(tmpBase, "sf-home");
	upstreamLog = join(sfHomeDir, "agent", "upstream-feedback.jsonl");

	mkdirSync(join(forgeDir, ".sf"), { recursive: true });
	mkdirSync(join(sfHomeDir, "agent"), { recursive: true });

	// Fake forge repo — name matches but no loader.ts (so sfRuntimeRoot → basePath/.sf)
	writeFileSync(
		join(forgeDir, "package.json"),
		JSON.stringify({ name: "singularity-forge", version: "0.0.1" }),
		"utf-8",
	);

	originalSfHome = process.env.SF_HOME;
	process.env.SF_HOME = sfHomeDir;
});

afterEach(() => {
	if (originalSfHome === undefined) delete process.env.SF_HOME;
	else process.env.SF_HOME = originalSfHome;
	rmSync(tmpBase, { recursive: true, force: true });
});

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makeEntry(
	overrides: Partial<PersistedSelfFeedbackEntry> = {},
): PersistedSelfFeedbackEntry {
	return {
		id: `sf-${Math.random().toString(36).slice(2, 10)}`,
		kind: "runaway-guard-hard-pause",
		severity: "medium",
		summary: "Runaway guard triggered",
		source: "detector",
		ts: new Date().toISOString(),
		basePath: "/some/external/repo",
		repoIdentity: "external",
		sfVersion: "1.0.0",
		blocking: false,
		...overrides,
	};
}

function writeUpstream(entries: PersistedSelfFeedbackEntry[]): void {
	const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
	writeFileSync(upstreamLog, lines, "utf-8");
}

function readForgeJsonl(): PersistedSelfFeedbackEntry[] {
	return readAllSelfFeedback(forgeDir);
}

// ─── Tests ────────────────────────────────────────────────────────────────────

test("bails silently when basePath is not forge — no package.json", () => {
	const nonForge = join(tmpBase, "other");
	mkdirSync(nonForge, { recursive: true });

	writeUpstream([
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/b" }),
		makeEntry({ basePath: "/repo/c" }),
	]);

	const count = bridgeUpstreamFeedback(nonForge);
	assert.equal(count, 0);
	// No forge-local jsonl created
	assert.ok(!existsSync(join(nonForge, ".sf", "self-feedback.jsonl")));
});

test("bails silently when basePath has wrong package name", () => {
	const nonForge = join(tmpBase, "other2");
	mkdirSync(nonForge, { recursive: true });
	writeFileSync(
		join(nonForge, "package.json"),
		JSON.stringify({ name: "some-other-tool" }),
		"utf-8",
	);

	writeUpstream([
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/b" }),
		makeEntry({ basePath: "/repo/c" }),
	]);

	assert.equal(bridgeUpstreamFeedback(nonForge), 0);
});

test("files a rollup when ≥3 entries of same kind from ≥2 distinct repos", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a", kind: "runaway-guard-hard-pause" }),
		makeEntry({ basePath: "/repo/b", kind: "runaway-guard-hard-pause" }),
		makeEntry({ basePath: "/repo/c", kind: "runaway-guard-hard-pause" }),
	]);

	const count = bridgeUpstreamFeedback(forgeDir);
	assert.equal(count, 1);

	const entries = readForgeJsonl();
	assert.equal(entries.length, 1);
	const rollup = entries[0];
	assert.equal(rollup.kind, "upstream-rollup:runaway-guard-hard-pause");
	assert.equal(rollup.source, "detector");
	assert.match(rollup.summary, /3 external-repo entries/);
	assert.match(rollup.summary, /3 repos/);

	// Rollup appears in BACKLOG.md
	const backlog = readFileSync(join(forgeDir, ".sf", "BACKLOG.md"), "utf-8");
	assert.match(backlog, /upstream-rollup:runaway-guard-hard-pause/);
});

test("idempotent — re-running with same upstream state files 0 new entries", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/b" }),
		makeEntry({ basePath: "/repo/c" }),
	]);

	const first = bridgeUpstreamFeedback(forgeDir);
	assert.equal(first, 1);

	const second = bridgeUpstreamFeedback(forgeDir);
	assert.equal(second, 0);

	assert.equal(readForgeJsonl().length, 1);
});

test("skips a kind when an open rollup for it already exists", () => {
	// Pre-populate forge backlog with an open rollup of the same kind
	writeUpstream([
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/b" }),
		makeEntry({ basePath: "/repo/c" }),
	]);
	bridgeUpstreamFeedback(forgeDir); // file initial rollup

	// Add more upstream entries
	const existing = JSON.parse(
		readFileSync(upstreamLog, "utf-8").trim().split("\n")[0],
	);
	writeUpstream([
		existing as PersistedSelfFeedbackEntry,
		makeEntry({ basePath: "/repo/d" }),
		makeEntry({ basePath: "/repo/e" }),
		makeEntry({ basePath: "/repo/f" }),
	]);

	const count = bridgeUpstreamFeedback(forgeDir);
	assert.equal(count, 0, "should not re-file while open rollup exists");
	assert.equal(readForgeJsonl().length, 1);
});

test("re-fires after the previous rollup is resolved and new upstream entries arrive", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/b" }),
		makeEntry({ basePath: "/repo/c" }),
	]);
	bridgeUpstreamFeedback(forgeDir);

	// Resolve the open rollup
	const [rollup] = readForgeJsonl();
	const resolved = markResolved(
		rollup.id,
		{ reason: "fixed", evidence: { kind: "human-clear" } },
		forgeDir,
	);
	assert.ok(resolved);

	// New upstream entries still present → should re-file
	const count = bridgeUpstreamFeedback(forgeDir);
	assert.equal(count, 1);
	const entries = readForgeJsonl();
	assert.equal(entries.filter((e) => !e.resolvedAt).length, 1);
});

test("filters out entries older than 30 days", () => {
	const oldTs = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString();
	writeUpstream([
		makeEntry({ basePath: "/repo/a", ts: oldTs }),
		makeEntry({ basePath: "/repo/b", ts: oldTs }),
		makeEntry({ basePath: "/repo/c", ts: oldTs }),
	]);

	assert.equal(bridgeUpstreamFeedback(forgeDir), 0);
	assert.equal(readForgeJsonl().length, 0);
});

test("caps severity at medium even when contributing entries are higher", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a", severity: "critical" }),
		makeEntry({ basePath: "/repo/b", severity: "high" }),
		makeEntry({ basePath: "/repo/c", severity: "critical" }),
	]);

	bridgeUpstreamFeedback(forgeDir);
	const [rollup] = readForgeJsonl();
	assert.equal(rollup.severity, "medium");
	assert.equal(rollup.blocking, false);
});

test("does not file when count < 3", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/b" }),
	]);

	assert.equal(bridgeUpstreamFeedback(forgeDir), 0);
});

test("does not file when distinct repos < 2 (all from same repo)", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/a" }),
		makeEntry({ basePath: "/repo/a" }),
	]);

	assert.equal(bridgeUpstreamFeedback(forgeDir), 0);
});

test("skips resolved upstream entries", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a", resolvedAt: new Date().toISOString() }),
		makeEntry({ basePath: "/repo/b", resolvedAt: new Date().toISOString() }),
		makeEntry({ basePath: "/repo/c", resolvedAt: new Date().toISOString() }),
	]);

	assert.equal(bridgeUpstreamFeedback(forgeDir), 0);
});

test("handles multiple distinct kinds independently", () => {
	writeUpstream([
		makeEntry({ basePath: "/repo/a", kind: "kind-alpha" }),
		makeEntry({ basePath: "/repo/b", kind: "kind-alpha" }),
		makeEntry({ basePath: "/repo/c", kind: "kind-alpha" }),
		makeEntry({ basePath: "/repo/a", kind: "kind-beta" }),
		makeEntry({ basePath: "/repo/b", kind: "kind-beta" }),
		makeEntry({ basePath: "/repo/c", kind: "kind-beta" }),
	]);

	const count = bridgeUpstreamFeedback(forgeDir);
	assert.equal(count, 2);
	const entries = readForgeJsonl();
	const kinds = new Set(entries.map((e) => e.kind));
	assert.ok(kinds.has("upstream-rollup:kind-alpha"));
	assert.ok(kinds.has("upstream-rollup:kind-beta"));
});

test("returns 0 when upstream log does not exist", () => {
	// Don't write any upstream log
	assert.equal(bridgeUpstreamFeedback(forgeDir), 0);
});
