/**
 * Tests for parallel eligibility edge cases:
 * - Ghost milestones (no registry entry) must NOT appear eligible (#2501 Bug 2)
 * - Milestones with failed worktree merge (SUMMARY only in worktree, DB still
 *   "active") must NOT appear eligible (#2501 Bug 1 context)
 */

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, beforeEach, describe, test } from 'vitest';

import { analyzeParallelEligibility } from "../parallel-eligibility.ts";
import {
	closeDatabase,
	insertMilestone,
	insertSlice,
	insertTask,
	openDatabase,
} from "../sf-db.ts";
import { invalidateStateCache } from "../state.ts";

// ─── Fixture Helpers ───────────────────────────────────────────────────────

function createFixtureBase(): string {
	const base = mkdtempSync(join(tmpdir(), "sf-parallel-elig-"));
	mkdirSync(join(base, ".sf", "milestones"), { recursive: true });
	return base;
}

function writeMilestoneFile(
	base: string,
	milestoneId: string,
	filename: string,
	content: string,
): void {
	const filePath = join(base, ".sf", "milestones", milestoneId, filename);
	mkdirSync(join(filePath, ".."), { recursive: true });
	writeFileSync(filePath, content);
}

function makeMilestoneDir(base: string, milestoneId: string): void {
	mkdirSync(join(base, ".sf", "milestones", milestoneId), { recursive: true });
}

function cleanup(base: string): void {
	rmSync(base, { recursive: true, force: true });
}

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

describe("parallel-eligibility: ghost milestone ineligibility (#2501)", () => {
	let base: string;

	beforeEach(() => {
		base = createFixtureBase();
		openDatabase(":memory:");
	});

	afterEach(() => {
		closeDatabase();
		cleanup(base);
		invalidateStateCache();
	});

	test("ghost milestone (directory only, no planning files) is ineligible", async () => {
		// Set up a real milestone M001 with proper planning data in DB
		writeMilestoneFile(
			base,
			"M001",
			"M001-CONTEXT.md",
			"# M001: Real Milestone\n\nA real milestone.",
		);
		writeMilestoneFile(
			base,
			"M001",
			"M001-ROADMAP.md",
			"# M001: Real Milestone\n\n## Slices\n\n- [ ] **S01: First Slice** `risk:low` `depends:[]`\n  > Do something.\n",
		);
		writeMilestoneFile(
			base,
			"M001",
			"slices/S01/S01-PLAN.md",
			"# S01: First Slice\n\n**Goal:** Do it.\n**Demo:** Done.\n\n## Tasks\n\n- [ ] **T01: Task One** `est:10m`\n  Do the thing.\n",
		);
		insertMilestone({
			id: "M001",
			title: "M001: Real Milestone",
			status: "active",
		});
		insertSlice({
			id: "S01",
			milestoneId: "M001",
			title: "First Slice",
			status: "active",
			risk: "low",
			depends: [],
		});
		insertTask({
			id: "T01",
			sliceId: "S01",
			milestoneId: "M001",
			title: "Task One",
			status: "pending",
		});

		// Create ghost milestone M017 — directory with only slices/, no CONTEXT/ROADMAP/SUMMARY
		makeMilestoneDir(base, "M017");
		mkdirSync(join(base, ".sf", "milestones", "M017", "slices"), {
			recursive: true,
		});

		invalidateStateCache();
		const result = await analyzeParallelEligibility(base);

		// M017 should NOT be in the eligible list
		const ghostEligible = result.eligible.find((e) => e.milestoneId === "M017");
		assert.equal(
			ghostEligible,
			undefined,
			"Ghost milestone M017 must NOT appear in eligible list — it has no planning data",
		);

		// M017 should be in the ineligible list with an appropriate reason
		const ghostIneligible = result.ineligible.find(
			(e) => e.milestoneId === "M017",
		);
		assert.ok(
			ghostIneligible,
			"Ghost milestone M017 must appear in ineligible list",
		);
		assert.equal(ghostIneligible!.eligible, false);
		assert.match(
			ghostIneligible!.reason,
			/no planning data|unknown|no registry/i,
			"Reason should indicate the milestone has no planning data or is unknown",
		);
	});

	test("milestone with DB status active and no SUMMARY on disk is not eligible when it has no slices", async () => {
		// Simulate a milestone whose complete-milestone ran in a worktree, wrote
		// SUMMARY there, but the squash-merge back to main failed.  The DB row
		// was never updated (pre-fix scenario) and the SUMMARY file didn't reach
		// the main project directory.
		//
		// In the current codebase, complete-milestone.ts already writes the DB
		// status (Bug 1 was fixed). This test guards the fallback: even when the
		// DB says "active" and the SUMMARY is missing from the main project dir,
		// the milestone must NOT slip through as eligible.

		// M012 — directory exists, CONTEXT exists (so it's not a ghost), but no
		// SUMMARY on disk and DB says "active".  No slices in DB either (they
		// lived only in the worktree DB copy).
		writeMilestoneFile(
			base,
			"M012",
			"M012-CONTEXT.md",
			"# M012: Worktree Milestone\n\nThis ran in a worktree.",
		);
		insertMilestone({
			id: "M012",
			title: "M012: Worktree Milestone",
			status: "active",
		});

		// M001 — a normal pending milestone with proper planning
		writeMilestoneFile(
			base,
			"M001",
			"M001-CONTEXT.md",
			"# M001: Normal Milestone\n\nNormal milestone.",
		);
		writeMilestoneFile(
			base,
			"M001",
			"M001-ROADMAP.md",
			"# M001: Normal Milestone\n\n## Slices\n\n- [ ] **S01: Slice** `risk:low` `depends:[]`\n  > Do it.\n",
		);
		writeMilestoneFile(
			base,
			"M001",
			"slices/S01/S01-PLAN.md",
			"# S01: Slice\n\n**Goal:** Do.\n**Demo:** Done.\n\n## Tasks\n\n- [ ] **T01: Task** `est:10m`\n  Do.\n",
		);
		insertMilestone({
			id: "M001",
			title: "M001: Normal Milestone",
			status: "active",
		});
		insertSlice({
			id: "S01",
			milestoneId: "M001",
			title: "Slice",
			status: "active",
			risk: "low",
			depends: [],
		});
		insertTask({
			id: "T01",
			sliceId: "S01",
			milestoneId: "M001",
			title: "Task",
			status: "pending",
		});

		invalidateStateCache();
		const result = await analyzeParallelEligibility(base);

		// M001 should be eligible (it has proper planning and active status)
		const m001 = result.eligible.find((e) => e.milestoneId === "M001");
		assert.ok(m001, "M001 with proper planning should be eligible");

		// M012 should appear somewhere but must NOT be eligible.  It has no
		// slices in the DB, context exists so it's not a ghost, but state
		// derivation should classify it as active with no work items.  Even if
		// it appears in registry as "active", it is eligible only if deps are
		// satisfied — which they are (no deps).  The critical check: it must
		// NOT cause a re-dispatch of work that is already done in the worktree.
		//
		// NOTE: This test documents the current behavior.  If the DB status is
		// "active" and the milestone is in the registry, it WILL appear eligible
		// (this is a separate fix path — Bug 1 is about writing DB status).
		// We verify the fix path through Bug 2's ghost handling above.
	});
});
