import { describe, expect, it } from "vitest";
import { AVAILABLE_BOSS_IDS, BOSS_DEFINITIONS } from "./bossCatalog";
import {
  RUN_BUFF_ORDER,
  RUN_BUFFS,
  bossHealthMultiplier,
  compileRunModifiers,
  effectiveRunBuffRank,
  formatRunBuffEffect,
  increaseRunBuffRank,
  selectRandomBossPair,
  selectRogueTrialsBosses,
  selectUnseenBosses,
  selectRunBuffDraft,
} from "./roguelike";
import type { RunBuffRanks } from "./types";

describe("roguelike progression", () => {
  it("adds 10% base boss HP per completed round", () => {
    expect(bossHealthMultiplier(1)).toBe(1);
    expect(bossHealthMultiplier(2)).toBe(1.1);
    expect(bossHealthMultiplier(5)).toBe(1.4);
  });

  it("defines 18 unique infusion-eligible ability buffs without legacy ids", () => {
    expect(RUN_BUFF_ORDER).toHaveLength(18);
    expect(new Set(RUN_BUFF_ORDER)).toHaveLength(18);
    expect(RUN_BUFF_ORDER.every((id) => RUN_BUFFS[id].infusionEligible)).toBe(true);
    expect(RUN_BUFF_ORDER).not.toContain("vital-bloom");
    expect(RUN_BUFF_ORDER).not.toContain("deep-wells");
    expect(RUN_BUFF_ORDER).not.toContain("restoring-grace");
  });

  it("compiles capped ranks and overlays one passive infusion rank", () => {
    const ranks: RunBuffRanks = {
      "mend-echo": 9,
      "mend-efficiency": 2,
      "renew-duration": 2,
      "shield-guard": 3,
      "purify-renew": 1,
      "radiance-shield": 2,
      "barrier-regen": 3,
    };
    const modifiers = compileRunModifiers(ranks, "mend-efficiency");

    expect(modifiers.mendExtraTargets).toBe(3);
    expect(modifiers.mendManaMultiplier).toBeCloseTo(0.75 ** 3);
    expect(modifiers.renewDurationBonus).toBe(4);
    expect(modifiers.shieldDamageTakenMultiplier).toBeCloseTo(0.76);
    expect(modifiers.purifyAppliesRenew).toBe(true);
    expect(modifiers.radianceAbsorb).toBe(18);
    expect(modifiers.barrierHealingPerSecond).toBe(9);
    expect(effectiveRunBuffRank(ranks, "mend-efficiency", "mend-efficiency")).toBe(3);
    expect(formatRunBuffEffect("mend-efficiency", 3)).toBe("58% less Mend mana cost");
  });

  it("increments earned ranks without exceeding each buff cap", () => {
    const first = increaseRunBuffRank({}, "purify-renew");
    const capped = increaseRunBuffRank(first, "purify-renew");
    expect(first["purify-renew"]).toBe(1);
    expect(capped["purify-renew"]).toBe(1);
  });

  it("draws unique eligible choices and handles one or zero remaining buffs", () => {
    expect(selectRunBuffDraft({}, null, () => 0)).toEqual(RUN_BUFF_ORDER.slice(0, 3));
    const maxed = Object.fromEntries(RUN_BUFF_ORDER.map((id) => [id, RUN_BUFFS[id].maxRank])) as RunBuffRanks;
    maxed["mend-efficiency"] = 2;
    expect(selectRunBuffDraft(maxed, null, () => 0)).toEqual(["mend-efficiency"]);
    expect(selectRunBuffDraft(maxed, "mend-efficiency", () => 0)).toEqual([]);
  });

  it("selects a distinct pair that excludes both bosses from the prior round", () => {
    const values = [0, 0];
    const pair = selectRandomBossPair(["bulldrome", "broodfang-spider"], () => values.shift() ?? 0);

    expect(pair).toEqual(["sandglass-scorpion", "cragclaw-crab"]);
    expect(new Set(pair)).toHaveLength(2);
    expect(pair).not.toContain("bulldrome");
    expect(pair).not.toContain("broodfang-spider");
  });

  it("selects three distinct unseen bosses for Rogue Trials round five", () => {
    const seen = [
      "bulldrome",
      "sandglass-scorpion",
      "cragclaw-crab",
      "mournveil-ghost",
      "crownshard-golem",
      "crystal-bat-matriarch",
      "stormwool-alpaca",
      "cluckhorn-colossus",
    ] as const;
    const trio = selectRogueTrialsBosses(5, seen, () => 0);

    expect(trio).toEqual(["ashwing-demon", "riftclaw-demon", "tempestscale-dragon"]);
    expect(new Set(trio)).toHaveLength(3);
    expect(trio.every((bossId) => !seen.includes(bossId as typeof seen[number]))).toBe(true);
  });

  it("never selects two Memory Sequence bosses in one pair or trio", () => {
    const pair = selectRandomBossPair([], () => 0.04);
    const trio = selectUnseenBosses(3, [], () => 0);
    const memoryBossCount = (bossIds: readonly (typeof AVAILABLE_BOSS_IDS)[number][]) => bossIds
      .filter((bossId) => BOSS_DEFINITIONS[bossId].mechanicIds.includes("memory-sequence"))
      .length;

    expect(memoryBossCount(pair)).toBeLessThanOrEqual(1);
    expect(memoryBossCount(trio)).toBeLessThanOrEqual(1);
  });

  it("fails instead of silently reusing seen bosses when unseen pool is too small", () => {
    expect(() => selectUnseenBosses(3, AVAILABLE_BOSS_IDS.slice(0, -2))).toThrow(/Cannot select 3 unseen bosses/);
  });
});
