import { beforeEach, describe, expect, it } from "vitest";
import { healingEffect } from "./healerEffects";
import { createClassInventory } from "./healers";
import { BARRIER_RADIUS, barrierProtects, healerFieldContains, useGameStore } from "./store";
import type { HealerClassId, MemberId, WorldPosition } from "./types";

function startQuietEncounter(classId: HealerClassId) {
  useGameStore.getState().configureHealer(classId, "Aelia", createClassInventory(classId));
  useGameStore.getState().startEncounter();
  useGameStore.setState((state) => ({
    boss: { ...state.boss, nextMeleeAt: 999 },
    bossMotion: { ...state.bossMotion, nextMechanicAt: 999 },
  }));
}

function member(id: MemberId) {
  return useGameStore.getState().party.find((candidate) => candidate.id === id)!;
}

function setHealth(values: Partial<Record<MemberId, number>>) {
  useGameStore.setState((state) => ({
    party: state.party.map((candidate) => values[candidate.id] === undefined
      ? candidate
      : { ...candidate, hp: values[candidate.id]! }),
  }));
}

describe("Restoration Druid combat kit", () => {
  beforeEach(() => startQuietEncounter("druid"));

  it("generates Verdancy from healing ticks and spends it on Regrowth", () => {
    setHealth({ brann: 50 });
    expect(useGameStore.getState().castAbility("ability2")).toBe(true);
    useGameStore.getState().tick(1.01);
    expect(useGameStore.getState().healerMechanic.resource).toBe(1);

    expect(useGameStore.getState().castAbility("ability1")).toBe(true);
    expect(useGameStore.getState().activeCast?.resourceSpent).toBe(1);
    expect(useGameStore.getState().healerMechanic.resource).toBe(0);
    useGameStore.getState().tick(0.51);

    expect(member("brann").hp).toBeCloseTo(87);
    expect(healingEffect(member("brann"), "regrowth")).toBeDefined();
  });

  it("lets Nature's Cure trigger every active healing effect immediately", () => {
    setHealth({ brann: 50 });
    useGameStore.getState().castAbility("ability2");
    useGameStore.getState().tick(0.51);
    useGameStore.setState((state) => ({
      party: state.party.map((candidate) => candidate.id === "brann"
        ? { ...candidate, debuffs: [{ id: "test", name: "Test Hex", expiresAt: 10, nextTickAt: 9, tickDamage: 1 }] }
        : candidate),
    }));

    expect(useGameStore.getState().castAbility("ability4")).toBe(true);
    expect(member("brann").hp).toBe(56);
    expect(member("brann").debuffs).toEqual([]);
  });

  it("stacks Lifebloom, then spends Verdancy to force its bloom", () => {
    setHealth({ brann: 40 });
    for (let stack = 1; stack <= 3; stack += 1) {
      expect(useGameStore.getState().castAbility("ability3")).toBe(true);
      expect(healingEffect(member("brann"), "lifebloom")?.stacks).toBe(stack);
      useGameStore.getState().tick(0.51);
    }
    useGameStore.setState((state) => ({
      party: state.party.map((candidate) => candidate.id === "brann" ? { ...candidate, hp: 40 } : candidate),
      healerMechanic: { ...state.healerMechanic, resource: 2 },
    }));

    expect(useGameStore.getState().castAbility("ability3")).toBe(true);
    expect(healingEffect(member("brann"), "lifebloom")).toBeUndefined();
    expect(member("brann").hp).toBe(70);
    expect(useGameStore.getState().healerMechanic.resource).toBe(0);
  });

  it("targets only three most injured allies with Wild Growth", () => {
    setHealth({ aelia: 90, brann: 40, nia: 20, orin: 30, vale: 80 });
    expect(useGameStore.getState().castAbility("ability5")).toBe(true);
    const grown = useGameStore.getState().party
      .filter((candidate) => healingEffect(candidate, "wild-growth"))
      .map((candidate) => candidate.id);
    expect(grown).toEqual(["brann", "nia", "orin"]);
  });

  it("extends active growth and accelerates its next tick with Flourish", () => {
    setHealth({ brann: 50 });
    useGameStore.getState().castAbility("ability2");
    useGameStore.getState().tick(0.51);
    expect(useGameStore.getState().castAbility("ability6")).toBe(true);

    const effect = healingEffect(member("brann"), "rejuvenation")!;
    expect(effect.expiresAt).toBe(13);
    expect(effect.nextTickAt).toBeCloseTo(1);
    expect(useGameStore.getState().healerMechanic.flourishExpiresAt).toBeCloseTo(6.51);
    useGameStore.getState().tick(0.5);
    expect(healingEffect(member("brann"), "rejuvenation")?.nextTickAt).toBeCloseTo(1.5);
  });
});

describe("Restoration Shaman combat kit", () => {
  beforeEach(() => startQuietEncounter("shaman"));

  it("builds Tidal Surge with Riptide and spends it on Healing Wave", () => {
    setHealth({ brann: 40 });
    expect(useGameStore.getState().castAbility("ability2")).toBe(true);
    expect(useGameStore.getState().healerMechanic.resource).toBe(1);
    useGameStore.getState().tick(0.51);

    expect(useGameStore.getState().castAbility("ability1")).toBe(true);
    expect(useGameStore.getState().activeCast?.resourceSpent).toBe(1);
    expect(useGameStore.getState().activeCast?.completesAt).toBeCloseTo(0.51 + 0.325);
    useGameStore.getState().tick(0.34);

    expect(member("brann").hp).toBeCloseTo(107.9);
    expect(useGameStore.getState().healerMechanic.resource).toBe(0);
  });

  it("consumes Earth Shield after damage and generates Tidal Surge", () => {
    expect(useGameStore.getState().castAbility("ability3")).toBe(true);
    useGameStore.getState().tick(0.51);
    useGameStore.setState((state) => ({
      party: state.party.map((candidate) => candidate.id === "brann"
        ? { ...candidate, debuffs: [{ id: "shock", name: "Shock", expiresAt: 2, nextTickAt: 0.6, tickDamage: 20 }] }
        : candidate),
    }));

    useGameStore.getState().tick(0.1);
    expect(member("brann").reactiveHeal?.charges).toBe(5);
    expect(member("brann").hp).toBeLessThan(member("brann").maxHp);
    expect(useGameStore.getState().healerMechanic.resource).toBe(1);
  });

  it("uses Tidal Surge to add positional Chain Heal jumps", () => {
    const partyPositions: Record<MemberId, WorldPosition> = {
      brann: [0, 0], nia: [0, 1], orin: [0, 2], aelia: [0, 3], vale: [20, 20],
    };
    setHealth({ aelia: 40, brann: 40, nia: 20, orin: 20, vale: 20 });
    useGameStore.setState({ partyPositions });
    useGameStore.getState().castAbility("ability2");
    useGameStore.getState().tick(0.51);
    useGameStore.setState({ partyPositions });
    const valeBefore = member("vale").hp;

    expect(useGameStore.getState().castAbility("ability5")).toBe(true);
    expect(useGameStore.getState().healerMechanic.resource).toBe(0);
    expect(member("brann").hp).toBeGreaterThan(52);
    expect(member("nia").hp).toBeGreaterThan(20);
    expect(member("orin").hp).toBeGreaterThan(20);
    expect(member("aelia").hp).toBeGreaterThan(40);
    expect(member("vale").hp).toBe(valeBefore);
  });

  it("equalizes nearby health percentages without granting Barrier reduction", () => {
    const partyPositions: Record<MemberId, WorldPosition> = {
      aelia: [0, 0], brann: [0, 1], nia: [0, 2], orin: [20, 20], vale: [20, 21],
    };
    useGameStore.setState((state) => ({
      partyPositions,
      party: state.party.map((candidate) => candidate.id === "aelia"
        ? { ...candidate, hp: 20 }
        : candidate.id === "brann"
          ? { ...candidate, hp: 120 }
          : candidate.id === "nia"
            ? { ...candidate, hp: 47 }
            : candidate),
    }));

    expect(useGameStore.getState().castAbility("ability6")).toBe(true);
    expect(barrierProtects([0, 0], useGameStore.getState().barrier, 1)).toBe(false);
    useGameStore.getState().tick(1.01);

    expect(member("aelia").hp / member("aelia").maxHp).toBeCloseTo(0.5);
    expect(member("brann").hp / member("brann").maxHp).toBeCloseTo(0.5);
    expect(member("nia").hp / member("nia").maxHp).toBeCloseTo(0.5);
  });

  it("links allies within the enlarged 4m Spirit Link radius", () => {
    useGameStore.getState().castAbility("ability6");
    const field = useGameStore.getState().barrier;

    expect(healerFieldContains([field.center[0] + BARRIER_RADIUS - 0.01, field.center[1]], field, 1)).toBe(true);
    expect(healerFieldContains([field.center[0] + BARRIER_RADIUS + 0.01, field.center[1]], field, 1)).toBe(false);
  });
});

describe("Dawnforged Paladin combat kit", () => {
  beforeEach(() => startQuietEncounter("paladin"));

  it("echoes single-target healing through Beacon of Light", () => {
    setHealth({ brann: 50, nia: 30 });
    expect(useGameStore.getState().castAbility("ability3")).toBe(true);
    useGameStore.getState().tick(0.51);
    useGameStore.getState().selectMember("nia");

    expect(useGameStore.getState().castAbility("ability1")).toBe(true);
    useGameStore.getState().tick(0.56);

    expect(member("nia").hp).toBeCloseTo(64);
    expect(member("brann").hp).toBeCloseTo(63.6);
  });

  it("turns Crusader Strike into boss damage, triage healing, and Conviction", () => {
    setHealth({ brann: 40 });
    const bossHp = useGameStore.getState().boss.hp;

    expect(useGameStore.getState().castAbility("ability2")).toBe(true);

    expect(useGameStore.getState().boss.hp).toBe(bossHp - 18);
    expect(member("brann").hp).toBe(58);
    expect(useGameStore.getState().healerMechanic.resource).toBe(1);
  });

  it("spends full Conviction on Word of Glory and splashes party healing", () => {
    setHealth({ brann: 30, nia: 40 });
    useGameStore.setState((state) => ({
      healerMechanic: { ...state.healerMechanic, resource: 3 },
    }));

    expect(useGameStore.getState().castAbility("ability5")).toBe(true);

    expect(useGameStore.getState().healerMechanic.resource).toBe(0);
    expect(member("brann").hp).toBe(98);
    expect(member("nia").hp).toBe(48);
  });

  it("converts party attack damage into triage healing during Avenging Crusader", () => {
    setHealth({ brann: 20 });
    expect(useGameStore.getState().castAbility("ability6")).toBe(true);
    useGameStore.getState().tick(2);

    expect(useGameStore.getState().partyDamageEvents.length).toBeGreaterThan(0);
    expect(member("brann").hp).toBeGreaterThan(20);
  });
});

describe("Continuum Chronomancer combat kit", () => {
  beforeEach(() => startQuietEncounter("chronomancer"));

  it("anchors health, rewinds later damage, and gains a Chronoshard", () => {
    setHealth({ brann: 80 });
    expect(useGameStore.getState().castAbility("ability2")).toBe(true);
    useGameStore.getState().tick(0.51);
    setHealth({ brann: 35 });

    expect(useGameStore.getState().castAbility("ability2")).toBe(true);

    expect(member("brann").hp).toBe(80);
    expect(useGameStore.getState().healerMechanic.resource).toBe(1);
    expect(useGameStore.getState().healerMechanic.temporalAnchor).toBeNull();
  });

  it("schedules Echo of Tomorrow without front-loading its delayed heal", () => {
    setHealth({ brann: 30 });
    expect(useGameStore.getState().castAbility("ability3")).toBe(true);
    expect(member("brann").hp).toBe(40);

    useGameStore.getState().tick(2);
    useGameStore.getState().tick(0.9);
    expect(member("brann").hp).toBe(40);
    useGameStore.getState().tick(0.11);

    expect(member("brann").hp).toBe(68);
    expect(healingEffect(member("brann"), "temporal-echo")).toBeUndefined();
  });

  it("spends Chronoshards to heal the party and advance cooldowns", () => {
    setHealth({ aelia: 70, brann: 50, nia: 40 });
    useGameStore.setState((state) => ({
      healerMechanic: { ...state.healerMechanic, resource: 3 },
      cooldowns: { ...state.cooldowns, ability3: 10 },
    }));

    expect(useGameStore.getState().castAbility("ability5")).toBe(true);

    expect(useGameStore.getState().healerMechanic.resource).toBe(0);
    expect(useGameStore.getState().cooldowns.ability3).toBe(7);
    expect(member("aelia").hp).toBe(100);
    expect(member("brann").hp).toBe(84);
    expect(member("nia").hp).toBe(74);
  });

  it("restores only health lost during Time Loop and never resurrects", () => {
    setHealth({ aelia: 90, brann: 100, nia: 80 });
    expect(useGameStore.getState().castAbility("ability6")).toBe(true);
    setHealth({ aelia: 30, brann: 50, nia: 0 });

    useGameStore.getState().tick(2);
    useGameStore.getState().tick(2);
    useGameStore.getState().tick(2.01);

    expect(member("aelia").hp).toBe(90);
    expect(member("brann").hp).toBe(100);
    expect(member("nia").hp).toBe(0);
    expect(useGameStore.getState().healerMechanic.timeLoop).toBeNull();
  });
});
