import { describe, expect, it } from "vitest";
import {
	DAMAGE_MULTIPLIER_PARAM_KEY,
	INCOMING_DAMAGE_MULTIPLIER_PARAM_KEY,
	SPECIAL_POINTS_PARAM_KEY,
} from "../weapon-params-constants";
import type {
	DamageMultiplierWithHistory,
	ParsedWeaponParams,
} from "../weapon-params-types";
import { classifyParamChange } from "./param-directions";
import * as WeaponParams from "./WeaponParams";

const VERSIONS = ["1.0.0", "2.0.0", "3.0.0"];

const emptyParsed = (weaponId: number): ParsedWeaponParams => ({
	weaponId,
	categories: {},
});

const row = (overrides: {
	mainWeaponIds?: number[];
	subWeaponIds?: number[];
	specialWeaponIds?: number[];
	targets: DamageMultiplierWithHistory[];
}) => ({
	mainWeaponIds: [],
	subWeaponIds: [],
	specialWeaponIds: [],
	...overrides,
});

describe("damageMultipliersForWeapon", () => {
	it("collects only rows applying to the weapon for the given kind", () => {
		const rows = {
			a: row({
				specialWeaponIds: [11],
				targets: [{ target: "Chariot", current: 2, history: [] }],
			}),
			b: row({
				specialWeaponIds: [12],
				targets: [{ target: "ShockSonar", current: 2, history: [] }],
			}),
		};

		const result = WeaponParams.damageMultipliersForWeapon(rows, 11, "special");

		expect(result.map((m) => m.target)).toEqual(["Chariot"]);
	});

	it("de-duplicates identical target histories shared across rows", () => {
		const sharedTarget: DamageMultiplierWithHistory = {
			target: "GreatBarrier_Barrier",
			current: 1.4,
			history: [{ version: "1.0.0", value: 2.8 }],
		};
		const rows = {
			bullet: row({ specialWeaponIds: [10], targets: [sharedTarget] }),
			bombCore: row({
				specialWeaponIds: [10],
				targets: [{ ...sharedTarget }],
			}),
		};

		const result = WeaponParams.damageMultipliersForWeapon(rows, 10, "special");

		expect(result).toHaveLength(1);
	});

	it("merges several rows of the same target into the most informative entry", () => {
		const rows = {
			swing: row({
				specialWeaponIds: [11],
				targets: [{ target: "Chariot", current: 4.5, history: [] }],
			}),
			throwBombCore: row({
				specialWeaponIds: [11],
				targets: [
					{
						target: "Chariot",
						current: 3.273,
						history: [
							{ version: "2.0.0", value: 2 },
							{ version: "3.0.0", value: 6 },
						],
					},
				],
			}),
		};

		const result = WeaponParams.damageMultipliersForWeapon(rows, 11, "special");

		expect(result).toHaveLength(1);
		expect(result[0].history).toHaveLength(2);
		expect(result[0].current).toBe(3.273);
	});

	it("orders entries like DAMAGE_RECEIVERS", () => {
		const rows = {
			a: row({
				specialWeaponIds: [11],
				targets: [
					{ target: "Wsb_Shield", current: 2, history: [] },
					{ target: "Chariot", current: 3, history: [] },
				],
			}),
		};

		const result = WeaponParams.damageMultipliersForWeapon(rows, 11, "special");

		// Chariot precedes Wsb_Shield in DAMAGE_RECEIVERS
		expect(result.map((m) => m.target)).toEqual(["Chariot", "Wsb_Shield"]);
	});
});

describe("patchHistory damage multipliers", () => {
	const buildWith = (multiplier: DamageMultiplierWithHistory) =>
		WeaponParams.patchHistory(emptyParsed(11), VERSIONS, [], [multiplier]);

	it("attributes a change to the version after the recorded one and flags a higher rate as a buff", () => {
		const patches = buildWith({
			target: "Wsb_Shield",
			current: 2.2,
			history: [{ version: "1.0.0", value: 2 }],
		});

		expect(patches).toHaveLength(1);
		expect(patches[0].version).toBe("2.0.0");
		expect(patches[0].changes).toEqual([
			{
				category: DAMAGE_MULTIPLIER_PARAM_KEY,
				key: "Wsb_Shield",
				from: 2,
				to: 2.2,
				kind: "buff",
			},
		]);
	});

	it("flags a lower rate as a nerf", () => {
		const patches = buildWith({
			target: "NiceBall_Armor",
			current: 1.82,
			history: [{ version: "2.0.0", value: 2.6 }],
		});

		expect(patches).toHaveLength(1);
		expect(patches[0].version).toBe("3.0.0");
		expect(patches[0].changes[0].kind).toBe("nerf");
	});
});

describe("incomingDamageMultipliersForWeapon", () => {
	it("collects other weapons' rates against the weapon's receiver targets", () => {
		const rows = {
			fromSpecial: row({
				specialWeaponIds: [10],
				targets: [
					{
						target: "GreatBarrier_Barrier",
						current: 1.4,
						history: [{ version: "1.0.0", value: 2.8 }],
					},
				],
			}),
			fromMains: row({
				mainWeaponIds: [200, 201],
				targets: [
					{
						target: "GreatBarrier_WeakPoint",
						current: 3,
						history: [{ version: "2.0.0", value: 2 }],
					},
				],
			}),
			unrelated: row({
				mainWeaponIds: [400],
				targets: [{ target: "Chariot", current: 2, history: [] }],
			}),
		};

		// special id 2 is Big Bubbler (GreatBarrier_Barrier + GreatBarrier_WeakPoint)
		const result = WeaponParams.incomingDamageMultipliersForWeapon(
			rows,
			2,
			"special",
		);

		expect(result.map((m) => m.target)).toEqual([
			"GreatBarrier_Barrier",
			"GreatBarrier_WeakPoint",
		]);
		expect(result[1].attackers.mainWeaponIds).toEqual([200, 201]);
	});

	it("de-duplicates the same attacker group and target across rows", () => {
		const target = {
			target: "GreatBarrier_Barrier",
			current: 1.4,
			history: [{ version: "1.0.0", value: 2.8 }],
		};
		const rows = {
			bullet: row({ specialWeaponIds: [10], targets: [target] }),
			bombCore: row({ specialWeaponIds: [10], targets: [{ ...target }] }),
		};

		const result = WeaponParams.incomingDamageMultipliersForWeapon(
			rows,
			2,
			"special",
		);

		expect(result).toHaveLength(1);
	});

	it("returns nothing for a weapon that is not a damageable object", () => {
		const rows = {
			a: row({
				specialWeaponIds: [10],
				targets: [
					{
						target: "GreatBarrier_Barrier",
						current: 1,
						history: [{ version: "1.0.0", value: 2 }],
					},
				],
			}),
		};

		// special id 1 (Trizooka) is not in INCOMING_DAMAGE_RECEIVERS
		expect(
			WeaponParams.incomingDamageMultipliersForWeapon(rows, 1, "special"),
		).toEqual([]);
	});
});

describe("patchHistory incoming damage multipliers", () => {
	it("flags a higher incoming rate as a nerf to the defending weapon and carries the attackers", () => {
		const patches = WeaponParams.patchHistory(
			emptyParsed(2),
			VERSIONS,
			[],
			[],
			[
				{
					target: "GreatBarrier_Barrier",
					attackers: {
						mainWeaponIds: [200],
						subWeaponIds: [],
						specialWeaponIds: [],
					},
					current: 2.8,
					history: [{ version: "1.0.0", value: 1.4 }],
				},
			],
		);

		expect(patches).toHaveLength(1);
		expect(patches[0].version).toBe("2.0.0");
		expect(patches[0].changes[0]).toMatchObject({
			category: INCOMING_DAMAGE_MULTIPLIER_PARAM_KEY,
			key: "GreatBarrier_Barrier",
			from: 1.4,
			to: 2.8,
			kind: "nerf",
			attackers: { mainWeaponIds: [200] },
		});
	});

	it("flags a lower incoming rate as a buff to the defending weapon", () => {
		const patches = WeaponParams.patchHistory(
			emptyParsed(2),
			VERSIONS,
			[],
			[],
			[
				{
					target: "GreatBarrier_Barrier",
					attackers: {
						mainWeaponIds: [200],
						subWeaponIds: [],
						specialWeaponIds: [],
					},
					current: 1.4,
					history: [{ version: "1.0.0", value: 2.8 }],
				},
			],
		);

		expect(patches[0].changes[0].kind).toBe("buff");
	});
});

describe("parse damage falloff curves", () => {
	it("serializes a DistanceDamage array into a scaled damage @ distance string", () => {
		const parsed = WeaponParams.parse(
			0,
			{
				BlastParam: {
					DistanceDamage: [
						{ Damage: 1800, Distance: 3.6 },
						{ Damage: 300, Distance: 7 },
					],
				},
			},
			VERSIONS,
		);

		expect(parsed.categories.BlastParam.DistanceDamage.current).toBe(
			"180 @ 3.6, 30 @ 7",
		);
	});

	it("flattens nested breakpoint arrays", () => {
		const parsed = WeaponParams.parse(
			0,
			{
				BlastParam: {
					DistanceDamage: [
						[{ Damage: 1800, Distance: 3.6 }],
						[{ Damage: 300, Distance: 7 }],
					],
				},
			},
			VERSIONS,
		);

		expect(parsed.categories.BlastParam.DistanceDamage.current).toBe(
			"180 @ 3.6, 30 @ 7",
		);
	});

	it("tracks per-version history of a damage falloff curve", () => {
		const parsed = WeaponParams.parse(
			0,
			{
				BlastParam: {
					DistanceDamage: [{ Damage: 600, Distance: 4 }],
					"DistanceDamage@2.0.0": [{ Damage: 400, Distance: 4 }],
				},
			},
			VERSIONS,
		);

		expect(parsed.categories.BlastParam.DistanceDamage.history).toEqual([
			{ version: "2.0.0", value: "40 @ 4" },
		]);
	});
});

describe("classifyParamChange damage falloff curves", () => {
	it("flags higher damage as a buff", () => {
		expect(
			classifyParamChange("BlastParam", "DistanceDamage", "40 @ 4", "60 @ 4"),
		).toBe("buff");
	});

	it("flags lower damage as a nerf", () => {
		expect(
			classifyParamChange("BlastParam", "DistanceDamage", "60 @ 4", "40 @ 4"),
		).toBe("nerf");
	});

	it("flags longer reach at the same damage as a buff", () => {
		expect(
			classifyParamChange(
				"BlastParam",
				"DistanceDamage",
				"70 @ 0.94, 50 @ 3.3",
				"70 @ 1.01, 50 @ 3.37",
			),
		).toBe("buff");
	});

	it("flags shorter reach at the same damage as a nerf", () => {
		expect(
			classifyParamChange(
				"BlastParam",
				"DistanceDamage",
				"70 @ 1.01, 50 @ 3.37",
				"70 @ 0.975, 50 @ 3.37",
			),
		).toBe("nerf");
	});

	it("is neutral when damage rises but reach shrinks", () => {
		expect(
			classifyParamChange("BlastParam", "DistanceDamage", "60 @ 4", "70 @ 3.5"),
		).toBe("neutral");
	});

	it("is neutral when the curve gains or loses a breakpoint", () => {
		expect(
			classifyParamChange(
				"BlastParam",
				"DistanceDamage",
				"60 @ 4",
				"60 @ 4, 30 @ 8",
			),
		).toBe("neutral");
	});
});

describe("kitPatchHistories", () => {
	const kitHistory = () =>
		WeaponParams.kitPatchHistories({
			mainParsed: emptyParsed(11),
			versions: VERSIONS,
			kits: [{ weaponId: 11, subWeaponId: 1, specialWeaponId: 2 }],
			specialPointsByKit: {
				"11": {
					weaponId: 11,
					current: 180,
					history: [{ version: "1.0.0", value: 200 }],
				},
			},
			mainDamageMultipliers: [
				{
					target: "Wsb_Shield",
					current: 2.2,
					history: [{ version: "1.0.0", value: 2 }],
				},
			],
			subParams: { "1": emptyParsed(1) },
			subDamageMultipliers: {
				"1": [
					{
						target: "Chariot",
						current: 3,
						history: [{ version: "1.0.0", value: 2 }],
					},
				],
			},
			subIncomingDamageMultipliers: {},
			specialParams: { "2": emptyParsed(2) },
			specialDamageMultipliers: {
				"2": [
					{
						target: "NiceBall_Armor",
						current: 1.5,
						history: [{ version: "2.0.0", value: 2 }],
					},
				],
			},
			specialIncomingDamageMultipliers: {},
		});

	it("folds the kit's main, sub and special weapon changes into one descending history", () => {
		const [history] = kitHistory();

		expect(history.weaponId).toBe(11);
		expect(history.patches.map((patch) => patch.version)).toEqual([
			"3.0.0",
			"2.0.0",
		]);
	});

	it("tags each change with its source and groups main before sub before special", () => {
		const [history] = kitHistory();

		const v2 = history.patches.find((patch) => patch.version === "2.0.0")!;
		// special points + main damage rate (both main), then the sub weapon's damage rate
		expect(v2.changes.map((change) => change.source)).toEqual([
			"main",
			"main",
			"sub",
		]);
		expect(v2.changes[0].category).toBe(SPECIAL_POINTS_PARAM_KEY);

		const v3 = history.patches.find((patch) => patch.version === "3.0.0")!;
		expect(v3.changes.map((change) => change.source)).toEqual(["special"]);
	});
});
