import { describe, expect, it } from "vitest";
import { freshParty } from "../data";
import { createBaseMotion } from "./shared";
import { advanceMechanicLoadout, BOSS_MECHANIC_POOL, bossAnimationCue, SOUL_SIPHON } from "./mechanicPool";
import type { BossMechanicContext } from "./types";
import type { BossState, PoolTelegraph, WorldPosition } from "../types";

const POSITIONS: BossMechanicContext["partyPositions"] = {
  aelia: [0, 4.5],
  brann: [0, 0],
  nia: [-3, 2],
  orin: [3, 2],
  vale: [0, -2],
};

function state(): BossState {
  return {
    id: "bulldrome",
    name: "Bulldrome",
    maxHp: 500,
    hp: 500,
    nextMeleeAt: Number.POSITIVE_INFINITY,
  };
}

function context(time: number, positions = POSITIONS): BossMechanicContext {
  return {
    boss: state(),
    motion: createBaseMotion("bulldrome"),
    party: freshParty(),
    partyPositions: structuredClone(positions),
    time,
    delta: 0.1,
    damageMember: (member, amount) => ({ ...member, hp: Math.max(0, member.hp - amount) }),
  };
}

function memoryTelegraph(): PoolTelegraph {
  return {
    id: "test-memory",
    kind: "memory",
    name: "Memory Sequence",
    center: [0, -8],
    radius: 0,
    activatesAt: 0,
    inputStartsAt: 0,
    expiresAt: 10,
    damage: 15,
    targetId: "aelia",
    sequence: ["triangle", "cross", "circle", "square"],
    tiles: [
      { symbol: "triangle", center: [-3, 2] },
      { symbol: "cross", center: [3, 2] },
      { symbol: "circle", center: [-3, -2] },
      { symbol: "square", center: [3, -2] },
    ],
    inputIndex: 0,
    resolved: false,
    hitIds: [],
  };
}

function advanceMemory(
  time: number,
  telegraph: PoolTelegraph,
  party = freshParty(),
  aeliaPosition: WorldPosition = [0, 4.5],
) {
  const positions = structuredClone(POSITIONS);
  positions.aelia = aeliaPosition;
  const source = context(time, positions);
  source.party = party;
  source.motion = {
    ...source.motion,
    activeMechanicId: "memory-sequence",
    poolTelegraphs: [telegraph],
  };
  return advanceMechanicLoadout(source, ["memory-sequence", "bull-charge"]);
}

function soulSiphonTelegraph(): PoolTelegraph {
  return {
    id: "test-soul-siphon",
    kind: "soul-siphon",
    name: "Soul Siphon",
    center: [0, -7],
    radius: 0,
    activatesAt: 0,
    expiresAt: Number.POSITIVE_INFINITY,
    damage: 0,
    targetId: "aelia",
    soulSiphon: {
      targetId: "aelia",
      ghostPosition: [0, 3.6],
      wardPosition: [0, -7],
      wardRadius: SOUL_SIPHON.wardRadius,
      nextDamageAt: SOUL_SIPHON.tickInterval,
      tickCount: 0,
    },
    resolved: false,
    hitIds: [],
  };
}

function advanceSoulSiphon(
  time: number,
  telegraph: PoolTelegraph,
  party = freshParty(),
  aeliaPosition: WorldPosition = [0, 4.5],
) {
  const positions = structuredClone(POSITIONS);
  positions.aelia = aeliaPosition;
  const source = context(time, positions);
  source.party = party;
  source.motion = {
    ...source.motion,
    activeMechanicId: "soul-siphon",
    poolTelegraphs: [telegraph],
  };
  return advanceMechanicLoadout(source, ["soul-siphon", "bull-charge"]);
}

describe("shared boss mechanic pool", () => {
  it.each(BOSS_MECHANIC_POOL.filter(({ id }) => id !== "basic-melee"))("starts $name directly from its canonical ID", ({ id }) => {
    const source = context(0);
    source.motion.nextMechanicAt = 0;
    const started = advanceMechanicLoadout(source, [id, id]);

    expect(started.motion.activeMechanicId).toBe(id);
    expect(["idle", "move", "attack", "special"]).toContain(bossAnimationCue(started.motion));
  });

  it("schedules the exact mechanic ID selected by a boss loadout", () => {
    const source = context(0);
    source.motion.nextMechanicAt = 0;
    const started = advanceMechanicLoadout(source, ["meteor-spread", "bull-charge"]);

    expect(started.motion.activeMechanicId).toBe("meteor-spread");
    expect(started.motion.poolTelegraphs).not.toHaveLength(0);
    expect(started.events[0].message).toContain(":");
    expect(bossAnimationCue(started.motion)).toBe("attack");
  });

  it("moves a web caster sideways during Binding Web and animates its return home", () => {
    const source = context(0);
    source.motion.nextMechanicAt = 0;
    const started = advanceMechanicLoadout(source, ["binding-web", "venom-purge"]);
    expect(bossAnimationCue(started.motion)).toBe("move");

    const next = context(0.1);
    next.boss = started.boss;
    next.motion = started.motion;
    next.party = started.party;
    const advanced = advanceMechanicLoadout(next, ["binding-web", "venom-purge"]);
    expect(advanced.motion.position).not.toEqual(started.motion.position);

    advanced.motion.activeMechanicId = null;
    advanced.motion.mode = "holding";
    expect(bossAnimationCue(advanced.motion)).toBe("move");
  });

  it("splits soak damage across allies inside its indicator", () => {
    const source = context(0);
    const activatesAt = 1.5;
    const soak = {
      id: "test-soak",
      kind: "soak" as const,
      name: "Aetheric Soak",
      center: [0, 0] as WorldPosition,
      radius: 2.5,
      activatesAt,
      expiresAt: activatesAt + 0.5,
      damage: 0,
      totalDamage: 90,
      minimumParticipants: 3,
      targetId: "brann" as const,
      resolved: false,
      hitIds: [],
    };
    const motion = { ...source.motion, activeMechanicId: "aetheric-soak" as const, poolTelegraphs: [soak] };
    const positions = structuredClone(POSITIONS);
    positions.aelia = [0.2, 0];
    positions.brann = [0, 0];
    positions.nia = [-0.2, 0];
    positions.vale = [0, -4];
    const atImpact = { ...source, motion, partyPositions: positions, time: activatesAt };
    const resolved = advanceMechanicLoadout(atImpact, ["aetheric-soak", "bull-charge"]);

    expect(resolved.party.find((member) => member.id === "aelia")!.hp).toBe(source.party[0].hp - 30);
    expect(resolved.party.find((member) => member.id === "brann")!.hp).toBe(source.party[1].hp - 30);
    expect(resolved.party.find((member) => member.id === "nia")!.hp).toBe(source.party[2].hp - 30);
    expect(resolved.events[0].message).toContain("splits 90 damage across 3 allies");
  });

  it("resolves a donut only in its outer ring", () => {
    const source = context(0);
    const activatesAt = 1.5;
    const donut = {
      id: "test-donut",
      kind: "donut" as const,
      name: "Hollow Collapse",
      center: [0, 0] as WorldPosition,
      radius: 5,
      innerRadius: 2,
      activatesAt,
      expiresAt: activatesAt + 0.5,
      damage: 25,
      resolved: false,
      hitIds: [],
    };
    const positions = structuredClone(POSITIONS);
    positions.aelia = [0, 0];
    positions.brann = [3, 0];
    const atImpact = {
      ...source,
      motion: { ...source.motion, activeMechanicId: "hollow-collapse" as const, poolTelegraphs: [donut] },
      partyPositions: positions,
      time: activatesAt,
    };
    const resolved = advanceMechanicLoadout(atImpact, ["hollow-collapse", "bull-charge"]);

    expect(resolved.party.find((member) => member.id === "aelia")!.hp).toBe(source.party[0].hp);
    expect(resolved.party.find((member) => member.id === "brann")!.hp).toBe(source.party[1].hp - 25);
  });

  it("requires only the healer to cross four tiles in shown order", () => {
    let current = advanceMemory(1, memoryTelegraph(), freshParty(), [-3, 2]);
    expect(current.motion.poolTelegraphs[0].inputIndex).toBe(1);

    current = advanceMemory(1.1, current.motion.poolTelegraphs[0], current.party, [0, 4.5]);
    current = advanceMemory(1.2, current.motion.poolTelegraphs[0], current.party, [3, 2]);
    expect(current.motion.poolTelegraphs[0].inputIndex).toBe(2);

    current = advanceMemory(1.3, current.motion.poolTelegraphs[0], current.party, [0, 4.5]);
    current = advanceMemory(1.4, current.motion.poolTelegraphs[0], current.party, [-3, -2]);
    current = advanceMemory(1.5, current.motion.poolTelegraphs[0], current.party, [0, 4.5]);
    current = advanceMemory(1.6, current.motion.poolTelegraphs[0], current.party, [3, -2]);

    expect(current.motion.poolTelegraphs).toHaveLength(0);
    expect(current.events[0].message).toBe("Memory Sequence cleared by healer.");
    expect(current.party).toEqual(freshParty());
  });

  it("ignores NPC positions but deals 15 raidwide damage when healer selects a wrong tile", () => {
    const waiting = advanceMemory(1, memoryTelegraph(), freshParty(), [0, 4.5]);
    expect(waiting.events).toEqual([]);
    expect(waiting.motion.poolTelegraphs[0].inputIndex).toBe(0);

    const failed = advanceMemory(1, memoryTelegraph(), freshParty(), [3, 2]);
    for (const member of failed.party) {
      expect(member.hp).toBe(member.maxHp - 15);
    }
    expect(failed.events[0].message).toContain("15 raidwide damage");
  });

  it("drains the healer until they enter the opposite cleansing ward", () => {
    const started = soulSiphonTelegraph();
    const firstTick = advanceSoulSiphon(SOUL_SIPHON.tickInterval + 0.01, started);
    expect(firstTick.party.find((member) => member.id === "aelia")!.hp).toBe(
      freshParty()[0].hp - SOUL_SIPHON.tickDamage,
    );
    expect(firstTick.motion.poolTelegraphs[0].soulSiphon?.ghostPosition).not.toEqual(started.soulSiphon?.ghostPosition);

    const wardPosition = firstTick.motion.poolTelegraphs[0].soulSiphon!.wardPosition;
    const cleansed = advanceSoulSiphon(1, firstTick.motion.poolTelegraphs[0], firstTick.party, wardPosition);
    expect(cleansed.motion.poolTelegraphs).toHaveLength(0);
    expect(cleansed.events[0].message).toContain("cleansing ward");
  });

  it("deals half-strength ramping Soul Siphon damage and caps the ramp", () => {
    const result = advanceSoulSiphon(SOUL_SIPHON.tickInterval * 6 + 0.01, soulSiphonTelegraph());
    const aelia = result.party.find((member) => member.id === "aelia")!;

    expect(SOUL_SIPHON.tickDamage).toBe(3);
    expect(SOUL_SIPHON.tickRamp).toBe(1);
    expect(aelia.hp).toBe(freshParty()[0].hp - (3 + 4 + 5 + 6 + 7 + 7));
    expect(result.motion.poolTelegraphs[0].soulSiphon?.tickCount).toBe(6);
  });
});
