import { beforeEach, describe, expect, it } from "vitest";
import { ARENA_CENTER, ARENA_WALL_RADIUS } from "./arena";
import { BOSS_DEFINITIONS } from "./bossCatalog";
import { createClassInventory } from "./healers";
import { createDefaultGearProgress } from "./progression/gear";
import { equipPassiveInfusion } from "./progression/infusions";
import { MAX_ACTIVE_ROSTER, PARTY_RECRUITS_PER_WAVE, rpgEncounterDifficulty } from "./rpgRoguelike";
import { useGameStore } from "./store";

function finishRpgDrafts() {
  let safety = 0;
  while (useGameStore.getState().rpgRun?.phase === "party-draft" && safety++ < 20) {
    const run = useGameStore.getState().rpgRun!;
    const draft = run.partyDraft!;
    const tank = draft.offers.find((offer) => offer.role === "Tank");
    const offers = tank ? [tank, ...draft.offers.filter((offer) => offer !== tank)] : draft.offers;
    for (const offer of offers) {
      const current = useGameStore.getState().rpgRun!;
      if (current.roster.length >= MAX_ACTIVE_ROSTER) break;
      if ((current.partyDraft?.recruitedThisWaveIds.length ?? 0) >= PARTY_RECRUITS_PER_WAVE) break;
      useGameStore.getState().dispatchRpgAction({ type: "party-recruit", candidateId: offer.candidateId });
    }
    useGameStore.getState().dispatchRpgAction({ type: "party-next-wave" });
  }
  while (useGameStore.getState().rpgRun?.phase === "spell-draft" && safety++ < 40) {
    const offers = useGameStore.getState().rpgRun!.spellDraft!.offers.slice(0, 2);
    for (const spellId of offers) useGameStore.getState().dispatchRpgAction({ type: "spell-pick", spellId });
    useGameStore.getState().dispatchRpgAction({ type: "spell-next-wave" });
  }
  if (safety >= 40) throw new Error("RPG draft did not terminate");
}

function silenceBosses() {
  useGameStore.setState((state) => ({
    boss: { ...state.boss, nextMeleeAt: 999 },
    bossMotion: { ...state.bossMotion, nextMechanicAt: 999 },
    additionalBosses: state.additionalBosses.map((entry) => ({
      ...entry,
      boss: { ...entry.boss, nextMeleeAt: 999 },
      motion: { ...entry.motion, nextMechanicAt: 999 },
    })),
  }));
}

function completeCurrentChallenge() {
  const objective = useGameStore.getState().rpgRun!.currentChallenge!.objective;
  useGameStore.setState((state) => ({
    endlessBossKills: objective.metric === "bossKills" ? objective.target : state.endlessBossKills,
    blockbreaker: objective.metric === "bricksBroken"
      ? { ...state.blockbreaker, bricksBroken: objective.target }
      : state.blockbreaker,
    aetherAssault: objective.metric === "kills"
      ? { ...state.aetherAssault, kills: objective.target }
      : state.aetherAssault,
  }));
  silenceBosses();
  useGameStore.getState().tick(0.1);
}

describe("RPG Roguelike store integration", () => {
  beforeEach(() => {
    useGameStore.getState().configureHealer(
      "priest",
      "Aelia",
      createClassInventory("priest"),
      "bulldrome",
      "rpg-roguelike",
    );
  });

  it("starts in a deterministic gated party draft with semantic focus", () => {
    const state = useGameStore.getState();
    expect(state.runMode).toBe("rpg-roguelike");
    expect(state.phase).toBe("briefing");
    expect(state.rpgRun?.phase).toBe("party-draft");
    expect(state.rpgRun?.partyDraft?.offers).toHaveLength(5);
    expect(state.abilityLoadout).toEqual({});
    expect(state.rpgFocusId).toMatch(/^party-offer:/);
  });

  it("starts with neutral permanent gear and infusion modifiers", () => {
    const progress = createDefaultGearProgress();
    progress.vale.slots.feet.level = 10;
    const infused = equipPassiveInfusion(progress, "priest", "mend-efficiency");
    useGameStore.getState().configureHealer(
      "priest",
      "Aelia",
      createClassInventory("priest"),
      "bulldrome",
      "rpg-roguelike",
      infused,
    );

    const state = useGameStore.getState();
    expect(state.passiveRunBuffId).toBeNull();
    expect(state.runModifiers.mendManaMultiplier).toBe(1);
    expect(state.gearModifiers.aelia.maxHealth).toBe(1);
  });

  it("projects four drafted companions and a mixed six-spell loadout into a challenge", () => {
    finishRpgDrafts();
    const drafted = useGameStore.getState().rpgRun!;
    expect(drafted.phase).toBe("challenge-briefing");
    expect(drafted.roster).toHaveLength(4);
    expect(drafted.selectedSpellIds).toHaveLength(6);

    expect(useGameStore.getState().dispatchRpgAction({ type: "challenge-start" })).toBe(true);
    const state = useGameStore.getState();
    expect(state.phase).toBe("combat");
    expect(state.rpgRun?.phase).toBe("challenge-active");
    expect(state.party).toHaveLength(5);
    expect(state.party.slice(1).every((member) => member.runProfile)).toBe(true);
    expect(Object.values(state.abilityLoadout)).toEqual(drafted.selectedSpellIds);
    expect(state.endlessMode).toBe(true);
  });

  it("keeps dual hallway tuning and applies the solo boss-room difficulty profile", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });

    const challenge = useGameStore.getState();
    const challengeDifficulty = rpgEncounterDifficulty("hallway-challenge", 0);
    expect(challenge.additionalBosses).toHaveLength(1);
    expect(challenge.boss.maxHp).toBe(Math.round(
      BOSS_DEFINITIONS[challenge.boss.id].maxHp * challengeDifficulty.healthMultiplier,
    ));
    expect(challenge.difficultyDamageMultiplier).toBe(challengeDifficulty.damageMultiplier);

    completeCurrentChallenge();
    useGameStore.getState().dispatchRpgAction({ type: "boss-start" });

    const bossRoom = useGameStore.getState();
    const bossDifficulty = rpgEncounterDifficulty("boss-room", 0);
    expect(bossRoom.additionalBosses).toHaveLength(0);
    expect(bossRoom.boss.maxHp).toBe(Math.round(
      BOSS_DEFINITIONS[bossRoom.boss.id].maxHp * bossDifficulty.healthMultiplier,
    ));
    expect(bossRoom.difficultyDamageMultiplier).toBe(bossDifficulty.damageMultiplier);
  });

  it("tracks Paladin and Chronomancer resources independently in mixed spell runs", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });
    silenceBosses();
    useGameStore.setState((state) => ({
      abilityLoadout: {
        ability1: "paladin-crusader-strike",
        ability2: "paladin-word-of-glory",
        ability3: "chronomancer-time-anchor",
      },
      selectedMemberId: "brann",
      party: state.party.map((member) => member.id === "brann" ? { ...member, hp: 40 } : member),
    }));

    expect(useGameStore.getState().castAbility("ability1")).toBe(true);
    expect(useGameStore.getState().rpgSpellResources.conviction).toBe(1);
    expect(useGameStore.getState().healerMechanic.resource).toBe(0);
    useGameStore.getState().tick(0.51);
    expect(useGameStore.getState().castAbility("ability2")).toBe(true);
    expect(useGameStore.getState().rpgSpellResources.conviction).toBe(0);

    useGameStore.getState().tick(0.51);
    expect(useGameStore.getState().castAbility("ability3")).toBe(true);
    useGameStore.getState().tick(0.51);
    useGameStore.setState((state) => ({
      party: state.party.map((member) => member.id === "brann" ? { ...member, hp: 30 } : member),
    }));
    expect(useGameStore.getState().castAbility("ability3")).toBe(true);
    expect(useGameStore.getState().rpgSpellResources.chronoshards).toBe(1);
  });

  it("applies public in-combat RPG actions without rebuilding the encounter", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });
    const member = useGameStore.getState().rpgRun!.roster[0];
    useGameStore.setState((state) => ({
      time: 12,
      mana: 37,
      boss: { ...state.boss, hp: state.boss.maxHp * 0.5 },
    }));
    const bossHp = useGameStore.getState().boss.hp;

    useGameStore.getState().dispatchRpgAction({
      type: "party-vitals",
      vitals: [{ instanceId: member.instanceId, hp: member.hp - 1 }],
    });

    const state = useGameStore.getState();
    expect(state.time).toBe(12);
    expect(state.mana).toBe(37);
    expect(state.boss.hp).toBe(bossHp);
    expect(state.rpgRun?.roster.find((candidate) => candidate.instanceId === member.instanceId)?.hp).toBe(member.hp - 1);
  });

  it("turns challenge failure into a boss briefing instead of ending the run", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });
    silenceBosses();
    useGameStore.setState((state) => ({ party: state.party.map((member) => ({ ...member, hp: 0 })) }));

    useGameStore.getState().tick(0.1);

    const state = useGameStore.getState();
    expect(state.rpgRun?.phase).toBe("boss-briefing");
    expect(state.rpgRun?.lastChallengeResult?.succeeded).toBe(false);
    expect(state.phase).toBe("briefing");
    expect(state.endlessMode).toBe(false);
  });

  it("ends the run when the healer falls, even if companions finish the boss", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });
    const objective = useGameStore.getState().rpgRun!.currentChallenge!.objective;
    useGameStore.setState((state) => ({
      endlessBossKills: objective.metric === "bossKills" ? objective.target : state.endlessBossKills,
      blockbreaker: objective.metric === "bricksBroken"
        ? { ...state.blockbreaker, bricksBroken: objective.target }
        : state.blockbreaker,
      aetherAssault: objective.metric === "kills"
        ? { ...state.aetherAssault, kills: objective.target }
        : state.aetherAssault,
    }));
    silenceBosses();
    useGameStore.getState().tick(0.1);
    useGameStore.getState().dispatchRpgAction({ type: "boss-start" });
    silenceBosses();
    useGameStore.setState((state) => ({
      boss: { ...state.boss, hp: 0 },
      party: state.party.map((member) => member.id === "aelia" ? { ...member, hp: 0 } : member),
    }));

    useGameStore.getState().tick(0.1);

    const state = useGameStore.getState();
    expect(state.phase).toBe("defeat");
    expect(state.rpgRun?.phase).toBe("defeat");
    expect(state.rpgRun?.playerHp).toBe(0);
    expect(state.rpgRun?.bossesDefeated).toBe(0);
  });

  it("ends the run when every companion falls, even if the healer and boss remain", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });
    completeCurrentChallenge();
    useGameStore.getState().dispatchRpgAction({ type: "boss-start" });
    silenceBosses();
    useGameStore.setState((state) => ({
      party: state.party.map((member) => member.id === "aelia" ? member : { ...member, hp: 0 }),
    }));

    useGameStore.getState().tick(0.1);

    expect(useGameStore.getState().phase).toBe("defeat");
    expect(useGameStore.getState().rpgRun?.phase).toBe("defeat");
    expect(useGameStore.getState().rpgRun?.bossesDefeated).toBe(0);
  });

  it("targets a living companion at room start and blocks casts after the boss is cleared", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });
    completeCurrentChallenge();
    const first = useGameStore.getState().rpgRun!.roster[0];
    useGameStore.getState().dispatchRpgAction({
      type: "party-vitals",
      vitals: [{ instanceId: first.instanceId, hp: 0 }],
    });
    useGameStore.getState().dispatchRpgAction({ type: "boss-start" });
    expect(useGameStore.getState().selectedMemberId).not.toBe("brann");
    silenceBosses();
    useGameStore.setState((state) => ({ boss: { ...state.boss, hp: 0 } }));
    useGameStore.getState().tick(0.1);

    const before = useGameStore.getState();
    expect(before.rpgRun?.phase).toBe("boss-cleared");
    expect(before.castAbility("ability1")).toBe(false);
    expect(useGameStore.getState().mana).toBe(before.mana);
  });

  it("unlocks the north portal after a boss and opens exactly one run chest", () => {
    finishRpgDrafts();
    useGameStore.getState().dispatchRpgAction({ type: "challenge-start" });
    const objective = useGameStore.getState().rpgRun!.currentChallenge!.objective;
    useGameStore.setState((state) => ({
      endlessBossKills: objective.metric === "bossKills" ? objective.target : state.endlessBossKills,
      blockbreaker: objective.metric === "bricksBroken"
        ? { ...state.blockbreaker, bricksBroken: objective.target }
        : state.blockbreaker,
      aetherAssault: objective.metric === "kills"
        ? { ...state.aetherAssault, kills: objective.target }
        : state.aetherAssault,
    }));
    silenceBosses();
    useGameStore.getState().tick(0.1);
    expect(useGameStore.getState().rpgRun?.phase).toBe("boss-briefing");

    useGameStore.getState().dispatchRpgAction({ type: "boss-start" });
    silenceBosses();
    const companion = useGameStore.getState().party[1];
    const companionInstanceId = companion.runProfile!.instanceId;
    useGameStore.setState((state) => ({
      boss: { ...state.boss, hp: 0 },
      party: state.party.map((member) => member.id === companion.id ? { ...member, hp: member.maxHp * 0.25 } : member),
    }));
    useGameStore.getState().tick(0.1);
    expect(useGameStore.getState().rpgRun?.phase).toBe("boss-cleared");
    expect(useGameStore.getState().phase).toBe("combat");
    const runMaxHp = useGameStore.getState().rpgRun!.roster
      .find((member) => member.instanceId === companionInstanceId)!.stats.maxHp;
    expect(useGameStore.getState().rpgRun!.roster.find((member) => member.instanceId === companionInstanceId)!.hp)
      .toBeCloseTo(runMaxHp * 0.25);

    useGameStore.setState((state) => ({
      party: state.party.map((member) => member.id === companion.id ? { ...member, hp: member.maxHp } : member),
    }));

    useGameStore.getState().setPlayerPosition([ARENA_CENTER[0], ARENA_CENTER[1] - ARENA_WALL_RADIUS - 0.7]);
    const reward = useGameStore.getState().rpgRun;
    expect(reward?.phase).toBe("reward");
    expect(reward?.roster.find((member) => member.instanceId === companionInstanceId)?.hp).toBe(runMaxHp);
    expect(reward?.pendingReward?.choices).toHaveLength(3);
    const chestId = reward?.pendingReward?.id;
    useGameStore.getState().setPlayerPosition([ARENA_CENTER[0], ARENA_CENTER[1] - ARENA_WALL_RADIUS - 0.8]);
    expect(useGameStore.getState().rpgRun?.pendingReward?.id).toBe(chestId);
  });
});
