import { describe, it, expect } from 'vitest';
import { normalizeSpell5e, normalizeSpellPf2e } from './spell';
import { normalizeArmor5e } from './armor';
import { normalizeMonsterDefenses, normalizeMonsterDefensesPf2e } from './monsterDefenses';
import { effectiveArmorClass } from '../types';

describe('normalizeSpell5e', () => {
  it('reads concentration from requires_concentration and parses a Dex half-save + damage', () => {
    const m = normalizeSpell5e({
      slug: 'fireball', name: 'Fireball', level_int: 3, school: 'Evocation',
      concentration: 'no', requires_concentration: false, ritual: 'no',
      desc: 'Each creature in a 20-foot-radius sphere must make a dexterity saving throw. A target takes 8d6 fire damage on a failed save, or half as much damage on a successful one.',
    } as never);
    expect(m).not.toBeNull();
    expect(m!.level).toBe(3);
    expect(m!.concentration).toBe(false);
    expect(m!.save).toEqual({ ability: 'dex', basis: 'half' });
    expect(m!.damage).toContainEqual({ dice: '8d6', type: 'fire' });
  });

  it('treats the "yes" string and requires_concentration flag as concentration', () => {
    const m = normalizeSpell5e({
      slug: 'hold-person', name: 'Hold Person', level_int: 2, school: 'Enchantment',
      concentration: 'yes', requires_concentration: true, ritual: 'no',
      desc: 'The target must succeed on a wisdom saving throw or be paralyzed.',
    } as never);
    expect(m!.concentration).toBe(true);
    expect(m!.save).toEqual({ ability: 'wis', basis: 'negates' });
    expect(m!.damage).toEqual([]);
  });

  it('returns null without a slug or name', () => {
    expect(normalizeSpell5e({ name: 'x' } as never)).toBeNull();
  });

  it('does not snapshot self-inflicted backlash damage (Wish)', () => {
    const m = normalizeSpell5e({
      slug: 'wish', name: 'Wish', level_int: 9, school: 'Conjuration', desc:
        'The stress of casting this spell weakens you. You take 1d10 necrotic damage per level of the spell you wished to duplicate.',
    } as never);
    expect(m!.damage).toEqual([]); // 1d10 necrotic is the caster's cost, not the spell's effect
  });

  it('captures dice with a flat modifier (Magic Missile 1d4 + 1 force)', () => {
    const m = normalizeSpell5e({
      slug: 'magic-missile', name: 'Magic Missile', level_int: 1, school: 'Evocation', desc:
        'Each dart hits a creature of your choice and deals 1d4 + 1 force damage to its target.',
    } as never);
    expect(m!.damage).toContainEqual({ dice: '1d4+1', type: 'force' });
  });

  it('picks the operative save, not an incidental mention (Irresistible Dance → Wis)', () => {
    const m = normalizeSpell5e({
      slug: 'irresistible-dance', name: 'Irresistible Dance', level_int: 6, school: 'Enchantment', desc:
        'The target has disadvantage on dexterity saving throws. The creature can make a wisdom saving throw to regain control of its body.',
    } as never);
    expect(m!.save).toEqual({ ability: 'wis', basis: 'negates' });
  });

  it('normalizes a pf2e Foundry spell with explicit save + concentrate trait', () => {
    const m = normalizeSpellPf2e({
      name: 'Fear', slug: 'fear',
      system: {
        level: { value: 1 },
        traits: { value: ['concentrate', 'emotion', 'enchantment', 'fear'] },
        defense: { save: { statistic: 'will' } },
      },
    } as never);
    expect(m!.level).toBe(1);
    expect(m!.concentration).toBe(true);
    expect(m!.school).toBe('enchantment');
    expect(m!.save).toEqual({ ability: 'wis', basis: 'half' });
  });
});

describe('normalizeArmor5e', () => {
  it('parses heavy armor with no dex bonus', () => {
    const m = normalizeArmor5e({ name: 'Plate', type: 'Heavy', ac: '18', dexCap: 0, stealthPenalty: true });
    expect(m).toMatchObject({ category: 'heavy', baseAc: 18, dexCap: 0, stealthDisadvantage: true });
    expect(effectiveArmorClass(m!, 3)).toBe(18); // dex ignored
  });

  it('caps medium armor dex at 2', () => {
    const m = normalizeArmor5e({ name: 'Half Plate', type: 'Medium', ac: '15 + Dex (max 2)', dexCap: 2 });
    expect(effectiveArmorClass(m!, 4)).toBe(17); // 15 + min(4,2)
    expect(effectiveArmorClass(m!, 1)).toBe(16); // 15 + 1
  });

  it('lets light armor take the full dex bonus', () => {
    const m = normalizeArmor5e({ name: 'Leather', type: 'Light', ac: '11 + Dex', dexCap: null });
    expect(effectiveArmorClass(m!, 5)).toBe(16);
  });

  it('models a shield as a flat additive bonus', () => {
    const m = normalizeArmor5e({ name: 'Shield', type: 'Shield', ac: '+2', dexCap: null });
    expect(m).toMatchObject({ category: 'shield', baseAc: 2 });
    // leather (16) + shield (+2)
    const leather = normalizeArmor5e({ name: 'Leather', type: 'Light', ac: '11 + Dex', dexCap: null })!;
    expect(effectiveArmorClass(leather, 5, m!.baseAc)).toBe(18);
  });
});

describe('normalizeMonsterDefenses', () => {
  it('parses a clean immunity list and condition immunities', () => {
    const d = normalizeMonsterDefenses({
      slug: 'x', name: 'X', damage_immunities: 'fire, poison', condition_immunities: 'poisoned, charmed',
    } as never);
    expect(d.immune).toEqual(['fire', 'poison']);
    expect(d.conditionImmune).toEqual(['poisoned', 'charmed']);
    expect(d.notes).toEqual([]);
  });

  it('drops qualified clauses into notes instead of enforcing them', () => {
    const d = normalizeMonsterDefenses({
      slug: 'x', name: 'X',
      damage_resistances: 'bludgeoning, piercing, and slashing from nonmagical attacks',
    } as never);
    expect(d.resist).toEqual([]); // qualified — not auto-applied
    expect(d.notes[0]).toMatch(/nonmagical/);
  });

  it('separates a clean type from a qualified clause split by semicolon', () => {
    const d = normalizeMonsterDefenses({
      slug: 'x', name: 'X', damage_resistances: 'cold; bludgeoning from nonmagical attacks',
    } as never);
    expect(d.resist).toEqual(['cold']);
    expect(d.notes).toHaveLength(1);
  });
});

describe('normalizeMonsterDefensesPf2e', () => {
  it('splits immunities into damage types vs conditions and reads flat resist/weakness', () => {
    const d = normalizeMonsterDefensesPf2e({
      immunity: ['fire', 'poison', 'paralysis', 'unconscious', 'cold_iron'],
      resistance: { cold: 5, bludgeoning: 5, lawful: 5, all: 3 },
      weakness: { fire: 10 },
    });
    expect(d.immune).toEqual(['fire', 'poison']); // damage types only
    expect(d.conditionImmune).toEqual(['paralysis', 'unconscious', 'cold_iron']); // non-damage immunities
    expect(d.resistFlat).toContainEqual({ type: 'cold', amount: 5 });
    expect(d.resistFlat).toContainEqual({ type: 'all', amount: 3 });
    expect(d.resistFlat.find((r) => r.type === 'lawful')).toBeUndefined(); // alignment, not damage
    expect(d.weakness).toEqual([{ type: 'fire', amount: 10 }]);
  });

  it('tolerates missing/empty fields', () => {
    expect(normalizeMonsterDefensesPf2e({})).toEqual({ immune: [], conditionImmune: [], resistFlat: [], weakness: [], notes: [] });
  });
});
