import { describe, it, expect } from 'vitest';
import type { Combatant, Encounter } from '@/lib/schemas/encounter';
import {
  addCombatant,
  applyDamage,
  applyHealing,
  applyInitiatives,
  currentCombatant,
  endEncounter,
  moveCombatant,
  nextTurn,
  previousTurn,
  removeCombatant,
  setTempHp,
  startEncounter,
  tickConditions,
  updateCombatant,
} from './engine';

function mk(id: string, initiative: number, hp = 10): Combatant {
  return {
    id,
    name: id,
    kind: 'monster',
    initiative,
    initBonus: 0,
    ac: 12,
    hp: { current: hp, max: hp, temp: 0 },
    conditions: [],
    notes: '',
  };
}

function encounter(combatants: Combatant[]): Encounter {
  return {
    id: 'e1', campaignId: 'c1', name: 'Test', status: 'planning',
    round: 0, turnIndex: 0, combatants, log: [],
    createdAt: '', updatedAt: '',
  };
}

describe('initiative & turns', () => {
  it('starts sorted by initiative descending at round 1', () => {
    const e = startEncounter(encounter([mk('a', 5), mk('b', 20), mk('c', 12)]));
    expect(e.combatants.map((c) => c.id)).toEqual(['b', 'c', 'a']);
    expect(e.round).toBe(1);
    expect(currentCombatant(e)?.id).toBe('b');
  });

  it('nextTurn wraps and increments the round', () => {
    let e = startEncounter(encounter([mk('a', 20), mk('b', 10)]));
    e = nextTurn(e);
    expect(currentCombatant(e)?.id).toBe('b');
    expect(e.round).toBe(1);
    e = nextTurn(e);
    expect(currentCombatant(e)?.id).toBe('a');
    expect(e.round).toBe(2);
  });

  it('previousTurn wraps back and decrements the round', () => {
    let e = startEncounter(encounter([mk('a', 20), mk('b', 10)]));
    e = previousTurn(e);
    expect(currentCombatant(e)?.id).toBe('b');
    expect(e.round).toBe(1); // never below 1
  });
});

describe('mutating mid-combat keeps the turn anchored (C16-C19 regressions)', () => {
  it('removing a combatant BEFORE the current turn keeps the same active combatant', () => {
    let e = startEncounter(encounter([mk('a', 30), mk('b', 20), mk('c', 10)]));
    e = nextTurn(e); // current = b
    expect(currentCombatant(e)?.id).toBe('b');
    e = removeCombatant(e, 'a'); // remove someone earlier in order
    expect(currentCombatant(e)?.id).toBe('b'); // still b's turn
  });

  it('removing the CURRENT combatant passes the turn to the next', () => {
    let e = startEncounter(encounter([mk('a', 30), mk('b', 20), mk('c', 10)]));
    e = nextTurn(e); // current = b
    e = removeCombatant(e, 'b');
    expect(currentCombatant(e)?.id).toBe('c');
  });

  it('removing the last combatant in order wraps to top and bumps round', () => {
    let e = startEncounter(encounter([mk('a', 30), mk('b', 20), mk('c', 10)]));
    e = nextTurn(e); e = nextTurn(e); // current = c (last)
    e = removeCombatant(e, 'c');
    expect(currentCombatant(e)?.id).toBe('a');
    expect(e.round).toBe(2);
  });

  it('auto-numbers duplicate names (Goblin -> Goblin 1, Goblin 2, …)', () => {
    let e = encounter([]);
    e = addCombatant(e, mk('g1', 10));
    e = { ...e, combatants: e.combatants.map((c) => ({ ...c, name: 'Goblin' })) }; // first is "Goblin"
    e = addCombatant(e, { ...mk('g2', 10), name: 'Goblin' });
    expect(e.combatants.map((c) => c.name).sort()).toEqual(['Goblin 1', 'Goblin 2']);
    e = addCombatant(e, { ...mk('g3', 10), name: 'Goblin' });
    expect(e.combatants.find((c) => c.id === 'g3')?.name).toBe('Goblin 3');
  });

  it('does not number unique names', () => {
    let e = encounter([]);
    e = addCombatant(e, { ...mk('a', 10), name: 'Aragorn' });
    e = addCombatant(e, { ...mk('b', 10), name: 'Legolas' });
    expect(e.combatants.map((c) => c.name)).toEqual(['Aragorn', 'Legolas']);
  });

  it('adding a combatant mid-combat inserts by initiative without changing whose turn it is', () => {
    let e = startEncounter(encounter([mk('a', 30), mk('b', 10)]));
    e = nextTurn(e); // current = b
    e = addCombatant(e, mk('c', 25)); // inserts between a and b
    expect(e.combatants.map((x) => x.id)).toEqual(['a', 'c', 'b']);
    expect(currentCombatant(e)?.id).toBe('b'); // unchanged
  });

  it('changing initiative re-sorts but keeps the turn anchored', () => {
    let e = startEncounter(encounter([mk('a', 30), mk('b', 20), mk('c', 10)]));
    e = nextTurn(e); // current = b
    e = updateCombatant(e, 'c', { initiative: 99 });
    expect(e.combatants[0]?.id).toBe('c');
    expect(currentCombatant(e)?.id).toBe('b');
  });

  it('moveCombatant nudges order but keeps the turn anchored', () => {
    let e = startEncounter(encounter([mk('a', 30), mk('b', 20), mk('c', 10)]));
    e = nextTurn(e); // current = b
    e = moveCombatant(e, 'a', 1); // swap a and b positions
    expect(e.combatants.map((x) => x.id)).toEqual(['b', 'a', 'c']);
    expect(currentCombatant(e)?.id).toBe('b');
  });
});

describe('HP transitions', () => {
  it('temp HP absorbs damage first, then current can go negative (overkill visible)', () => {
    let c = setTempHp(mk('a', 0, 10), 5);
    c = applyDamage(c, 12);
    expect(c.hp.temp).toBe(0);
    expect(c.hp.current).toBe(3); // 10 - (12-5)
    c = applyDamage(c, 20);
    expect(c.hp.current).toBe(-17); // overkill tracked, not clamped to 0
  });

  it('temp HP does not stack — keeps the higher value', () => {
    let c = setTempHp(mk('a', 0, 10), 5);
    c = setTempHp(c, 3);
    expect(c.hp.temp).toBe(5);
    c = setTempHp(c, 8);
    expect(c.hp.temp).toBe(8);
  });

  it('healing never exceeds max', () => {
    let c = mk('a', 0, 10);
    c = applyDamage(c, 6);
    c = applyHealing(c, 100);
    expect(c.hp.current).toBe(10);
  });

  it('ignores NaN/negative inputs', () => {
    const c = mk('a', 0, 10);
    expect(applyDamage(c, Number.NaN).hp.current).toBe(10);
    expect(applyHealing(c, -5).hp.current).toBe(10);
  });
});

describe('endEncounter', () => {
  it('marks the encounter ended', () => {
    expect(endEncounter(encounter([])).status).toBe('ended');
  });
});

describe('condition durations', () => {
  it('ticks duration down and removes expired conditions', () => {
    const c = { ...mk('a', 0), conditions: [{ name: 'Frightened', value: 2, duration: 1 }, { name: 'Prone' }] };
    const { combatant, expired } = tickConditions(c);
    expect(expired).toEqual(['Frightened']);
    expect(combatant.conditions.map((x) => x.name)).toEqual(['Prone']); // indefinite stays
  });

  it('nextTurn ticks the newly-active combatant and logs expiry', () => {
    let e = startEncounter(encounter([mk('a', 20), mk('b', 10)]));
    e = updateCombatant(e, 'b', { conditions: [{ name: 'Dazed', duration: 1 }] });
    e = nextTurn(e); // b's turn begins -> Dazed expires
    expect(currentCombatant(e)?.conditions).toHaveLength(0);
    expect(e.log.some((l) => /Dazed wore off/.test(l.text))).toBe(true);
  });
});

describe('applyInitiatives', () => {
  it('reassigns initiative, re-sorts, and keeps the current combatant anchored', () => {
    let e = startEncounter(encounter([mk('a', 20), mk('b', 10), mk('c', 5)]));
    e = nextTurn(e); // current = b
    e = applyInitiatives(e, { a: 1, b: 2, c: 30 });
    expect(e.combatants.map((x) => x.id)).toEqual(['c', 'b', 'a']);
    expect(currentCombatant(e)?.id).toBe('b');
  });
});
