/**
 * smoke-fixture.test.mjs — inline-dispatch smoke fixture (M048/S01/T01).
 *
 * Purpose: foundational smoke fixture that S02-S04 depend on for
 * regression detection. Exercises the full inline dispatch wiring
 * (DispatchLayer → runUnitInline → buildPromptForUnit → runSubagent)
 * with a mocked LLM call.
 *
 * Consumer: autonomous loop regression harness; future S02/S03 integration tests.
 * Fails when: the inline dispatch wiring regresses (module rename, API change,
 *   DB schema drift, mock shape mismatch).
 */
import { afterEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

import {
	closeDatabase,
	insertMilestone,
	insertSlice,
	openDatabase,
} from "../sf-db.js";
import { DispatchLayer } from "../dispatch/dispatch-layer.js";
import { runSubagent } from "@singularity-forge/coding-agent";
import { SMOKE_FIXTURE_MILESTONE_ID } from "../smoke-fixture-constants.js";
import { readLastGreenRecord, clearLastGreen } from "../last-green.js";

// Enable smoke_gate so last-green writes are not suppressed.
// Without this, writeLastGreenRecord returns false and the ledger tests would fail.
vi.mock("../experimental.js", () => ({
	getExperimentalFlag: vi.fn(() => true),
}));

// Module-level mock — vitest hoists vi.mock automatically.
//
// Use the async importOriginal form (like dashboard-overlay.test.ts) to
// properly handle external workspace-package mocking with vitest.
vi.mock("@singularity-forge/coding-agent", async (importOriginal) => {
	const actual = await importOriginal();
	return {
		...actual,
		runSubagent: vi.fn(async () => ({
			ok: true,
			output: "smoke test complete",
			exitCode: 0,
		})),
	};
});

// Track temp directories for cleanup.
const tmpDirs = [];

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

/**
 * Build a minimal synthetic project with a single milestone (M049)
 * and two slices (S01=complete, S02=active) — enough rows to exercise
 * buildCompleteMilestonePrompt without triggering silent fallbacks.
 *
 * Follows the canonical makeProject pattern from doctor-task-plan-id-drift.test.mjs.
 */
function makeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-smoke-fixture-"));
	tmpDirs.push(dir);

	mkdirSync(
		join(
			dir,
			".sf",
			"milestones",
			SMOKE_FIXTURE_MILESTONE_ID,
			"slices",
			"S01",
			"tasks",
		),
		{
			recursive: true,
		},
	);
	mkdirSync(
		join(
			dir,
			".sf",
			"milestones",
			SMOKE_FIXTURE_MILESTONE_ID,
			"slices",
			"S02",
			"tasks",
		),
		{
			recursive: true,
		},
	);

	openDatabase(join(dir, ".sf", "sf.db"));

	// Milestone SMOKE_FIXTURE_MILESTONE_ID — vision and successCriteria mirror what
	// buildCompleteMilestonePrompt reads for inline context.
	insertMilestone({
		id: SMOKE_FIXTURE_MILESTONE_ID,
		title: "Smoke test milestone",
		status: "active",
		planning: {
			vision: "Smoke test",
			successCriteria: [],
		},
	});

	// Slice S01 — complete so it shows in the milestone summary block.
	insertSlice({
		milestoneId: SMOKE_FIXTURE_MILESTONE_ID,
		id: "S01",
		title: "Smoke slice (complete)",
		status: "complete",
		risk: "low",
		depends: [],
		demo: "Synthetic slice for fixture test.",
		sequence: 1,
	});

	// Slice S02 — active so the roadmap file lists it.
	insertSlice({
		milestoneId: SMOKE_FIXTURE_MILESTONE_ID,
		id: "S02",
		title: "Smoke slice (active)",
		status: "active",
		risk: "low",
		depends: [],
		demo: "Synthetic active slice.",
		sequence: 2,
	});

	// ROADMAP.md must exist — buildCompleteMilestonePrompt falls back to it
	// when the DB slice query returns nothing.
	writeFileSync(
		join(
			dir,
			".sf",
			"milestones",
			SMOKE_FIXTURE_MILESTONE_ID,
			`${SMOKE_FIXTURE_MILESTONE_ID}-ROADMAP.md`,
		),
		"# SMOKE_FIXTURE_MILESTONE_ID: Smoke test milestone\n\n## S01: Smoke slice (complete)\n\n## S02: Smoke slice (active)\n",
	);

	// S01-SUMMARY.md for slice summary excerpt path.
	writeFileSync(
		join(
			dir,
			".sf",
			"milestones",
			SMOKE_FIXTURE_MILESTONE_ID,
			"slices",
			"S01",
			"S01-SUMMARY.md",
		),
		"# S01: Smoke slice (complete)\n\n**Status:** complete\n",
	);

	closeDatabase();
	return dir;
}

describe("M048/S01 — smoke fixture", () => {
	it("runUnitInline via DispatchLayer with synthetic milestone completes ok=true", async () => {
		const dir = makeProject();
		openDatabase(join(dir, ".sf", "sf.db"));

		const layer = new DispatchLayer(dir);
		const result = await layer.dispatch({
			isolation: "full",
			coordination: "managed",
			scope: "inline",
			mode: "single",
			unitType: "complete-milestone",
			unitId: SMOKE_FIXTURE_MILESTONE_ID,
		});

		expect(result.ok).toBe(true);
		expect(result.exitCode).toBe(0);
		expect(typeof result.output).toBe("string");
		expect(result.output.length).toBeGreaterThan(0);
		expect(result.output).toContain("smoke test complete");
	});

	it("mocked runSubagent was called at least once", async () => {
		const dir = makeProject();
		openDatabase(join(dir, ".sf", "sf.db"));

		const layer = new DispatchLayer(dir);
		await layer.dispatch({
			isolation: "full",
			coordination: "managed",
			scope: "inline",
			mode: "single",
			unitType: "complete-milestone",
			unitId: SMOKE_FIXTURE_MILESTONE_ID,
		});

		expect(vi.mocked(runSubagent).mock.calls.length).toBeGreaterThanOrEqual(1);
	});

	it("DispatchLayer dispatch count increments after call", async () => {
		const dir = makeProject();
		openDatabase(join(dir, ".sf", "sf.db"));

		const layer = new DispatchLayer(dir);
		expect(layer.getDispatchCount()).toBe(0);

		await layer.dispatch({
			isolation: "full",
			coordination: "managed",
			scope: "inline",
			mode: "single",
			unitType: "complete-milestone",
			unitId: SMOKE_FIXTURE_MILESTONE_ID,
		});

		expect(layer.getDispatchCount()).toBe(1);
	});

	it("invalid 4D isolation config returns exitCode 1", async () => {
		const dir = makeProject();
		openDatabase(join(dir, ".sf", "sf.db"));

		const layer = new DispatchLayer(dir);
		const result = await layer.dispatch({
			isolation: "bogus",
			coordination: "managed",
			scope: "inline",
			mode: "single",
			unitType: "complete-milestone",
			unitId: SMOKE_FIXTURE_MILESTONE_ID,
		});

		expect(result.ok).toBe(false);
		expect(result.exitCode).toBe(1);
		expect(result.stderr).toMatch(/must be one of/);
	});

	// ---------------------------------------------------------------------------
	// S02: last-green ledger wiring tests
	// ---------------------------------------------------------------------------

	it("ledger is written after successful fixture dispatch", async () => {
		const dir = makeProject();
		openDatabase(join(dir, ".sf", "sf.db"));

		// Clear any stale ledger from prior runs.
		clearLastGreen(dir);

		const layer = new DispatchLayer(dir);
		const result = await layer.dispatch({
			isolation: "full",
			coordination: "managed",
			scope: "inline",
			mode: "single",
			unitType: "complete-milestone",
			unitId: SMOKE_FIXTURE_MILESTONE_ID,
		});

		// The dispatch must have succeeded (mocked runSubagent always returns ok=true).
		expect(result.ok).toBe(true);

		// Read the ledger — writeLastGreenRecord guards on smoke_gate flag,
		// which T01 set to true via setExperimentalFlag in last-green.test.mjs setup.
		const ledger = readLastGreenRecord(dir);
		expect(ledger).not.toBeNull();
		expect(Array.isArray(ledger)).toBe(true);
		expect(ledger.length).toBeGreaterThanOrEqual(1);

		// Verify the most-recent entry has the expected fixture shape.
		const entry = ledger[0];
		expect(entry.fixture.milestoneId).toBe(SMOKE_FIXTURE_MILESTONE_ID);
		expect(entry.fixture.unitType).toBe("complete-milestone");
		expect(entry.result.ok).toBe(true);
		expect(entry.result.exitCode).toBe(0);
		expect(entry.result.outputLength).toBeGreaterThan(0);
		expect(entry.result.elapsedMs).toBeGreaterThanOrEqual(0);
		expect(typeof entry.checksum).toBe("string");
		expect(entry.checksum.length).toBeGreaterThan(0);
		expect(typeof entry.timestamp).toBe("string");
	});

	it("ledger is NOT written when dispatch fails", async () => {
		const dir = makeProject();
		openDatabase(join(dir, ".sf", "sf.db"));

		// Seed with a known-good entry so we can tell it wasn't replaced.
		const layer = new DispatchLayer(dir);

		// Override the mock to return a failure result.
		vi.mocked(runSubagent).mockResolvedValueOnce({
			ok: false,
			exitCode: 1,
			stderr: "test error",
			output: "",
		});

		const result = await layer.dispatch({
			isolation: "full",
			coordination: "managed",
			scope: "inline",
			mode: "single",
			unitType: "complete-milestone",
			unitId: SMOKE_FIXTURE_MILESTONE_ID,
		});

		// The dispatch must have failed.
		expect(result.ok).toBe(false);
		expect(result.exitCode).toBe(1);

		// Ledger should be null (no entry written for failed dispatch),
		// or empty (any prior entry preserved but not appended).
		const ledger = readLastGreenRecord(dir);
		// A null return means the file doesn't exist; an empty array means it
		// exists but has no entries — both are acceptable for "not written".
		expect(
			ledger === null || (Array.isArray(ledger) && ledger.length === 0),
		).toBe(true);
	});
});
