import { describe, it, expect } from 'vitest';
import { characterDefaults, newSpellEntry, type Character } from '@/lib/schemas';
import { castSpell, availableSlotLevels, dropConcentration } from './cast';
import { spendResource, regainResource } from './resources';
import { rollDeathSave } from './deathSaves';

function mk(over: Partial<Character> = {}): Character {
  return {
    id: 'pc', campaignId: 'c', system: '5e', kind: 'pc', name: 'Mage', ancestry: '', className: 'Wizard', level: 5,
    abilities: { str: 8, dex: 14, con: 12, int: 16, wis: 10, cha: 10 },
    hp: { current: 0, max: 24, temp: 0 }, speed: 30, armorBonus: 0,
    skillRanks: {}, saveRanks: {}, perceptionRank: 'trained',
    ...characterDefaults(), notes: '', createdAt: '', updatedAt: '',
    ...over,
  };
}

describe('castSpell', () => {
  const fireball = newSpellEntry({ id: 's1', name: 'Fireball', level: 3, save: { ability: 'dex', basis: 'half' } });
  const haste = newSpellEntry({ id: 's2', name: 'Haste', level: 3, concentration: true });
  const bless = newSpellEntry({ id: 's3', name: 'Bless', level: 1, concentration: true });
  const firebolt = newSpellEntry({ id: 's4', name: 'Fire Bolt', level: 0 });

  const base = mk({
    spellcasting: { slots: [{ level: 1, max: 4, current: 4 }, { level: 3, max: 3, current: 2 }], spells: [fireball, haste, bless, firebolt] },
  });

  it('decrements the slot of the spell level', () => {
    const r = castSpell(base, 's1');
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    expect(r.patch.spellcasting!.slots.find((s) => s.level === 3)!.current).toBe(1);
    expect(r.log[0]).toMatch(/level-3 slot/);
  });

  it('upcasts: casting a level-3 spell with a higher slot consumes that slot', () => {
    const withHigh = mk({ spellcasting: { slots: [{ level: 3, max: 1, current: 0 }, { level: 5, max: 1, current: 1 }], spells: [fireball] } });
    const r = castSpell(withHigh, 's1', 5);
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    expect(r.patch.spellcasting!.slots.find((s) => s.level === 5)!.current).toBe(0);
  });

  it('fails when no slot of the requested level remains', () => {
    const empty = mk({ spellcasting: { slots: [{ level: 3, max: 2, current: 0 }], spells: [fireball] } });
    const r = castSpell(empty, 's1');
    expect(r.ok).toBe(false);
    if (r.ok) return;
    expect(r.reason).toMatch(/No level-3 slot/);
  });

  it('cantrips cost no slot', () => {
    const r = castSpell(base, 's4');
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    expect(r.patch.spellcasting).toBeUndefined();
    expect(r.log[0]).toMatch(/cantrip/);
  });

  it('sets concentration and replaces a prior concentration spell', () => {
    const concentrating = mk({
      concentration: { spellId: 's3', spellName: 'Bless' },
      spellcasting: { slots: [{ level: 3, max: 3, current: 2 }], spells: [haste, bless] },
    });
    const r = castSpell(concentrating, 's2');
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    expect(r.patch.concentration).toEqual({ spellId: 's2', spellName: 'Haste' });
    expect(r.log.some((l) => /Concentration on Bless ends/.test(l))).toBe(true);
  });

  it('consumes a warlock pact slot when no regular slot matches', () => {
    const warlock = mk({ spellcasting: { slots: [], pact: { level: 3, max: 2, current: 2 }, spells: [haste] } });
    const r = castSpell(warlock, 's2', 3);
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    expect(r.patch.spellcasting!.pact!.current).toBe(1);
  });

  it('availableSlotLevels offers the pact level for a warlock and upcast levels for a caster', () => {
    // Warlock: a level-1 spell can be cast with the level-3 pact slot (the only option).
    expect(availableSlotLevels({ slots: [], pact: { level: 3, max: 2, current: 2 }, spells: [] }, 1)).toEqual([3]);
    // Wizard: a level-1 spell can go in the level-1 or level-3 slot (upcast), but not empties.
    expect(availableSlotLevels({ slots: [{ level: 1, max: 4, current: 4 }, { level: 2, max: 2, current: 0 }, { level: 3, max: 3, current: 2 }], spells: [] }, 1)).toEqual([1, 3]);
    // Cantrips need no slot.
    expect(availableSlotLevels({ slots: [{ level: 1, max: 4, current: 4 }], spells: [] }, 0)).toEqual([]);
  });

  it('dropConcentration clears the slot', () => {
    const concentrating = mk({ concentration: { spellId: 's2', spellName: 'Haste' } });
    expect(dropConcentration(concentrating)).toEqual({ concentration: null });
    expect(dropConcentration(mk())).toEqual({});
  });
});

describe('spendResource / regainResource', () => {
  const base = mk({ resources: [{ id: 'ki', name: 'Ki', current: 2, max: 5, recovery: 'short' }] });

  it('spends and fails when too low', () => {
    const ok = spendResource(base, 'ki', 2);
    expect(ok.ok).toBe(true);
    if (ok.ok) expect(ok.patch.resources![0]!.current).toBe(0);
    const bad = spendResource(base, 'ki', 3);
    expect(bad.ok).toBe(false);
  });

  it('regains up to max', () => {
    const r = regainResource(base, 'ki', 10);
    expect(r.ok).toBe(true);
    if (r.ok) expect(r.patch.resources![0]!.current).toBe(5);
  });
});

describe('rollDeathSave', () => {
  it('nat 20 revives at 1 HP and clears the clock', () => {
    const r = rollDeathSave(mk({ defenses: { ...characterDefaults().defenses, deathSaves: { successes: 1, failures: 2 } } }), 20);
    expect(r.outcome).toBe('revived');
    expect(r.patch.hp!.current).toBe(1);
    expect(r.patch.defenses!.deathSaves).toEqual({ successes: 0, failures: 0 });
  });

  it('nat 1 adds two failures → dead at three', () => {
    const r = rollDeathSave(mk({ defenses: { ...characterDefaults().defenses, deathSaves: { successes: 0, failures: 1 } } }), 1);
    expect(r.outcome).toBe('dead');
  });

  it('≥10 succeeds; third success stabilizes', () => {
    const r = rollDeathSave(mk({ defenses: { ...characterDefaults().defenses, deathSaves: { successes: 2, failures: 0 } } }), 12);
    expect(r.outcome).toBe('stable');
  });

  it('<10 is a single failure, still pending', () => {
    const r = rollDeathSave(mk({ defenses: { ...characterDefaults().defenses, deathSaves: { successes: 0, failures: 0 } } }), 7);
    expect(r.outcome).toBe('pending');
    expect(r.patch.defenses!.deathSaves).toEqual({ successes: 0, failures: 1 });
  });
});
