// Component coverage for the Phase 30 ship-class calculator: forward
// results, single-target goal-seek wired through a mounted component, the
// Create flow against a real OrderDraftStore, and the planet area. The
// math itself is covered by `calc-model.test.ts` and the Go parity tests;
// here we assert the component renders and orchestrates them.

import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import { fireEvent, render } from "@testing-library/svelte";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";

// The calculator reads `page.params.id` to scope its long-lived state to
// the active game; stub a stable id so the component test has a router.
vi.mock("$app/state", () => ({ page: { params: { id: "calc-test-game" } } }));

import { i18n } from "../src/lib/i18n/index.svelte";
import CalculatorTab from "../src/lib/sidebar/calculator-tab.svelte";
import { calculatorState } from "../src/lib/calculator/calc-state.svelte";
import { CORE_CONTEXT_KEY, CoreHolder } from "../src/lib/core-context.svelte";
import {
	ORDER_DRAFT_CONTEXT_KEY,
	OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import {
	SELECTION_CONTEXT_KEY,
	SelectionStore,
} from "../src/lib/selection.svelte";
import {
	RENDERED_REPORT_CONTEXT_KEY,
	type RenderedReportSource,
} from "../src/lib/rendered-report.svelte";
import type { GameReport, ReportPlanet } from "../src/api/game-state";
import type { Core } from "../src/platform/core/index";
import { makeFakeCore } from "./fake-core";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { IDBPDatabase } from "idb";

const GAME_ID = "11111111-2222-3333-4444-555555555555";

let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
let draft: OrderDraftStore;

const LOCAL_PLANET: ReportPlanet = {
	number: 17,
	name: "Castle",
	x: 100,
	y: 100,
	kind: "local",
	owner: "me",
	size: 1000,
	resources: 10,
	industryStockpile: 0,
	materialsStockpile: 100,
	industry: 1000,
	population: 1000,
	colonists: 0,
	production: "Cruiser",
	freeIndustry: 1000,
};

function makeReport(over: Partial<GameReport> = {}): GameReport {
	return {
		localPlayerDrive: 1.2,
		localPlayerWeapons: 1.5,
		localPlayerShields: 1,
		localPlayerCargo: 1,
		localShipClass: [],
		planets: [],
		...over,
	} as unknown as GameReport;
}

function mount(opts: {
	core?: Core | null;
	report?: GameReport;
	selection?: SelectionStore;
} = {}) {
	const holder = new CoreHolder();
	holder.set(opts.core === undefined ? makeFakeCore() : opts.core);
	const selection = opts.selection ?? new SelectionStore();
	const report = opts.report ?? makeReport();
	const source: RenderedReportSource = {
		get report() {
			return report;
		},
	};
	const context = new Map<unknown, unknown>([
		[RENDERED_REPORT_CONTEXT_KEY, source],
		[ORDER_DRAFT_CONTEXT_KEY, draft],
		[CORE_CONTEXT_KEY, holder],
		[SELECTION_CONTEXT_KEY, selection],
	]);
	return render(CalculatorTab, { context });
}

async function setBlock(
	ui: { getByTestId(id: string): HTMLElement },
	key: string,
	value: number,
): Promise<void> {
	await fireEvent.input(ui.getByTestId(`calculator-block-${key}`), {
		target: { value: String(value) },
	});
}

beforeEach(async () => {
	dbName = `galaxy-calculator-${crypto.randomUUID()}`;
	db = await openGalaxyDB(dbName);
	draft = new OrderDraftStore();
	await draft.init({ cache: new IDBCache(db), gameId: GAME_ID });
	i18n.resetForTests("en");
	// The calculator state is a module singleton shared across cases.
	calculatorState.reset();
});

afterEach(async () => {
	draft.dispose();
	db.close();
	await new Promise<void>((resolve) => {
		const req = indexedDB.deleteDatabase(dbName);
		req.onsuccess = () => resolve();
		req.onerror = () => resolve();
		req.onblocked = () => resolve();
	});
});

describe("calculator-tab", () => {
	test("computes results once the blocks are valid", async () => {
		const ui = mount();
		// All-zero blocks are invalid: results read as unavailable.
		expect(ui.getByTestId("calculator-out-emptyMass")).toHaveTextContent("—");
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		// empty mass = 10 + 5 + 5 = 20.
		expect(ui.getByTestId("calculator-out-emptyMass")).toHaveTextContent("20");
	});

	test("locking attack back-solves the weapons block", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "armament", 2);
		await setBlock(ui, "weapons", 5);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
		await fireEvent.input(ui.getByTestId("calculator-locked-attack"), {
			target: { value: "30" },
		});
		// weapons = 30 / weaponsTech(1.5) = 20, shown read-only.
		const weapons = ui.getByTestId("calculator-block-weapons");
		expect(weapons).toHaveValue(20);
		expect(weapons).toHaveAttribute("readonly");
	});

	test("flags an unreachable speed target as infeasible", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		await fireEvent.click(ui.getByTestId("calculator-lock-speedEmpty"));
		// ceiling is 20 * driveTech(1.2) = 24; 100 is unreachable.
		await fireEvent.input(ui.getByTestId("calculator-locked-speedEmpty"), {
			target: { value: "100" },
		});
		const locked = ui.getByTestId("calculator-locked-speedEmpty");
		expect(locked).toHaveAttribute("title", expect.stringMatching(/cannot be reached/i));
	});

	test("create adds a ship-class command once the name is valid", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		const create = ui.getByTestId("calculator-create");
		expect(create).toBeDisabled();
		await fireEvent.input(ui.getByTestId("calculator-name"), {
			target: { value: "Cruiser" },
		});
		expect(create).not.toBeDisabled();
		await fireEvent.click(create);
		expect(draft.commands).toHaveLength(1);
		expect(draft.commands[0]).toMatchObject({
			kind: "createShipClass",
			name: "Cruiser",
			drive: 10,
			shields: 5,
			cargo: 5,
		});
	});

	test("planet area prompts for a selection when none is active", () => {
		const ui = mount();
		expect(ui.getByTestId("calculator-planet-none")).toBeInTheDocument();
	});

	test("planet area shows build stats for a selected own planet", async () => {
		const selection = new SelectionStore();
		selection.selectPlanet(17);
		const ui = mount({
			report: makeReport({ planets: [LOCAL_PLANET] }),
			selection,
		});
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		expect(ui.getByTestId("calculator-planet-name")).toHaveTextContent("Castle");
		expect(
			ui.getByTestId("calculator-ships-per-turn"),
		).not.toHaveTextContent("—");
	});

	test("zero cargo disables the load toggle", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 0);
		expect(ui.getByTestId("calculator-load-full")).toBeDisabled();
		expect(ui.getByTestId("calculator-load-custom")).toBeDisabled();
	});

	test("full load shows the cargo capacity", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		// A fresh design starts with cargo 0, which pins load to empty;
		// pick full now that there is a hold.
		await fireEvent.click(ui.getByTestId("calculator-load-full"));
		// capacity = cargoTech(1) * (5 + 25/20) = 6.25.
		expect(ui.getByTestId("calculator-full-capacity")).toHaveTextContent("6.25");
	});

	test("flags a custom load above cargo capacity", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		await fireEvent.click(ui.getByTestId("calculator-load-custom"));
		await fireEvent.input(ui.getByTestId("calculator-custom-load"), {
			target: { value: "100" },
		});
		expect(ui.getByTestId("calculator-custom-load")).toHaveAttribute(
			"aria-invalid",
			"true",
		);
	});

	test("marks an invalid block value with aria-invalid", async () => {
		const ui = mount();
		// 0.5 is neither 0 nor ≥ 1.
		await setBlock(ui, "drive", 0.5);
		expect(ui.getByTestId("calculator-block-drive")).toHaveAttribute(
			"aria-invalid",
			"true",
		);
	});

	test("disables the speed lock when drive is zero", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 0);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		expect(ui.getByTestId("calculator-lock-speedEmpty")).toBeDisabled();
	});

	test("displays computed values rounded up to three decimals", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 7);
		await setBlock(ui, "shields", 3);
		await setBlock(ui, "cargo", 1);
		// empty mass = 11; max speed = 11 * driveTech... use a value that is
		// not already 3-decimal: speedEmpty = 20*7*1.2 / 11 = 15.2727…
		// ceil to 3 → 15.273.
		expect(ui.getByTestId("calculator-out-speedEmpty")).toHaveTextContent(
			"15.273",
		);
	});

	test("tech defaults render as a number with an open-lock affordance", () => {
		const ui = mount();
		// Default state: no override → number + open lock, no input.
		expect(ui.getByTestId("calculator-tech-value-drive")).toHaveTextContent(
			"1.2",
		);
		expect(
			ui.getByTestId("calculator-tech-override-drive"),
		).toBeInTheDocument();
		expect(ui.queryByTestId("calculator-tech-drive")).toBeNull();
	});

	test("clicking the open tech lock reveals the input + closed lock", async () => {
		const ui = mount();
		await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
		// Now an input is rendered and the lock turned closed (reset).
		expect(ui.getByTestId("calculator-tech-drive")).toHaveValue(1.2);
		expect(ui.getByTestId("calculator-tech-reset-drive")).toBeInTheDocument();
		expect(ui.queryByTestId("calculator-tech-value-drive")).toBeNull();
	});

	test("flags a tech override below the player's current tech", async () => {
		const ui = mount();
		await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
		// Player drive is 1.2; setting 0.5 is below the floor.
		await fireEvent.input(ui.getByTestId("calculator-tech-drive"), {
			target: { value: "0.5" },
		});
		expect(ui.getByTestId("calculator-tech-drive")).toHaveAttribute(
			"aria-invalid",
			"true",
		);
	});

	test("smart step jumps from 0 to 1 on ArrowUp for ship blocks", async () => {
		const ui = mount();
		const drive = ui.getByTestId("calculator-block-drive") as HTMLInputElement;
		drive.focus();
		await fireEvent.keyDown(drive, { key: "ArrowUp" });
		expect(drive).toHaveValue(1);
		await fireEvent.keyDown(drive, { key: "ArrowUp" });
		expect(drive).toHaveValue(1.1);
		await fireEvent.keyDown(drive, { key: "ArrowDown" });
		expect(drive).toHaveValue(1);
		await fireEvent.keyDown(drive, { key: "ArrowDown" });
		expect(drive).toHaveValue(0);
	});

	test("regression: speed lock works at the ceiling with all-zero non-drive blocks", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 1);
		// Override drive tech to 1 so the ceiling math is plain.
		await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
		await fireEvent.input(ui.getByTestId("calculator-tech-drive"), {
			target: { value: "1" },
		});
		// With D=1, W=A=S=C=0 the only achievable speed is 20*driveTech=20.
		expect(ui.getByTestId("calculator-out-speedEmpty")).toHaveTextContent("20");
		await fireEvent.click(ui.getByTestId("calculator-lock-speedEmpty"));
		const locked = ui.getByTestId("calculator-locked-speedEmpty");
		expect(locked).toHaveValue(20);
		// The lock is feasible — no infeasible title and no red error class.
		expect(locked).not.toHaveAttribute(
			"title",
			expect.stringMatching(/cannot be reached/i),
		);
	});

	test("planet MAT defaults to a value + open lock and opens an input on click", async () => {
		const selection = new SelectionStore();
		selection.selectPlanet(17);
		const ui = mount({
			report: makeReport({ planets: [LOCAL_PLANET] }),
			selection,
		});
		// Initial state: the MAT shows the planet's value via the number cell
		// and an open lock; no input until the override is activated.
		expect(
			ui.getByTestId("calculator-planet-mat-value"),
		).toHaveTextContent("100");
		expect(
			ui.getByTestId("calculator-mat-override"),
		).toBeInTheDocument();
		expect(ui.queryByTestId("calculator-planet-mat")).toBeNull();
		await fireEvent.click(ui.getByTestId("calculator-mat-override"));
		expect(ui.getByTestId("calculator-planet-mat")).toHaveValue(100);
		expect(ui.getByTestId("calculator-mat-reset")).toBeInTheDocument();
	});

	test("flags a modernization target below the player's current tech", async () => {
		const ui = mount();
		await fireEvent.click(ui.getByTestId("calculator-mode-modernization"));
		// Player drive is 1.2; the target is seeded with the same value.
		await fireEvent.input(ui.getByTestId("calculator-target-drive"), {
			target: { value: "0.5" },
		});
		expect(ui.getByTestId("calculator-target-drive")).toHaveAttribute(
			"aria-invalid",
			"true",
		);
	});

	test("armament Arrow keys step the integer block by ±1 (clamped at 0)", async () => {
		const ui = mount();
		const armament = ui.getByTestId(
			"calculator-block-armament",
		) as HTMLInputElement;
		armament.focus();
		await fireEvent.keyDown(armament, { key: "ArrowUp" });
		expect(armament).toHaveValue(1);
		await fireEvent.keyDown(armament, { key: "ArrowUp" });
		expect(armament).toHaveValue(2);
		await fireEvent.keyDown(armament, { key: "ArrowDown" });
		expect(armament).toHaveValue(1);
		await fireEvent.keyDown(armament, { key: "ArrowDown" });
		expect(armament).toHaveValue(0);
		// Clamped at zero — another ArrowDown is a no-op.
		await fireEvent.keyDown(armament, { key: "ArrowDown" });
		expect(armament).toHaveValue(0);
	});

	test("renders unoverridden tech as a 3-decimal label (matches the report)", () => {
		// Player drive tech 1.2 → "1.200" via the shared ceil3 formatter,
		// always padded to three decimals (calculator labels are column-
		// aligned with the report).
		const ui = mount();
		const tech = ui.getByTestId("calculator-tech-value-drive");
		expect((tech.textContent ?? "").trim()).toBe("1.200");
	});

	test("planet MAT label renders through the 3-decimal formatter", () => {
		const selection = new SelectionStore();
		selection.selectPlanet(17);
		const ui = mount({
			report: makeReport({ planets: [LOCAL_PLANET] }),
			selection,
		});
		// Planet MAT is 100 → "100.000" through the shared formatter; the
		// label is monospaced + right-aligned via the existing `.mat-val`
		// rule. Integer MAT pads to three decimals like every other label.
		const mat = ui.getByTestId("calculator-planet-mat-value");
		expect((mat.textContent ?? "").trim()).toBe("100.000");
	});

	test("derived results pad to three decimals (integer empty mass)", async () => {
		// Integer-valued outputs read with the same trailing zeros as
		// fractional ones — column-aligned tabular display.
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		const mass = ui.getByTestId("calculator-out-emptyMass");
		expect((mass.textContent ?? "").trim()).toBe("20.000");
	});

	test("number inputs refuse a fourth decimal as the user types", async () => {
		const selection = new SelectionStore();
		selection.selectPlanet(17);
		const ui = mount({
			report: makeReport({ planets: [LOCAL_PLANET] }),
			selection,
		});
		// MAT input: typed "12.3456" must clamp to "12.345" on input.
		await fireEvent.click(ui.getByTestId("calculator-mat-override"));
		const mat = ui.getByTestId("calculator-planet-mat") as HTMLInputElement;
		await fireEvent.input(mat, { target: { value: "12.3456" } });
		expect(mat.value).toBe("12.345");
		expect(mat.valueAsNumber).toBeCloseTo(12.345, 9);

		// Custom-load input on a ship with a non-zero cargo: typed
		// "1.2345" must clamp to "1.234".
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		await fireEvent.click(ui.getByTestId("calculator-load-custom"));
		const load = ui.getByTestId("calculator-custom-load") as HTMLInputElement;
		await fireEvent.input(load, { target: { value: "1.2345" } });
		expect(load.value).toBe("1.234");
	});

	test("tech and target-tech inputs cap at three decimals", async () => {
		const ui = mount();
		// Tech override input.
		await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
		const tech = ui.getByTestId("calculator-tech-drive") as HTMLInputElement;
		await fireEvent.input(tech, { target: { value: "2.9999" } });
		expect(tech.value).toBe("2.999");

		// Modernization target tech input.
		await fireEvent.click(ui.getByTestId("calculator-mode-modernization"));
		const target = ui.getByTestId(
			"calculator-target-drive",
		) as HTMLInputElement;
		await fireEvent.input(target, { target: { value: "3.1416" } });
		expect(target.value).toBe("3.141");
	});

	test("lock value input caps at three decimals", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
		const lock = ui.getByTestId(
			"calculator-locked-attack",
		) as HTMLInputElement;
		await fireEvent.input(lock, { target: { value: "0.1234" } });
		expect(lock.value).toBe("0.123");
	});

	test("ship-block input caps at three decimals", async () => {
		const ui = mount();
		const drive = ui.getByTestId("calculator-block-drive") as HTMLInputElement;
		await fireEvent.input(drive, { target: { value: "1.2345" } });
		expect(drive.value).toBe("1.234");
	});

	test("lock spinner step is replaced by ArrowUp/ArrowDown (±0.001)", async () => {
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
		const locked = ui.getByTestId(
			"calculator-locked-attack",
		) as HTMLInputElement;
		// Lock value is seeded from outputs.attack (0 with no weapons).
		const start = Number(locked.value);
		locked.focus();
		await fireEvent.keyDown(locked, { key: "ArrowUp" });
		expect(Number(locked.value)).toBeCloseTo(start + 0.001, 9);
		await fireEvent.keyDown(locked, { key: "ArrowDown" });
		expect(Number(locked.value)).toBeCloseTo(start, 9);
	});

	test("flags the lock as infeasible when the back-solved block falls in (0, 1)", async () => {
		// attack lock → weapons = targetAttack / weaponsTech. weaponsTech
		// is 1.5; a target of 0.5 would force weapons = 0.333… which
		// fails the DWSC rule (must be 0 or ≥ 1).
		const ui = mount();
		await setBlock(ui, "drive", 10);
		await setBlock(ui, "armament", 2);
		await setBlock(ui, "weapons", 5);
		await setBlock(ui, "shields", 5);
		await setBlock(ui, "cargo", 5);
		await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
		await fireEvent.input(ui.getByTestId("calculator-locked-attack"), {
			target: { value: "0.5" },
		});
		const locked = ui.getByTestId("calculator-locked-attack");
		expect(locked).toHaveAttribute(
			"title",
			expect.stringMatching(/cannot be reached/i),
		);
		// The claimed block is not back-solved into the invalid (0, 1)
		// range — the weapons input keeps the user's typed value (5).
		expect(ui.getByTestId("calculator-block-weapons")).toHaveValue(5);
	});

	test("dropdown selection loads the class immediately (no blur needed)", async () => {
		const ui = mount({
			report: makeReport({
				localShipClass: [
					{
						name: "Scout",
						drive: 3,
						armament: 0,
						weapons: 0,
						shields: 2,
						cargo: 1,
					},
				],
			} as unknown as GameReport),
		});
		// A datalist option click sets the whole value at once — Firefox
		// reports no `inputType`, Chromium reports "insertReplacementText".
		// Simulate the latter; the calculator should load before any
		// `change` event.
		await fireEvent.input(ui.getByTestId("calculator-name"), {
			target: { value: "Scout" },
			inputType: "insertReplacementText",
		});
		expect(ui.getByTestId("calculator-block-drive")).toHaveValue(3);
		expect(ui.getByTestId("calculator-block-shields")).toHaveValue(2);
	});

	test("dropdown selection asks before discarding manual edits", async () => {
		const ui = mount({
			report: makeReport({
				localShipClass: [
					{
						name: "Scout",
						drive: 3,
						armament: 0,
						weapons: 0,
						shields: 2,
						cargo: 1,
					},
				],
			} as unknown as GameReport),
		});
		// The user has hand-edited the design.
		await setBlock(ui, "drive", 7);
		const confirm = vi.spyOn(window, "confirm").mockReturnValue(false);
		await fireEvent.input(ui.getByTestId("calculator-name"), {
			target: { value: "Scout" },
			inputType: "insertReplacementText",
		});
		expect(confirm).toHaveBeenCalledTimes(1);
		// The user said no — the manual edits stay.
		expect(ui.getByTestId("calculator-block-drive")).toHaveValue(7);
		// The name field is reverted to the previously loaded class (or
		// empty), so the field does not pretend the load happened.
		expect(ui.getByTestId("calculator-name")).toHaveValue("");

		confirm.mockReturnValue(true);
		await fireEvent.input(ui.getByTestId("calculator-name"), {
			target: { value: "Scout" },
			inputType: "insertReplacementText",
		});
		// Confirmed — the class is now loaded.
		expect(ui.getByTestId("calculator-block-drive")).toHaveValue(3);
		confirm.mockRestore();
	});

	test("dropdown selection loads silently when the design is clean", async () => {
		const ui = mount({
			report: makeReport({
				localShipClass: [
					{
						name: "Scout",
						drive: 3,
						armament: 0,
						weapons: 0,
						shields: 2,
						cargo: 1,
					},
				],
			} as unknown as GameReport),
		});
		const confirm = vi.spyOn(window, "confirm");
		await fireEvent.input(ui.getByTestId("calculator-name"), {
			target: { value: "Scout" },
			inputType: "insertReplacementText",
		});
		expect(confirm).not.toHaveBeenCalled();
		expect(ui.getByTestId("calculator-block-drive")).toHaveValue(3);
		confirm.mockRestore();
	});

	test("does not render a delete-class button after loading a class", async () => {
		const ui = mount({
			report: makeReport({
				localShipClass: [
					{
						name: "Scout",
						drive: 3,
						armament: 0,
						weapons: 0,
						shields: 2,
						cargo: 1,
					},
				],
			} as unknown as GameReport),
		});
		await fireEvent.input(ui.getByTestId("calculator-name"), {
			target: { value: "Scout" },
			inputType: "insertReplacementText",
		});
		// The loaded class state used to render a `delete <name>` button;
		// the calculator no longer owns delete-class — issue #53 will.
		expect(ui.queryByTestId("calculator-delete")).toBeNull();
	});

	test("bombing and cargo-capacity rows reserve the lock slot for column alignment", () => {
		const ui = mount();
		for (const id of ["calculator-out-bombing", "calculator-out-cargo-capacity"]) {
			const cell = ui.getByTestId(id).parentElement;
			expect(cell).not.toBeNull();
			// A hidden placeholder occupies the same width as the lock button
			// on the mass/speed/attack/defence rows, so the value column does
			// not drift right on the rows without a lock.
			expect(cell!.querySelector(".lock-slot")).not.toBeNull();
		}
	});
});
