import { describe, expect, test, vi } from "vitest";

import {
	computeCalculator,
	computePlanetBuild,
	type CalculatorInput,
} from "../src/lib/calculator/calc-model";
import { makeFakeCore } from "./fake-core";

function input(overrides: Partial<CalculatorInput> = {}): CalculatorInput {
	return {
		blocks: { drive: 10, armament: 0, weapons: 0, shields: 5, cargo: 5 },
		driveTech: 1.2,
		weaponsTech: 1.5,
		shieldsTech: 1,
		cargoTech: 1,
		loadMode: "full",
		customLoad: 0,
		lock: null,
		...overrides,
	};
}

describe("computeCalculator forward", () => {
	test("returns null outputs without a Core", () => {
		const result = computeCalculator(input(), null);
		expect(result.outputs).toBeNull();
		expect(result.valuesValid).toBe(false);
	});

	test("computes outputs for a valid design", () => {
		const core = makeFakeCore();
		const result = computeCalculator(input(), core);
		expect(result.valuesValid).toBe(true);
		expect(result.outputs).not.toBeNull();
		// empty mass = drive + shields + cargo = 20 (no weapons block).
		expect(result.outputs?.emptyMass).toBeCloseTo(20, 9);
		// cargo capacity = 1 * (5 + 25/20) = 6.25, full load mass = 26.25.
		expect(result.load).toBeCloseTo(6.25, 9);
		expect(result.outputs?.loadedMass).toBeCloseTo(26.25, 9);
	});

	test("hides outputs when blocks are invalid (armament without weapons)", () => {
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ blocks: { drive: 10, armament: 3, weapons: 0, shields: 5, cargo: 5 } }),
			core,
		);
		expect(result.valuesValid).toBe(false);
		expect(result.valueReason).toBe("armament_weapons_pair");
		expect(result.outputs).toBeNull();
	});

	test("empty load mode yields loaded mass equal to empty mass", () => {
		const core = makeFakeCore();
		const result = computeCalculator(input({ loadMode: "empty" }), core);
		expect(result.load).toBe(0);
		expect(result.outputs?.loadedMass).toBeCloseTo(
			result.outputs?.emptyMass ?? -1,
			9,
		);
	});
});

describe("computeCalculator goal-seek", () => {
	test("attack lock back-solves the weapons block", () => {
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ blocks: { drive: 10, armament: 2, weapons: 5, shields: 5, cargo: 5 }, lock: { output: "attack", value: 30 } }),
			core,
		);
		expect(result.lockFeasible).toBe(true);
		expect(result.computedInput).toBe("weapons");
		// weapons = 30 / weaponsTech(1.5) = 20.
		expect(result.blocks.weapons).toBeCloseTo(20, 9);
		expect(result.outputs?.attack).toBeCloseTo(30, 6);
	});

	test("loaded-speed lock back-solves the drive block", () => {
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ lock: { output: "speedLoaded", value: 5 } }),
			core,
		);
		expect(result.lockFeasible).toBe(true);
		expect(result.computedInput).toBe("drive");
		expect(result.outputs?.speedLoaded).toBeCloseTo(5, 6);
	});

	test("defence lock back-solves the shields block", () => {
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ lock: { output: "defense", value: 4 } }),
			core,
		);
		expect(result.lockFeasible).toBe(true);
		expect(result.computedInput).toBe("shields");
		expect(result.outputs?.defense).toBeCloseTo(4, 5);
	});

	test("empty-mass lock back-solves the cargo block", () => {
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ lock: { output: "emptyMass", value: 25 } }),
			core,
		);
		expect(result.computedInput).toBe("cargo");
		// cargo = 25 - (drive 10 + shields 5) = 10.
		expect(result.blocks.cargo).toBeCloseTo(10, 9);
		expect(result.outputs?.emptyMass).toBeCloseTo(25, 9);
	});

	test("loaded-mass lock back-solves the cargo load", () => {
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ lock: { output: "loadedMass", value: 30 } }),
			core,
		);
		expect(result.computedInput).toBe("load");
		// load = (30 - emptyMass 20) * cargoTech 1 = 10.
		expect(result.load).toBeCloseTo(10, 9);
		expect(result.outputs?.loadedMass).toBeCloseTo(30, 9);
	});

	test("an unreachable speed marks the lock infeasible", () => {
		const core = makeFakeCore();
		const result = computeCalculator(
			// ceiling is 20 * driveTech = 24; 100 is unreachable.
			input({ lock: { output: "speedEmpty", value: 100 } }),
			core,
		);
		expect(result.lockFeasible).toBe(false);
		expect(result.computedInput).toBeNull();
		// the claimed block keeps its raw value.
		expect(result.blocks.drive).toBe(10);
	});

	test("an attack target that back-solves to a (0, 1) weapons block is infeasible", () => {
		// weapons = targetAttack / weaponsTech; weaponsTech=1.5, a 0.5
		// target → weapons = 0.333…, which fails the DWSC rule (must be
		// 0 or ≥ 1). The lock is flagged infeasible so the UI shows the
		// red border, and the claimed block is left at its raw value so
		// the design preview keeps reading off the user's own design.
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ lock: { output: "attack", value: 0.5 } }),
			core,
		);
		expect(result.lockFeasible).toBe(false);
		expect(result.computedInput).toBeNull();
		// The claimed block stays at its raw value.
		expect(result.blocks.weapons).toBe(0);
	});

	test("an empty-mass target that back-solves to a (0, 1) cargo block is infeasible", () => {
		// emptyMass = drive + shields + cargo; with drive=10 shields=5,
		// rest excluding cargo = 15. Target 15.5 → cargo = 0.5, in the
		// invalid gap, so the lock is flagged.
		const core = makeFakeCore();
		const result = computeCalculator(
			input({ lock: { output: "emptyMass", value: 15.5 } }),
			core,
		);
		expect(result.lockFeasible).toBe(false);
		expect(result.computedInput).toBeNull();
		expect(result.blocks.cargo).toBe(5);
	});

	test("speed lock is feasible at the ceiling when rest mass is zero", () => {
		// Regression for the D=1, W=A=S=C=0 case: every block except
		// drive is zero, so speed equals 20*driveTech (the ceiling); the
		// solver must accept that exact target instead of flagging it
		// as unreachable.
		const core = makeFakeCore();
		const result = computeCalculator(
			input({
				blocks: { drive: 1, armament: 0, weapons: 0, shields: 0, cargo: 0 },
				driveTech: 1,
				lock: { output: "speedEmpty", value: 20 },
			}),
			core,
		);
		expect(result.lockFeasible).toBe(true);
		expect(result.computedInput).toBe("drive");
		expect(result.outputs?.speedEmpty).toBeCloseTo(20, 9);
	});

	test("calls the matching solver with the right context", () => {
		const weaponsForAttack = vi.fn(() => 7);
		const core = makeFakeCore({ weaponsForAttack });
		const result = computeCalculator(
			input({ blocks: { drive: 10, armament: 2, weapons: 5, shields: 5, cargo: 5 }, lock: { output: "attack", value: 30 } }),
			core,
		);
		expect(weaponsForAttack).toHaveBeenCalledWith({
			targetAttack: 30,
			weaponsTech: 1.5,
		});
		expect(result.blocks.weapons).toBe(7);
		expect(result.computedInput).toBe("weapons");
	});
});

describe("computePlanetBuild", () => {
	test("returns null without a Core", () => {
		expect(computePlanetBuild({ shipMass: 10, freeIndustry: 100, material: 0, resources: 10 }, null)).toBeNull();
	});

	test("derives ships-per-turn from the per-turn build loop", () => {
		const core = makeFakeCore();
		// shipMass 1, ample material: 100 production / (10 per ship) = 10 ships.
		const result = computePlanetBuild(
			{ shipMass: 1, freeIndustry: 100, material: 100, resources: 10 },
			core,
		);
		expect(result?.wholeShips).toBe(10);
		expect(result?.shipsPerTurn).toBeCloseTo(10, 9);
		expect(result?.turnsPerShip).toBeCloseTo(0.1, 9);
	});

	test("reports turns-per-ship when under one ship per turn", () => {
		const core = makeFakeCore();
		// shipMass 10, no material, resources 0.5: cost 120, 60 production → 0.5 ship.
		const result = computePlanetBuild(
			{ shipMass: 10, freeIndustry: 60, material: 0, resources: 0.5 },
			core,
		);
		expect(result?.wholeShips).toBe(0);
		expect(result?.shipsPerTurn).toBeCloseTo(0.5, 9);
		expect(result?.turnsPerShip).toBeCloseTo(2, 9);
	});
});
