/**
 * auto-start-needs-discussion.test.ts — Regression tests for #1726.
 *
 * When a milestone has only CONTEXT-DRAFT.md (phase: needs-discussion),
 * bootstrapAutoSession had two bugs:
 *
 *   1. The survivor branch check included needs-discussion, so a branch
 *      created by a prior failed bootstrap caused hasSurvivorBranch = true,
 *      skipping all showWorkflowEntry calls.
 *
 *   2. No needs-discussion handler existed in the !hasSurvivorBranch block,
 *      so the phase fell through to auto-mode which immediately stopped
 *      with "needs its own discussion before planning."
 *
 * Together these created an infinite loop: /sf creates worktree + branch,
 * stops immediately, next run detects the branch and skips entry, auto-mode
 * dispatches needs-discussion → stop, repeat.
 *
 * These tests verify:
 *   - deriveState correctly identifies needs-discussion phase
 *   - The survivor branch filter in auto-start.ts excludes needs-discussion
 *   - The !hasSurvivorBranch block has a needs-discussion handler
 */

import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { describe, test } from 'vitest';
import { fileURLToPath } from "node:url";
import { invalidateAllCaches } from "../cache.ts";
import { deriveState } from "../state.ts";

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

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

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

function writeContextDraft(base: string, mid: string, content: string): void {
	const dir = join(base, ".sf", "milestones", mid);
	mkdirSync(dir, { recursive: true });
	writeFileSync(join(dir, `${mid}-CONTEXT-DRAFT.md`), content);
}

function writeContext(base: string, mid: string, content: string): void {
	const dir = join(base, ".sf", "milestones", mid);
	mkdirSync(dir, { recursive: true });
	writeFileSync(join(dir, `${mid}-CONTEXT.md`), content);
}

function writeRoadmap(base: string, mid: string, content: string): void {
	const dir = join(base, ".sf", "milestones", mid);
	mkdirSync(dir, { recursive: true });
	writeFileSync(join(dir, `${mid}-ROADMAP.md`), content);
}

// ─── Source code analysis helper ─────────────────────────────────────────────

function readAutoStartSource(): string {
	const thisFile = fileURLToPath(import.meta.url);
	const thisDir = dirname(thisFile);
	return readFileSync(join(thisDir, "..", "auto-start.ts"), "utf-8");
}

// ═══════════════════════════════════════════════════════════════════════════════
// Tests
// ═══════════════════════════════════════════════════════════════════════════════

describe("auto-start-needs-discussion (#1726)", () => {
	test("1. CONTEXT-DRAFT.md only → needs-discussion phase", async () => {
		const base = createBase();
		try {
			writeContextDraft(base, "M001", "# Draft\nSeed discussion.");
			invalidateAllCaches();
			const state = await deriveState(base);
			assert.strictEqual(
				state.phase,
				"needs-discussion",
				"milestone with only CONTEXT-DRAFT should be needs-discussion",
			);
			assert.ok(
				!!state.activeMilestone,
				"activeMilestone should be set for needs-discussion",
			);
			assert.strictEqual(
				state.activeMilestone?.id,
				"M001",
				"activeMilestone.id should be M001",
			);
		} finally {
			cleanup(base);
		}
	});

	test("2. Survivor branch check excludes needs-discussion", () => {
		const source = readAutoStartSource();

		// Find the survivor branch check block (Milestone branch recovery comment)
		const survivorBlock = source.match(
			/\/\/ Milestone branch recovery.*?hasSurvivorBranch = nativeBranchExists/s,
		);
		assert.ok(
			!!survivorBlock,
			"found survivor branch check block in auto-start.ts",
		);

		if (survivorBlock) {
			const block = survivorBlock[0];
			// The condition should only check pre-planning, NOT needs-discussion
			assert.ok(
				!block.includes("needs-discussion"),
				"survivor branch filter must NOT include needs-discussion phase",
			);
			assert.ok(
				block.includes("pre-planning"),
				"survivor branch filter should include pre-planning phase",
			);
		}
	});

	test("3. needs-discussion handler exists in bootstrap", () => {
		const source = readAutoStartSource();

		// After the pre-planning handler, there should be a needs-discussion handler
		// that calls showWorkflowEntry
		const needsDiscussionHandler = source.match(
			/if\s*\(state\.phase\s*===\s*"needs-discussion"\)\s*\{[^}]*showWorkflowEntry/s,
		);
		assert.ok(
			!!needsDiscussionHandler,
			"needs-discussion handler calling showWorkflowEntry must exist in !hasSurvivorBranch block",
		);
	});

	test("4. needs-discussion handler has abort path", () => {
		const source = readAutoStartSource();

		// The handler should check postState.phase !== "needs-discussion" and abort
		// if discussion didn't promote the draft
		assert.ok(
			source.includes('postState.phase !== "needs-discussion"'),
			"needs-discussion handler must check if phase advanced after showWorkflowEntry",
		);
		assert.ok(
			source.includes("milestone draft was not promoted"),
			"needs-discussion handler must have abort message when draft not promoted",
		);
	});

	test("5. Full context + roadmap → not needs-discussion", async () => {
		const base = createBase();
		try {
			writeContextDraft(base, "M001", "# Draft\nSeed discussion.");
			writeContext(base, "M001", "# Context\nFull context.");
			writeRoadmap(
				base,
				"M001",
				"# M001: Test\n\n## Slices\n- [ ] **S01: Test Slice** `risk:low` `depends:[]`\n  > After this: works\n",
			);
			invalidateAllCaches();
			const state = await deriveState(base);
			assert.ok(
				state.phase !== "needs-discussion",
				"milestone with full context + roadmap should NOT be needs-discussion",
			);
		} finally {
			cleanup(base);
		}
	});

	test("6. No infinite loop: needs-discussion always routes to showWorkflowEntry", () => {
		const source = readAutoStartSource();

		// Verify needs-discussion does NOT appear in auto-dispatch trigger conditions
		// within auto-start.ts. The only place needs-discussion should appear is in
		// the showWorkflowEntry routing block.
		const survivorSection = source.match(
			/\/\/ Milestone branch recovery.*?let hasSurvivorBranch = false;[\s\S]*?if\s*\([^)]*state\.phase[^)]*\)\s*\{/,
		);
		if (survivorSection) {
			assert.ok(
				!survivorSection[0].includes("needs-discussion"),
				"survivor branch phase condition must not mention needs-discussion",
			);
		}

		// Verify needs-discussion IS handled inside the !hasSurvivorBranch block
		const notSurvivorBlock = source.match(
			/if\s*\(!hasSurvivorBranch\)\s*\{([\s\S]*?)\/\/ Unreachable safety check/,
		);
		assert.ok(
			!!notSurvivorBlock,
			"found !hasSurvivorBranch block in auto-start.ts",
		);
		if (notSurvivorBlock) {
			assert.ok(
				notSurvivorBlock[1].includes('"needs-discussion"'),
				"!hasSurvivorBranch block must handle needs-discussion phase",
			);
		}
	});

	test("7. Survivor branch + needs-discussion routes to showWorkflowEntry", () => {
		const source = readAutoStartSource();

		// When hasSurvivorBranch is true AND phase is needs-discussion, the code
		// must route to showWorkflowEntry instead of falling through to auto-mode.
		const survivorNeedsDiscussion = source.match(
			/if\s*\(hasSurvivorBranch\s*&&\s*state\.phase\s*===\s*"needs-discussion"\)\s*\{[^}]*showWorkflowEntry/s,
		);
		assert.ok(
			!!survivorNeedsDiscussion,
			"hasSurvivorBranch && needs-discussion must route to showWorkflowEntry",
		);

		// Verify the handler checks if the discussion succeeded
		const handlerBlock = source.match(
			/if\s*\(hasSurvivorBranch\s*&&\s*state\.phase\s*===\s*"needs-discussion"\)\s*\{([\s\S]*?)\n {4}\}/,
		);
		assert.ok(
			!!handlerBlock,
			"found survivor + needs-discussion handler block",
		);
		if (handlerBlock) {
			assert.ok(
				handlerBlock[1].includes('postState.phase !== "needs-discussion"'),
				"handler must check if phase advanced after discussion",
			);
			assert.ok(
				handlerBlock[1].includes("releaseLockAndReturn"),
				"handler must abort if discussion didn't promote draft",
			);
		}
	});

	test("8. headless bootstrap without artifacts starts a roadmap repair dispatch before aborting", () => {
		const source = readAutoStartSource();
		const repairMessageIdx = source.indexOf(
			"Starting roadmap planning repair session",
		);
		const repairDispatchIdx = source.indexOf(
			"dispatchNewMilestoneDiscuss(ctx, pi, base, nextId",
			repairMessageIdx,
		);
		const finalAbortIdx = source.indexOf(
			"Headless bootstrap repair completed but no milestone artifacts",
			repairDispatchIdx,
		);

		assert.ok(
			repairMessageIdx > -1,
			"bootstrap should announce a roadmap planning repair session when no artifacts are written",
		);
		assert.ok(
			repairDispatchIdx > repairMessageIdx,
			"bootstrap should dispatch a repair turn for the same reserved milestone ID",
		);
		assert.ok(
			finalAbortIdx > repairDispatchIdx,
			"bootstrap should only abort after the repair dispatch also fails to produce artifacts",
		);
	});

	test("9. pre-planning milestone without context is repaired in-place instead of allocating a new ID", () => {
		const source = readAutoStartSource();
		const noContextIdx = source.indexOf(
			// biome-ignore lint/suspicious/noTemplateCurlyInString: intentional literal — searching for template syntax in source code
			"Milestone ${mid} has no context. Bootstrapping from codebase analysis.",
		);
		const noContextBlock = source.slice(
			noContextIdx,
			source.indexOf("// Active milestone has CONTEXT-DRAFT", noContextIdx),
		);

		assert.ok(noContextIdx > -1, "pre-planning no-context branch should exist");
		assert.ok(
			noContextBlock.includes("dispatchNewMilestoneDiscuss(ctx, pi, base, mid"),
			"no-context repair should reuse the existing milestone id",
		);
		assert.ok(
			!noContextBlock.includes("bootstrapNewMilestone(base)"),
			"no-context repair must not allocate a new milestone id",
		);
		assert.ok(
			noContextBlock.includes("Reuse this milestone ID"),
			"repair prompt should explicitly forbid creating a replacement milestone",
		);
	});
});
