import * as fc from 'fast-check';
import { applyPerkModifiers } from './perk-math';
import type { Perk } from '@fog-explorer/api-interfaces';

// ── Helpers ───────────────────────────────────────────────────────────────────

function makePerk(overrides: Partial<Perk> & { amount: number; type?: 'additive' | 'multiplicative'; tags?: string[] }): Perk {
  return {
    id: '00000000-0000-4000-a000-000000000001',
    key: overrides.key ?? 'test-perk',
    name: 'Test Perk',
    description: '',
    tags: [],
    modifiers: [
      {
        target: 'successChance',
        type: overrides.type ?? 'additive',
        amount: overrides.amount,
        condition: overrides.tags ? { encounterTags: overrides.tags } : undefined,
      },
    ],
  };
}

// ── Unit tests ────────────────────────────────────────────────────────────────

describe('applyPerkModifiers', () => {
  it('returns baseProbability unchanged with no perks', () => {
    const { probability, applied } = applyPerkModifiers(0.5, [], []);
    expect(probability).toBe(0.5);
    expect(applied).toHaveLength(0);
  });

  it('applies a single additive modifier', () => {
    const { probability } = applyPerkModifiers(0.4, [makePerk({ amount: 0.2 })], []);
    expect(probability).toBeCloseTo(0.6);
  });

  it('applies a single multiplicative modifier', () => {
    const { probability } = applyPerkModifiers(0.5, [makePerk({ amount: 0.2, type: 'multiplicative' })], []);
    expect(probability).toBeCloseTo(0.6);
  });

  it('additives sum before multiplicatives apply', () => {
    const perks: Perk[] = [
      makePerk({ key: 'a', amount: 0.1 }),                              // additive +0.1
      makePerk({ key: 'b', amount: 0.1 }),                              // additive +0.1
      makePerk({ key: 'c', amount: 0.5, type: 'multiplicative' }),     // ×1.5
    ];
    // base 0.4 + 0.2 additive = 0.6 → × 1.5 = 0.9
    const { probability } = applyPerkModifiers(0.4, perks, []);
    expect(probability).toBeCloseTo(0.9);
  });

  it('clamps overflow above 1.0 to 1.0', () => {
    const perks = [makePerk({ amount: 5.0 })]; // base 0.5 + 5 = 5.5 → clamped
    const { probability } = applyPerkModifiers(0.5, perks, []);
    expect(probability).toBe(1);
  });

  it('clamps underflow below 0 to 0', () => {
    const perks = [makePerk({ amount: -5.0 })];
    const { probability } = applyPerkModifiers(0.5, perks, []);
    expect(probability).toBe(0);
  });

  it('negative multiplicative modifier reduces probability', () => {
    const { probability } = applyPerkModifiers(0.6, [makePerk({ amount: -0.5, type: 'multiplicative' })], []);
    expect(probability).toBeCloseTo(0.3);
  });

  it('skips modifier when condition tag not in encounter tags', () => {
    const perk = makePerk({ amount: 0.3, tags: ['totem'] });
    const { probability, applied } = applyPerkModifiers(0.5, [perk], ['generator']);
    expect(probability).toBeCloseTo(0.5);
    expect(applied).toHaveLength(0);
  });

  it('applies modifier when condition tag matches', () => {
    const perk = makePerk({ amount: 0.3, tags: ['generator'] });
    const { probability } = applyPerkModifiers(0.5, [perk], ['generator', 'objectives']);
    expect(probability).toBeCloseTo(0.8);
  });

  it('unconditional modifier applies regardless of encounter tags', () => {
    const perk = makePerk({ amount: 0.2 }); // no condition
    const { probability } = applyPerkModifiers(0.5, [perk], []);
    expect(probability).toBeCloseTo(0.7);
  });

  it('non-successChance target modifiers are ignored', () => {
    const perk: Perk = {
      id: '00000000-0000-4000-a000-000000000001',
      key: 'stat-perk',
      name: '',
      description: '',
      tags: [],
      modifiers: [{ target: 'objectives', type: 'additive', amount: 2 }],
    };
    const { probability } = applyPerkModifiers(0.5, [perk], []);
    expect(probability).toBe(0.5);
  });

  it('records one applied entry per modifier', () => {
    const perks = [
      makePerk({ key: 'a', amount: 0.1 }),
      makePerk({ key: 'b', amount: 0.2 }),
    ];
    const { applied } = applyPerkModifiers(0.4, perks, []);
    expect(applied).toHaveLength(2);
    expect(applied.map((a) => a.perkKey)).toEqual(expect.arrayContaining(['a', 'b']));
  });

  it('applied entries record preApply and postApply probabilities', () => {
    const perk = makePerk({ amount: 0.2 });
    const { applied } = applyPerkModifiers(0.4, [perk], []);
    expect(applied[0].preApplyProbability).toBeCloseTo(0.4);
    expect(applied[0].postApplyProbability).toBeCloseTo(0.6);
  });
});

// ── Property-based tests ──────────────────────────────────────────────────────

describe('applyPerkModifiers — property-based', () => {
  const arbAmount = fc.float({ min: Math.fround(-1), max: Math.fround(1), noNaN: true });

  const arbPerk: fc.Arbitrary<Perk> = fc.record({
    id: fc.uuid(),
    key: fc.string({ minLength: 1, maxLength: 20 }),
    name: fc.string({ minLength: 1 }),
    description: fc.string(),
    tags: fc.constant([]),
    modifiers: fc.array(
      fc.record({
        target: fc.constantFrom('successChance' as const),
        type: fc.constantFrom('additive' as const, 'multiplicative' as const),
        amount: arbAmount,
        condition: fc.option(
          fc.record({ encounterTags: fc.array(fc.constantFrom('generator', 'totem'), { minLength: 1 }) }),
          { nil: undefined }
        ),
      }),
      { maxLength: 3 }
    ),
  });

  it('result probability is always in [0, 1]', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 1, noNaN: true }),
        fc.array(arbPerk, { maxLength: 4 }),
        fc.array(fc.constantFrom('generator', 'totem', 'exit'), { maxLength: 3 }),
        (base, perks, tags) => {
          const { probability } = applyPerkModifiers(base, perks, tags);
          expect(probability).toBeGreaterThanOrEqual(0);
          expect(probability).toBeLessThanOrEqual(1);
        }
      )
    );
  });

  it('every applied modifier references a perk in the input', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 1, noNaN: true }),
        fc.array(arbPerk, { maxLength: 4 }),
        (base, perks) => {
          const { applied } = applyPerkModifiers(base, perks, []);
          const keys = new Set(perks.map((p) => p.key));
          for (const mod of applied) {
            expect(keys.has(mod.perkKey)).toBe(true);
          }
        }
      )
    );
  });

  it('no perks never changes base probability (within [0,1])', () => {
    fc.assert(
      fc.property(fc.float({ min: 0, max: 1, noNaN: true }), (base) => {
        const { probability, applied } = applyPerkModifiers(base, [], []);
        expect(probability).toBeCloseTo(base);
        expect(applied).toHaveLength(0);
      })
    );
  });

  it('additive modifiers with large positive amounts still clamp to 1', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 1, noNaN: true }),
        fc.array(
          fc.float({ min: Math.fround(0.1), max: Math.fround(10), noNaN: true }),
          { minLength: 1, maxLength: 5 }
        ),
        (base, amounts) => {
          const perks = amounts.map((amount, i) =>
            makePerk({ key: `p${i}`, amount })
          );
          const { probability } = applyPerkModifiers(base, perks, []);
          expect(probability).toBeLessThanOrEqual(1);
        }
      )
    );
  });

  it('additive modifiers with large negative amounts still clamp to 0', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 1, noNaN: true }),
        fc.array(
          fc.float({ min: Math.fround(-10), max: Math.fround(-0.1), noNaN: true }),
          { minLength: 1, maxLength: 5 }
        ),
        (base, amounts) => {
          const perks = amounts.map((amount, i) =>
            makePerk({ key: `p${i}`, amount })
          );
          const { probability } = applyPerkModifiers(base, perks, []);
          expect(probability).toBeGreaterThanOrEqual(0);
        }
      )
    );
  });
});
