/**
 * R090/S02 — Two-lane dispatcher integration tests.
 *
 * Tests the lane-aware dispatch selection logic that is used by loop.js.
 * The dispatch sink is mocked — these tests verify SELECTION decisions, not
 * unit execution.
 *
 * Scenarios:
 *   1. lanes ON, cap 1/1: plan-slice-A + execute-task-B dispatched in same tick
 *   2. saturated execute slot does not block subsequent planning dispatch
 *   3. SF_LANES=0 → only one dispatch per tick (back-compat)
 *   4. planningCap=2 → two planning units can dispatch in one tick
 *   5. execute-only queue: only execute units are selected
 *   6. mixed queue exhausts both caps before end of list
 */
import { describe, expect, it } from "vitest";
import {
	areLanesEnabled,
	getExecuteLaneCapacity,
	getPlanningLaneCapacity,
	selectLaneAwareUnits,
} from "../auto/unit-lanes.js";

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

/**
 * Simulate a dispatch tick over a candidate list.
 *
 * When lanes are enabled: selects up to planCap planning + execCap execute units.
 * When lanes are disabled: selects only the first candidate (back-compat).
 *
 * Returns the list of dispatched units (just their unitType/unitId).
 *
 * @param {Array<{unitType: string, unitId: string}>} candidates
 * @param {NodeJS.ProcessEnv} env
 */
function simulateTick(candidates, env = {}) {
	if (!areLanesEnabled(env)) {
		// Back-compat: one-at-a-time
		return candidates.length > 0 ? [candidates[0]] : [];
	}
	const planCap = getPlanningLaneCapacity(env);
	const execCap = getExecuteLaneCapacity(env);
	return selectLaneAwareUnits(candidates, planCap, execCap);
}

// ── Test data ─────────────────────────────────────────────────────────────────

const MIXED_QUEUE = [
	{ unitType: "plan-slice", unitId: "plan-slice-A" },
	{ unitType: "execute-task", unitId: "execute-task-B" },
	{ unitType: "plan-slice", unitId: "plan-slice-C" },
	{ unitType: "execute-task", unitId: "execute-task-D" },
	{ unitType: "plan-slice", unitId: "execute-slice-E" },
];

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

describe("R090/S02 — two-lane dispatcher", () => {
	it("dispatches plan-slice-A AND execute-task-B in the same tick when lanes ON (cap 1/1)", () => {
		const dispatched = simulateTick(MIXED_QUEUE, {}); // default: lanes ON, cap 1/1
		expect(dispatched).toHaveLength(2);
		expect(dispatched[0].unitId).toBe("plan-slice-A");
		expect(dispatched[1].unitId).toBe("execute-task-B");
	});

	it("saturated execute slot does NOT block subsequent planning dispatch", () => {
		// Execute cap = 0 → only planning units should be selected,
		// and the scan must continue past execute candidates to find them.
		const env = { SF_LANES_EXECUTE_N: "0" };
		// We call selectLaneAwareUnits directly here to sidestep the env parse
		// (getExecuteLaneCapacity returns default 1 for "0"). Use cap=0 directly.
		const selected = selectLaneAwareUnits(MIXED_QUEUE, 1, 0);
		expect(selected).toHaveLength(1);
		expect(selected[0].unitType).toBe("plan-slice");
		// Scan must have continued past execute-task-B to find plan-slice-A
		expect(selected[0].unitId).toBe("plan-slice-A");
	});

	it("saturated planning slot does NOT block subsequent execute dispatch", () => {
		// Planning cap = 0 → only execute units should be selected
		const selected = selectLaneAwareUnits(MIXED_QUEUE, 0, 1);
		expect(selected).toHaveLength(1);
		expect(selected[0].unitType).toBe("execute-task");
		expect(selected[0].unitId).toBe("execute-task-B");
	});

	it("SF_LANES=0 → only one dispatch per tick (back-compat one-at-a-time)", () => {
		const dispatched = simulateTick(MIXED_QUEUE, { SF_LANES: "0" });
		expect(dispatched).toHaveLength(1);
		expect(dispatched[0].unitId).toBe("plan-slice-A");
	});

	it("SF_LANES=false → only one dispatch per tick (back-compat)", () => {
		const dispatched = simulateTick(MIXED_QUEUE, { SF_LANES: "false" });
		expect(dispatched).toHaveLength(1);
		expect(dispatched[0].unitId).toBe("plan-slice-A");
	});

	it("planningCap=2 → two planning units can dispatch in one tick", () => {
		const selected = selectLaneAwareUnits(MIXED_QUEUE, 2, 1);
		// Expected: plan-slice-A (planning), execute-task-B (execute), plan-slice-C (planning)
		expect(selected).toHaveLength(3);
		const types = selected.map((u) => u.unitType);
		expect(types.filter((t) => t === "plan-slice")).toHaveLength(2);
		expect(types.filter((t) => t === "execute-task")).toHaveLength(1);
	});

	it("execute-only queue: selects only execute units", () => {
		const execQueue = [
			{ unitType: "execute-task", unitId: "T1" },
			{ unitType: "complete-slice", unitId: "S1" },
			{ unitType: "execute-task", unitId: "T2" },
		];
		const selected = selectLaneAwareUnits(execQueue, 1, 1);
		expect(selected).toHaveLength(1); // only 1 execute cap, no planning found
		expect(selected[0].unitType).toBe("execute-task");
	});

	it("planning-only queue: selects only planning units", () => {
		const planQueue = [
			{ unitType: "plan-slice", unitId: "P1" },
			{ unitType: "research-slice", unitId: "R1" },
		];
		const selected = selectLaneAwareUnits(planQueue, 1, 1);
		expect(selected).toHaveLength(1); // only 1 planning cap, no execute found
		expect(selected[0].unitId).toBe("P1");
	});

	it("mixed queue exhausts both caps without overselecting", () => {
		const dispatched = simulateTick(MIXED_QUEUE, {}); // lanes ON, cap 1/1
		// Must not dispatch more than cap allows
		const planningCount = dispatched.filter(
			(u) => u.unitType === "plan-slice",
		).length;
		const executeCount = dispatched.filter(
			(u) => u.unitType === "execute-task",
		).length;
		expect(planningCount).toBeLessThanOrEqual(1);
		expect(executeCount).toBeLessThanOrEqual(1);
	});

	it("areLanesEnabled returns true by default", () => {
		expect(areLanesEnabled({})).toBe(true);
	});

	it("areLanesEnabled returns false for SF_LANES=0", () => {
		expect(areLanesEnabled({ SF_LANES: "0" })).toBe(false);
	});
});
