import { describe, expect, it } from "vitest";
import {
  MAX_ACTIVE_ROSTER,
  PARTY_RECRUITS_PER_WAVE,
  createRpgRoguelikeRun,
  createRunGearItem,
  moveRpgFocus,
  partyCompositionLabel,
  partyRolePresentation,
  reduceRpgRoguelikeRun,
  rpgFocusId,
  rpgFocusItems,
  type RpgRoguelikeRunState,
} from ".";

function reachSpellDraft(seed = 810): RpgRoguelikeRunState {
  let state = createRpgRoguelikeRun({ seed });
  while (state.phase === "party-draft") {
    const draft = state.partyDraft!;
    const tank = draft.offers.find((offer) => offer.role === "Tank");
    const ordered = tank ? [tank, ...draft.offers.filter((offer) => offer !== tank)] : draft.offers;
    for (const offer of ordered) {
      if (state.roster.length >= MAX_ACTIVE_ROSTER || state.partyDraft!.recruitedThisWaveIds.length >= PARTY_RECRUITS_PER_WAVE) break;
      state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offer.candidateId });
    }
    state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
  }
  return state;
}

describe("RPG Roguelike semantic UI focus", () => {
  it("presents party roles as Tank/DPS and summarizes duplicate tanks", () => {
    expect(partyRolePresentation("Tank")).toEqual({ label: "Tank", icon: "⬡", className: "tank" });
    expect(partyRolePresentation("Damage")).toEqual({ label: "DPS", icon: "⚔", className: "damage" });
    expect(partyCompositionLabel([
      { role: "Tank" },
      { role: "Tank" },
      { role: "Damage" },
      { role: "Damage" },
    ])).toBe("2 Tanks · 2 DPS");
  });

  it("includes each party role in controller action labels", () => {
    const state = createRpgRoguelikeRun({ seed: 19 });
    const labels = new Map(rpgFocusItems(state).map((item) => [item.id, item.label]));
    for (const candidate of state.partyDraft!.offers) {
      expect(labels.get(rpgFocusId.partyOffer(candidate.candidateId))).toBe(
        `Recruit ${candidate.name}, ${partyRolePresentation(candidate.role).label}`,
      );
    }
  });

  it("moves horizontally within card rows and vertically between action groups", () => {
    let state = createRpgRoguelikeRun({ seed: 120 });
    const offers = state.partyDraft!.offers;
    const first = rpgFocusId.partyOffer(offers[0].candidateId);
    const second = rpgFocusId.partyOffer(offers[1].candidateId);
    const last = rpgFocusId.partyOffer(offers[offers.length - 1].candidateId);
    expect(moveRpgFocus(state, first, "right")).toBe(second);
    expect(moveRpgFocus(state, first, "left")).toBe(last);
    expect(moveRpgFocus(state, second, "down")).toBe(rpgFocusId.partyContinue);

    state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offers[0].candidateId });
    state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
    const laterOffer = rpgFocusId.partyOffer(state.partyDraft!.offers[0].candidateId);
    const rosterAction = rpgFocusId.partyMember(offers[0].candidateId);
    expect(moveRpgFocus(state, laterOffer, "down")).toBe(rosterAction);
    expect(moveRpgFocus(state, rosterAction, "down")).toBe(rpgFocusId.partyContinue);
  });

  it("orders party offers, later-wave roster removals, then Continue", () => {
    let state = createRpgRoguelikeRun({ seed: 12 });
    const firstOffer = state.partyDraft!.offers[0];
    expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
      ...state.partyDraft!.offers.map((offer) => rpgFocusId.partyOffer(offer.candidateId)),
      rpgFocusId.partyContinue,
    ]);

    state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: firstOffer.candidateId });
    expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.partyMember(firstOffer.candidateId));
    state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
    expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
      ...state.partyDraft!.offers.map((offer) => rpgFocusId.partyOffer(offer.candidateId)),
      rpgFocusId.partyMember(firstOffer.candidateId),
      rpgFocusId.partyContinue,
    ]);
  });

  it("omits final-wave removals that cannot be refilled", () => {
    let state = createRpgRoguelikeRun({ seed: 91 });
    state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
    const prior = state.partyDraft!.offers.find((offer) => offer.role === "Tank")!;
    state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: prior.candidateId });
    state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
    for (const offer of state.partyDraft!.offers.slice(0, 3)) {
      state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offer.candidateId });
    }

    expect(state.roster).toHaveLength(4);
    expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.partyMember(prior.candidateId));
    expect(reduceRpgRoguelikeRun(state, { type: "party-remove", memberId: prior.candidateId })).toBe(state);
  });

  it("locks first-wave spell picks, then exposes selected removals before Continue", () => {
    let state = reachSpellDraft();
    const spellId = state.spellDraft!.offers[0];
    state = reduceRpgRoguelikeRun(state, { type: "spell-pick", spellId });
    expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.spellSelected(spellId));

    state = reduceRpgRoguelikeRun(state, { type: "spell-next-wave" });
    expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
      ...state.spellDraft!.offers.map((offer) => rpgFocusId.spellOffer(offer)),
      rpgFocusId.spellSelected(spellId),
      rpgFocusId.spellContinue,
    ]);
  });

  it("orders shop purchases, bag sales, Rest, Revive, then Leave and omits disabled actions", () => {
    const drafted = reachSpellDraft(44);
    const offerItem = createRunGearItem("shop-weapon", "player", "weapon", 1);
    const soldItem = createRunGearItem("sold-armor", "player", "armor", 1);
    const bagItem = createRunGearItem("bag-trinket", "player", "trinket", 0);
    const first = drafted.roster[0];
    const second = drafted.roster[1];
    const state: RpgRoguelikeRunState = {
      ...drafted,
      phase: "shop",
      currency: 200,
      roster: drafted.roster.map((member) => member.instanceId === first.instanceId
        ? { ...member, hp: 0 }
        : member.instanceId === second.instanceId
          ? { ...member, hp: 1 }
          : member),
      bag: [bagItem],
      shop: {
        act: 1,
        offers: [
          { id: "available", item: offerItem, price: 80, sold: false },
          { id: "sold", item: soldItem, price: 20, sold: true },
        ],
        restCost: 35,
        reviveCost: 60,
      },
    };
    expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
      rpgFocusId.shopOffer("available"),
      rpgFocusId.shopSell(bagItem.id),
      rpgFocusId.shopRest,
      rpgFocusId.shopRevive(first.instanceId),
      rpgFocusId.shopLeave,
    ]);
  });
});
