import { describe, it, expect } from 'vitest';
import { abilityModifier } from './abilities';
import { dnd5e, proficiencyBonus } from './dnd5e';
import { pf2e, pf2eProficiency } from './pf2e';
import type { CharacterRulesInput } from './types';

describe('abilityModifier', () => {
  it.each([
    [10, 0],
    [11, 0],
    [12, 1],
    [8, -1],
    [20, 5],
    [1, -5],
    [30, 10],
  ])('score %i -> %i', (score, mod) => {
    expect(abilityModifier(score)).toBe(mod);
  });

  it('never returns NaN for garbage input', () => {
    expect(abilityModifier(Number.NaN)).toBe(0);
    expect(abilityModifier(Infinity)).toBe(0);
  });
});

describe('5e proficiency bonus', () => {
  it.each([
    [1, 2],
    [4, 2],
    [5, 3],
    [9, 4],
    [13, 5],
    [17, 6],
    [20, 6],
  ])('level %i -> +%i', (lvl, pb) => {
    expect(proficiencyBonus(lvl)).toBe(pb);
  });

  it('clamps out-of-range levels (old app gave +251 at level 1000)', () => {
    expect(proficiencyBonus(1000)).toBe(6);
    expect(proficiencyBonus(0)).toBe(2);
    expect(proficiencyBonus(-5)).toBe(2);
  });
});

describe('5e derived stats', () => {
  const input: CharacterRulesInput = {
    level: 5,
    abilities: { str: 16, dex: 14, con: 14, int: 10, wis: 12, cha: 8 },
    skillRanks: { athletics: 'trained', stealth: 'expert' },
    saveRanks: { str: 'trained', con: 'trained' },
  };

  it('adds proficiency to trained skills and double for expertise', () => {
    const skills = dnd5e.skillModifiers(input);
    const athletics = skills.find((s) => s.key === 'athletics')!;
    const stealth = skills.find((s) => s.key === 'stealth')!;
    const arcana = skills.find((s) => s.key === 'arcana')!;
    expect(athletics.modifier).toBe(3 + 3); // str mod + PB(5)=3
    expect(stealth.modifier).toBe(2 + 6); // dex mod + 2*PB
    expect(arcana.modifier).toBe(0); // untrained int
  });

  it('computes spell save DC', () => {
    expect(dnd5e.spellSaveDc!(input, 'wis')).toBe(8 + 3 + 1);
  });

  it('unarmored AC does not penalise below 10 + dex', () => {
    // regression: old app applied a phantom -1 from heavy armor DEX cap
    const ac = dnd5e.baseArmorClass(input);
    expect(ac).toBe(10 + 2);
  });
});

describe('pf2e proficiency', () => {
  it('adds level + rank bonus when trained, 0 when untrained', () => {
    expect(pf2eProficiency(5, 'untrained')).toBe(0);
    expect(pf2eProficiency(5, 'trained')).toBe(5 + 2);
    expect(pf2eProficiency(5, 'expert')).toBe(5 + 4);
    expect(pf2eProficiency(10, 'legendary')).toBe(10 + 8);
  });

  it('skill modifier folds in ability + proficiency', () => {
    const skills = pf2e.skillModifiers({
      level: 3,
      abilities: { str: 18, dex: 10, con: 12, int: 10, wis: 14, cha: 10 },
      skillRanks: { athletics: 'expert' },
    });
    const athletics = skills.find((s) => s.key === 'athletics')!;
    expect(athletics.modifier).toBe(4 + (3 + 4)); // str mod + (level + expert bonus)
  });

  it('only con/dex/wis are real saves', () => {
    const saves = pf2e.saveModifiers({
      level: 4,
      abilities: { str: 10, dex: 14, con: 16, int: 10, wis: 12, cha: 10 },
      saveRanks: { con: 'expert', dex: 'trained', wis: 'trained' },
    });
    expect(saves.con).toBe(3 + (4 + 4));
    expect(saves.dex).toBe(2 + (4 + 2));
    expect(saves.str).toBe(0); // not a save: just ability mod
  });
});
