import { describe, it, expect } from 'vitest';
import { parseDice, rollDice, DiceParseError } from './notation';
import { createRng } from '@/lib/rng';

describe('parseDice', () => {
  it('parses a simple die group', () => {
    expect(parseDice('3d6')).toEqual([{ kind: 'dice', sign: 1, count: 3, sides: 6 }]);
  });

  it('defaults count to 1', () => {
    expect(parseDice('d20')).toEqual([{ kind: 'dice', sign: 1, count: 1, sides: 20 }]);
  });

  it('parses mixed terms with signs', () => {
    expect(parseDice('2d6 + 1d4 - 2')).toEqual([
      { kind: 'dice', sign: 1, count: 2, sides: 6 },
      { kind: 'dice', sign: 1, count: 1, sides: 4 },
      { kind: 'const', sign: -1, value: 2 },
    ]);
  });

  it('parses keep-highest', () => {
    expect(parseDice('4d6kh3')).toEqual([
      { kind: 'dice', sign: 1, count: 4, sides: 6, keep: { op: 'kh', n: 3 } },
    ]);
  });

  it.each(['', 'abc', '3d', 'd', '1d6+', '0d6', '3d6kh4', '1d1'])(
    'rejects malformed input %s',
    (expr) => {
      expect(() => parseDice(expr)).toThrow(DiceParseError);
    },
  );
});

describe('rollDice', () => {
  it('is deterministic for a given seed', () => {
    const a = rollDice('3d6+2', createRng('seed-1'));
    const b = rollDice('3d6+2', createRng('seed-1'));
    expect(a.total).toBe(b.total);
    expect(a.terms[0]!.dice.map((d) => d.value)).toEqual(b.terms[0]!.dice.map((d) => d.value));
  });

  it('stays within bounds', () => {
    for (let i = 0; i < 200; i++) {
      const r = rollDice('1d20', createRng(i));
      expect(r.total).toBeGreaterThanOrEqual(1);
      expect(r.total).toBeLessThanOrEqual(20);
    }
  });

  it('keep-highest drops the lowest dice and excludes them from the total', () => {
    const r = rollDice('4d6kh3', createRng('chargen'));
    const dropped = r.terms[0]!.dice.filter((d) => !d.kept);
    expect(dropped).toHaveLength(1);
    const keptSum = r.terms[0]!.dice.filter((d) => d.kept).reduce((s, d) => s + d.value, 0);
    expect(r.total).toBe(keptSum);
    // the dropped die is <= every kept die
    const minKept = Math.min(...r.terms[0]!.dice.filter((d) => d.kept).map((d) => d.value));
    expect(dropped[0]!.value).toBeLessThanOrEqual(minKept);
  });

  it('applies constant modifiers', () => {
    const r = rollDice('1d6+5', createRng(1));
    expect(r.total).toBe(r.terms[0]!.dice[0]!.value + 5);
  });
});
