/**
 * M010/S05 test — inline routing behavioral contract.
 *
 * Covers the routing condition in runUnit() for inline-eligible unit types:
 *   - Inline fires by default when SF_INLINE_DISPATCH is absent/unset
 *   - Inline is suppressed when SF_INLINE_DISPATCH=0
 *   - Inline fires when SF_INLINE_DISPATCH=1 (backward compat)
 *   - Inline is suppressed for unit types not in INLINE_ELIGIBLE_UNITS
 *
 * Routing condition: isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0"
 *
 * Default-on: inline fires when env var is absent, "1", or any value other than "0".
 * SF_INLINE_DISPATCH=0 is the explicit off-switch.
 *
 * These tests target the src build so they run against the current source
 * routing condition in runUnit() at src/resources/extensions/sf/auto/run-unit.js.
 */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
	INLINE_ELIGIBLE_UNITS,
	isInlineEligible,
	runUnitInline,
} from "../dispatch/run-unit-inline.js";

/**
 * Snapshot the current SF_INLINE_DISPATCH value and delete it so the
 * environment reads as "absent". Returns the saved value (or undefined).
 */
function isolateEnv() {
	const saved = process.env.SF_INLINE_DISPATCH;
	delete process.env.SF_INLINE_DISPATCH;
	return saved;
}

/**
 * Restore SF_INLINE_DISPATCH to a specific value (including undefined).
 */
function restoreEnv(value) {
	if (value === undefined) {
		delete process.env.SF_INLINE_DISPATCH;
	} else {
		process.env.SF_INLINE_DISPATCH = value;
	}
}

describe("M010/S05 — routing condition contract", () => {
	afterEach(() => {
		// Always leave SF_INLINE_DISPATCH undefined between tests so
		// one test's mutation cannot bleed into the next.
		delete process.env.SF_INLINE_DISPATCH;
	});

	it("isInlineEligible returns true for all INLINE_ELIGIBLE_UNITS members", () => {
		for (const unitType of INLINE_ELIGIBLE_UNITS) {
			expect(isInlineEligible(unitType)).toBe(true);
		}
	});

	it("isInlineEligible returns false for non-members regardless of env", () => {
		// Absent
		delete process.env.SF_INLINE_DISPATCH;
		expect(isInlineEligible("execute-task")).toBe(false);

		// Explicitly enabled — should still be false for non-members
		process.env.SF_INLINE_DISPATCH = "1";
		expect(isInlineEligible("execute-task")).toBe(false);

		// Explicitly disabled — should still be false for non-members
		process.env.SF_INLINE_DISPATCH = "0";
		expect(isInlineEligible("execute-task")).toBe(false);
	});

	describe("routing condition: SF_INLINE_DISPATCH absent (default-on)", () => {
		it("inline SHOULD fire for validate-milestone when SF_INLINE_DISPATCH is absent (default-on)", () => {
			const saved = isolateEnv();
			try {
				// Routing condition: isInlineEligible(unitType) && SF_INLINE_DISPATCH !== "0"
				// Default-on: absent/unset means !== "0" is true → inline fires.
				const unitType = "validate-milestone";
				expect(isInlineEligible(unitType)).toBe(true);
				const inlineWouldFire =
					isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
				expect(inlineWouldFire).toBe(true);
			} finally {
				restoreEnv(saved);
			}
		});

		it("inline SHOULD fire for complete-milestone when SF_INLINE_DISPATCH is absent (default-on)", () => {
			const saved = isolateEnv();
			try {
				const unitType = "complete-milestone";
				expect(isInlineEligible(unitType)).toBe(true);
				const inlineWouldFire =
					isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
				expect(inlineWouldFire).toBe(true);
			} finally {
				restoreEnv(saved);
			}
		});

		it("inline SHOULD fire for reassess-roadmap when SF_INLINE_DISPATCH is absent (default-on)", () => {
			const saved = isolateEnv();
			try {
				const unitType = "reassess-roadmap";
				expect(isInlineEligible(unitType)).toBe(true);
				const inlineWouldFire =
					isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
				expect(inlineWouldFire).toBe(true);
			} finally {
				restoreEnv(saved);
			}
		});
	});

	describe("routing condition: SF_INLINE_DISPATCH=0 (explicit off-switch)", () => {
		it("inline should NOT fire for validate-milestone when SF_INLINE_DISPATCH=0", () => {
			process.env.SF_INLINE_DISPATCH = "0";
			const unitType = "validate-milestone";
			expect(isInlineEligible(unitType)).toBe(true);
			const inlineWouldFire =
				isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
			expect(inlineWouldFire).toBe(false);
		});

		it("inline should NOT fire for complete-milestone when SF_INLINE_DISPATCH=0", () => {
			process.env.SF_INLINE_DISPATCH = "0";
			const unitType = "complete-milestone";
			expect(isInlineEligible(unitType)).toBe(true);
			const inlineWouldFire =
				isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
			expect(inlineWouldFire).toBe(false);
		});

		it("inline should NOT fire for reassess-roadmap when SF_INLINE_DISPATCH=0", () => {
			process.env.SF_INLINE_DISPATCH = "0";
			const unitType = "reassess-roadmap";
			expect(isInlineEligible(unitType)).toBe(true);
			const inlineWouldFire =
				isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
			expect(inlineWouldFire).toBe(false);
		});
	});

	describe("routing condition: SF_INLINE_DISPATCH=1 (backward compat)", () => {
		it("inline SHOULD fire for inline-eligible types when SF_INLINE_DISPATCH=1", () => {
			process.env.SF_INLINE_DISPATCH = "1";
			const unitType = "validate-milestone";
			expect(isInlineEligible(unitType)).toBe(true);
			const inlineWouldFire =
				isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
			expect(inlineWouldFire).toBe(true);
		});

		it("inline SHOULD still NOT fire for non-members when SF_INLINE_DISPATCH=1", () => {
			process.env.SF_INLINE_DISPATCH = "1";
			const unitType = "execute-task";
			expect(isInlineEligible(unitType)).toBe(false);
			const inlineWouldFire =
				isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
			expect(inlineWouldFire).toBe(false);
		});

		it("inline SHOULD fire when SF_INLINE_DISPATCH is set to any value other than '0'", () => {
			for (const val of ["true", "yes", "enable", "2", "on"]) {
				process.env.SF_INLINE_DISPATCH = val;
				const unitType = "validate-milestone";
				expect(isInlineEligible(unitType)).toBe(true);
				const inlineWouldFire =
					isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
				expect(inlineWouldFire).toBe(true);
			}
		});
	});

	describe("routing condition: non-member unit types always route inline=NO", () => {
		it.each([
			"execute-task",
			"plan-slice",
			"discuss-milestone",
			"research-slice",
			"plan-milestone",
			"complete-slice",
			"complete-task",
		])("isInlineEligible('%s') returns false", (unitType) => {
			expect(isInlineEligible(unitType)).toBe(false);
		});
	});
});

describe("M010/S01 — runUnitInline scaffold", () => {
	it("INLINE_ELIGIBLE_UNITS is the source of truth", () => {
		expect(INLINE_ELIGIBLE_UNITS).toBeInstanceOf(Set);
		// Pin the curated initial set per S01 scope.
		const expected = ["validate-milestone", "complete-milestone", "reassess-roadmap"];
		expect([...INLINE_ELIGIBLE_UNITS].sort()).toEqual([...expected].sort());
	});

	it("isInlineEligible reflects the set", () => {
		for (const unitType of INLINE_ELIGIBLE_UNITS) {
			expect(isInlineEligible(unitType)).toBe(true);
		}
		// Negative cases: any unit type not in the set returns false.
		expect(isInlineEligible("execute-task")).toBe(false);
		expect(isInlineEligible("plan-slice")).toBe(false);
		expect(isInlineEligible("discuss-milestone")).toBe(false);
		expect(isInlineEligible("research-slice")).toBe(false);
		expect(isInlineEligible("")).toBe(false);
	});

	it("runUnitInline rejects ineligible unit types loudly (exitCode 2)", async () => {
		const result = await runUnitInline("execute-task", "M999/S01/T01", {
			basePath: "/tmp/sf-inline-test-doesnt-need-to-exist",
		});
		expect(result.ok).toBe(false);
		expect(result.exitCode).toBe(2);
		expect(result.stderr).toBeTruthy();
		expect(result.stderr).toMatch(/not yet eligible for inline dispatch/i);
		expect(result.stderr).toContain("validate-milestone");
		expect(result.stderr).toContain("complete-milestone");
		expect(result.stderr).toContain("reassess-roadmap");
	});

	it("runUnitInline rejects empty unit type", async () => {
		const result = await runUnitInline("", "M001", {
			basePath: "/tmp",
		});
		expect(result.ok).toBe(false);
		expect(result.exitCode).toBe(2);
	});

	it("API surface: runUnitInline returns a structured result shape", async () => {
		const result = await runUnitInline("execute-task", "M999", {
			basePath: "/tmp",
		});
		// Even on failure, the result must have the four documented fields.
		expect(result).toHaveProperty("ok");
		expect(result).toHaveProperty("output");
		expect(result).toHaveProperty("exitCode");
		// stderr is optional on success but present on failure.
		expect(result).toHaveProperty("stderr");
		expect(typeof result.ok).toBe("boolean");
		expect(typeof result.output).toBe("string");
		expect(typeof result.exitCode).toBe("number");
	});
});

describe("M010/S05 — runUnit() routing condition via isInlineEligible", () => {
	/**
	 * Routing condition mirror: computes exactly what runUnit()'s if-statement
	 * would compute without calling runUnit() itself. This pins the routing
	 * contract in isolation, consistent with how the rest of the test file
	 * uses isInlineEligible directly.
	 */
	function wouldInlineFire(unitType) {
		return isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0";
	}

	describe("default-on: SF_INLINE_DISPATCH absent", () => {
		afterEach(() => {
			delete process.env.SF_INLINE_DISPATCH;
		});

		it("inline fires for validate-milestone (S05 default-on contract)", () => {
			delete process.env.SF_INLINE_DISPATCH;
			expect(wouldInlineFire("validate-milestone")).toBe(true);
		});

		it("inline fires for complete-milestone (S05 default-on contract)", () => {
			delete process.env.SF_INLINE_DISPATCH;
			expect(wouldInlineFire("complete-milestone")).toBe(true);
		});

		it("inline fires for reassess-roadmap (S05 default-on contract)", () => {
			delete process.env.SF_INLINE_DISPATCH;
			expect(wouldInlineFire("reassess-roadmap")).toBe(true);
		});

		it("inline does NOT fire for execute-task (not inline-eligible)", () => {
			delete process.env.SF_INLINE_DISPATCH;
			expect(wouldInlineFire("execute-task")).toBe(false);
		});

		it("inline does NOT fire for plan-slice (not inline-eligible)", () => {
			delete process.env.SF_INLINE_DISPATCH;
			expect(wouldInlineFire("plan-slice")).toBe(false);
		});
	});

	describe("off-switch: SF_INLINE_DISPATCH=0", () => {
		afterEach(() => {
			delete process.env.SF_INLINE_DISPATCH;
		});

		it("inline is suppressed for validate-milestone when SF_INLINE_DISPATCH=0", () => {
			process.env.SF_INLINE_DISPATCH = "0";
			expect(wouldInlineFire("validate-milestone")).toBe(false);
		});

		it("inline is suppressed for complete-milestone when SF_INLINE_DISPATCH=0", () => {
			process.env.SF_INLINE_DISPATCH = "0";
			expect(wouldInlineFire("complete-milestone")).toBe(false);
		});

		it("inline is suppressed for reassess-roadmap when SF_INLINE_DISPATCH=0", () => {
			process.env.SF_INLINE_DISPATCH = "0";
			expect(wouldInlineFire("reassess-roadmap")).toBe(false);
		});
	});

	describe("backward compat: SF_INLINE_DISPATCH=1", () => {
		afterEach(() => {
			delete process.env.SF_INLINE_DISPATCH;
		});

		it("inline fires for validate-milestone when SF_INLINE_DISPATCH=1", () => {
			process.env.SF_INLINE_DISPATCH = "1";
			expect(wouldInlineFire("validate-milestone")).toBe(true);
		});

		it("non-inline-eligible types stay off even with SF_INLINE_DISPATCH=1", () => {
			process.env.SF_INLINE_DISPATCH = "1";
			expect(wouldInlineFire("execute-task")).toBe(false);
			expect(wouldInlineFire("plan-slice")).toBe(false);
		});
	});

	describe("any truthy non-'0' value fires inline", () => {
		afterEach(() => {
			delete process.env.SF_INLINE_DISPATCH;
		});

		it.each(["true", "yes", "on", "enable", "2"])(
			"SF_INLINE_DISPATCH='%s' fires inline for validate-milestone",
			(val) => {
				process.env.SF_INLINE_DISPATCH = val;
				expect(wouldInlineFire("validate-milestone")).toBe(true);
			},
		);
	});
});
