import { describe, it, expect } from 'vitest';
import { deriveState, tickConditionsEndOfTurn } from './creatureState';
import { concentrationDC } from './cast';
import type { Condition } from '@/lib/schemas/common';

const cond = (name: string, value?: number): Condition => (value !== undefined ? { name, value } : { name });

describe('deriveState (5e)', () => {
  it('grappled zeroes speed', () => {
    const s = deriveState('5e', 30, [cond('Grappled')]);
    expect(s.speed).toBe(0);
    expect(s.badges).toContain('Speed 0');
  });

  it('poisoned imposes disadvantage on attacks and ability checks', () => {
    const s = deriveState('5e', 30, [cond('Poisoned')]);
    expect(s.attackModifier).toBe('disadvantage');
    expect(s.abilityCheckDisadvantage).toBe(true);
  });

  it('restrained: speed 0, disadv attacks, attacked with advantage, dex save disadvantage', () => {
    const s = deriveState('5e', 30, [cond('Restrained')]);
    expect(s.speed).toBe(0);
    expect(s.attackModifier).toBe('disadvantage');
    expect(s.attacksAgainstModifier).toBe('advantage');
    expect(s.saveModifiers.dex).toBe('disadvantage');
  });

  it('paralyzed auto-fails str/dex saves and incapacitates', () => {
    const s = deriveState('5e', 30, [cond('Paralyzed')]);
    expect(s.incapacitated).toBe(true);
    expect(s.saveModifiers.str).toBe('auto-fail');
    expect(s.saveModifiers.dex).toBe('auto-fail');
  });

  it('opposing advantage and disadvantage cancel to normal', () => {
    // invisible grants advantage to attack; poisoned imposes disadvantage → normal
    const s = deriveState('5e', 30, [cond('Invisible'), cond('Poisoned')]);
    expect(s.attackModifier).toBe('normal');
  });

  it('unknown/homebrew condition contributes nothing', () => {
    const s = deriveState('5e', 30, [cond('Inspired by bards')]);
    expect(s).toMatchObject({ speed: 30, attackModifier: 'normal', incapacitated: false });
    expect(s.badges).toEqual([]);
  });

  it('exhaustion applies cumulative level effects (3 = ability-check + attack + save disadvantage, speed halved)', () => {
    const s = deriveState('5e', 30, [cond('Exhaustion', 3)]);
    expect(s.abilityCheckDisadvantage).toBe(true);
    expect(s.attackModifier).toBe('disadvantage');
    expect(s.saveModifiers.wis).toBe('disadvantage');
    expect(s.speed).toBe(15); // halved at level 2+
    expect(s.badges).toContain('Exhaustion 3');
  });

  it('exhaustion 5 zeroes speed', () => {
    expect(deriveState('5e', 30, [cond('Exhaustion', 5)]).speed).toBe(0);
  });

  it('prone badge is range-qualified (advantage melee, disadvantage ranged)', () => {
    const s = deriveState('5e', 30, [cond('Prone')]);
    expect(s.badges).toContain('Attacked: adv. melee / disadv. ranged');
  });
});

describe('deriveState (pf2e)', () => {
  it('frightened is an all-checks penalty equal to its value', () => {
    const s = deriveState('pf2e', 25, [cond('Frightened', 2)]);
    expect(s.statusPenalties.all).toBe(2);
    expect(s.badges).toContain('−2 all checks');
  });

  it('same-family penalties take the worst; different families stay independent', () => {
    const both = deriveState('pf2e', 25, [cond('Frightened', 1), cond('Sickened', 3)]);
    expect(both.statusPenalties.all).toBe(3); // both target "all" → worst wins
    const mixed = deriveState('pf2e', 25, [cond('Enfeebled', 2), cond('Clumsy', 1)]);
    expect(mixed.statusPenalties.str).toBe(2); // Str-based
    expect(mixed.statusPenalties.dex).toBe(1); // Dex-based — NOT merged with enfeebled
    expect(mixed.badges).toEqual(expect.arrayContaining(['−2 Str', '−1 Dex/AC']));
  });

  it('slowed is action loss, not a check penalty', () => {
    const s = deriveState('pf2e', 25, [cond('Slowed', 2)]);
    expect(s.statusPenalties).toEqual({});
    expect(s.badges).toContain('Slowed 2 (−2 actions)');
  });

  it('grabbed zeroes speed and grants advantage to attackers', () => {
    const s = deriveState('pf2e', 25, [cond('Grabbed')]);
    expect(s.speed).toBe(0);
    expect(s.attacksAgainstModifier).toBe('advantage');
  });

  it('frightened decays by 1 at end of turn, removed at 0', () => {
    expect(tickConditionsEndOfTurn('pf2e', [cond('Frightened', 2)])).toEqual([{ name: 'Frightened', value: 1 }]);
    expect(tickConditionsEndOfTurn('pf2e', [cond('Frightened', 1)])).toEqual([]);
    // 5e frightened (unvalued) is untouched
    expect(tickConditionsEndOfTurn('5e', [cond('Frightened')])).toEqual([{ name: 'Frightened' }]);
  });
});

describe('concentrationDC', () => {
  it('is half the damage, floored, minimum 10', () => {
    expect(concentrationDC(8)).toBe(10); // floor(4)=4 → min 10
    expect(concentrationDC(22)).toBe(11);
    expect(concentrationDC(40)).toBe(20);
  });
});
