import * as fc from 'fast-check';
import { resolveEncounter, type ResolverInput } from './encounter-resolver';
import type { EncounterDefinition, Perk } from '@fog-explorer/api-interfaces';

// ── Arbitraries ──────────────────────────────────────────────────────────────

const arbSeed = fc.string({ minLength: 8, maxLength: 16 });

const arbEncounter = (tags: string[] = []): fc.Arbitrary<EncounterDefinition> =>
  fc.record({
    key: fc.constantFrom('generator', 'totem', 'chest', 'hook', 'exit-gate'),
    baseProbability: fc.float({ min: 0, max: 1, noNaN: true }),
    tags: fc.constant(tags),
  });

const arbStats = fc.record({
  objectives: fc.integer({ min: 1, max: 10 }),
  survival: fc.integer({ min: 1, max: 10 }),
  altruism: fc.integer({ min: 1, max: 10 }),
});

const arbInput = (overrides: Partial<ResolverInput> = {}): fc.Arbitrary<ResolverInput> =>
  fc.record({
    seed: arbSeed,
    missionId: fc.uuid(),
    tickIndex: fc.nat({ max: 999 }),
    difficulty: fc.integer({ min: 1, max: 3 }),
    encounter: arbEncounter(),
    survivor: fc.record({
      id: fc.uuid(),
      state: fc.constantFrom('active' as const, 'injured' as const),
      stats: arbStats,
      perkSlots: fc.constant<Perk[]>([]),
      hookCount: fc.integer({ min: 0, max: 2 }),
    }),
  }).map((base) => ({ ...base, ...overrides }));

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

function makeInput(partial: Partial<ResolverInput> = {}): ResolverInput {
  return {
    seed: 'fixed-seed',
    missionId: '00000000-0000-4000-a000-000000000001',
    tickIndex: 0,
    difficulty: 1,
    encounter: { key: 'generator', baseProbability: 0.5, tags: [] },
    survivor: {
      id: '00000000-0000-4000-a000-000000000002',
      state: 'active',
      stats: { objectives: 5, survival: 5, altruism: 5 },
      perkSlots: [],
      hookCount: 0,
    },
    ...partial,
  };
}

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

describe('resolveEncounter', () => {
  it('is deterministic: same seed always produces same result', () => {
    const input = makeInput();
    const a = resolveEncounter(input);
    const b = resolveEncounter(input);
    expect(a).toEqual(b);
  });

  it('produces different results for different seeds', () => {
    const results = new Set(
      Array.from({ length: 20 }, (_, i) =>
        resolveEncounter(makeInput({ seed: `seed-${i}` })).success
      )
    );
    // With 20 different seeds at p≈0.6 both true and false should appear
    expect(results.size).toBeGreaterThan(1);
  });

  it('state change is null on success', () => {
    // Force a near-certain success with baseProbability=1
    const result = resolveEncounter(makeInput({ encounter: { key: 'generator', baseProbability: 1, tags: [] } }));
    expect(result.success).toBe(true);
    expect(result.survivorStateChange).toBeNull();
  });

  it('injured survivor transitions to downed on failure', () => {
    // Force failure: baseProbability=0
    const result = resolveEncounter(
      makeInput({
        encounter: { key: 'hook', baseProbability: 0, tags: [] },
        survivor: {
          id: '00000000-0000-4000-a000-000000000002',
          state: 'injured',
          stats: { objectives: 1, survival: 5, altruism: 1 },
          perkSlots: [],
          hookCount: 1,
        },
      })
    );
    expect(result.success).toBe(false);
    expect(result.survivorStateChange).toEqual({ from: 'injured', to: 'downed' });
  });

  it('downed survivor at hookCount 2 transitions to sacrificed on failure', () => {
    const result = resolveEncounter(
      makeInput({
        encounter: { key: 'hook', baseProbability: 0, tags: [] },
        survivor: {
          id: '00000000-0000-4000-a000-000000000002',
          state: 'downed',
          stats: { objectives: 1, survival: 1, altruism: 1 },
          perkSlots: [],
          hookCount: 2,
        },
      })
    );
    expect(result.success).toBe(false);
    expect(result.survivorStateChange).toEqual({ from: 'downed', to: 'sacrificed' });
  });

  it('downed survivor below hookCount 2 has no state change on failure', () => {
    const result = resolveEncounter(
      makeInput({
        encounter: { key: 'hook', baseProbability: 0, tags: [] },
        survivor: {
          id: '00000000-0000-4000-a000-000000000002',
          state: 'downed',
          stats: { objectives: 1, survival: 5, altruism: 1 },
          perkSlots: [],
          hookCount: 1,
        },
      })
    );
    expect(result.success).toBe(false);
    expect(result.survivorStateChange).toBeNull();
  });

  it('perk modifier with matching tag applies to success chance', () => {
    const perk: Perk = {
      id: '00000000-0000-4000-a000-000000000003',
      key: 'adrenaline',
      name: 'Adrenaline',
      description: '',
      tags: [],
      modifiers: [
        { target: 'successChance', type: 'additive', amount: 0.5, condition: { encounterTags: ['generator'] } },
      ],
    };
    // With p=0 base but +0.5 from perk on matching tag → p=0.5 → some successes
    const results = Array.from({ length: 10 }, (_, i) =>
      resolveEncounter(
        makeInput({
          seed: `perk-test-${i}`,
          encounter: { key: 'gen', baseProbability: 0, tags: ['generator'] },
          survivor: {
            id: '00000000-0000-4000-a000-000000000002',
            state: 'active',
            stats: { objectives: 1, survival: 5, altruism: 1 },
            perkSlots: [perk],
            hookCount: 0,
          },
        })
      ).success
    );
    expect(results.some(Boolean)).toBe(true);
  });

  it('perk modifier without matching tag does not apply', () => {
    const perk: Perk = {
      id: '00000000-0000-4000-a000-000000000003',
      key: 'irrelevant-perk',
      name: 'Irrelevant',
      description: '',
      tags: [],
      modifiers: [
        { target: 'successChance', type: 'additive', amount: 0.99, condition: { encounterTags: ['totem'] } },
      ],
    };
    // p=0, perk requires 'totem' but encounter has 'generator' → still fails
    const result = resolveEncounter(
      makeInput({
        encounter: { key: 'gen', baseProbability: 0, tags: ['generator'] },
        survivor: {
          id: '00000000-0000-4000-a000-000000000002',
          state: 'active',
          stats: { objectives: 1, survival: 5, altruism: 1 },
          perkSlots: [perk],
          hookCount: 0,
        },
      })
    );
    expect(result.success).toBe(false);
    expect(result.modifiersApplied).toHaveLength(0);
  });

  it('logText is non-empty for all outcomes', () => {
    for (let i = 0; i < 20; i++) {
      const result = resolveEncounter(makeInput({ seed: `log-test-${i}` }));
      expect(result.logText.length).toBeGreaterThan(0);
    }
  });

  // Edge cases: modifier overflow / degenerate inputs
  it('massive positive perk modifier clamps probability to 1 — never throws', () => {
    const perk: Perk = {
      id: '00000000-0000-4000-a000-000000000003',
      key: 'overpowered',
      name: '',
      description: '',
      tags: [],
      modifiers: [{ target: 'successChance', type: 'additive', amount: 100 }],
    };
    const result = resolveEncounter(
      makeInput({ encounter: { key: 'gen', baseProbability: 0.5, tags: [] },
        survivor: { id: '00000000-0000-4000-a000-000000000002', state: 'active',
          stats: { objectives: 5, survival: 5, altruism: 5 }, perkSlots: [perk], hookCount: 0 } })
    );
    expect(result.success).toBe(true); // clamped to 1 → always succeeds
  });

  it('massive negative perk modifier clamps probability to 0 — never throws', () => {
    const perk: Perk = {
      id: '00000000-0000-4000-a000-000000000003',
      key: 'cursed',
      name: '',
      description: '',
      tags: [],
      modifiers: [{ target: 'successChance', type: 'additive', amount: -100 }],
    };
    const result = resolveEncounter(
      makeInput({ encounter: { key: 'gen', baseProbability: 0.5, tags: [] },
        survivor: { id: '00000000-0000-4000-a000-000000000002', state: 'active',
          stats: { objectives: 5, survival: 5, altruism: 5 }, perkSlots: [perk], hookCount: 0 } })
    );
    expect(result.success).toBe(false); // clamped to 0 → always fails
  });

  it('empty perkSlots array is handled without error', () => {
    expect(() =>
      resolveEncounter(makeInput({ survivor: {
        id: '00000000-0000-4000-a000-000000000002', state: 'active',
        stats: { objectives: 5, survival: 5, altruism: 5 }, perkSlots: [], hookCount: 0,
      } }))
    ).not.toThrow();
  });

  it('sacrificed survivor produces no state change on failure', () => {
    const result = resolveEncounter(makeInput({
      encounter: { key: 'gen', baseProbability: 0, tags: [] },
      survivor: { id: '00000000-0000-4000-a000-000000000002', state: 'sacrificed',
        stats: { objectives: 1, survival: 1, altruism: 1 }, perkSlots: [], hookCount: 2 },
    }));
    expect(result.success).toBe(false);
    expect(result.survivorStateChange).toBeNull();
  });

  it('idle survivor produces no state change on failure', () => {
    const result = resolveEncounter(makeInput({
      encounter: { key: 'gen', baseProbability: 0, tags: [] },
      survivor: { id: '00000000-0000-4000-a000-000000000002', state: 'idle',
        stats: { objectives: 1, survival: 1, altruism: 1 }, perkSlots: [], hookCount: 0 },
    }));
    expect(result.survivorStateChange).toBeNull();
  });
});

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

describe('resolveEncounter — property-based', () => {
  it('result fields match input identifiers', () => {
    fc.assert(
      fc.property(arbInput(), (input) => {
        const result = resolveEncounter(input);
        expect(result.missionId).toBe(input.missionId);
        expect(result.survivorId).toBe(input.survivor.id);
        expect(result.encounterKey).toBe(input.encounter.key);
        expect(result.tickIndex).toBe(input.tickIndex);
        expect(result.seed).toBe(input.seed);
      })
    );
  });

  it('state change is always null when encounter succeeds', () => {
    fc.assert(
      fc.property(arbInput(), (input) => {
        const result = resolveEncounter(input);
        if (result.success) {
          expect(result.survivorStateChange).toBeNull();
        }
      })
    );
  });

  it('state change from field always matches survivor current state', () => {
    fc.assert(
      fc.property(arbInput(), (input) => {
        const result = resolveEncounter(input);
        if (result.survivorStateChange) {
          expect(result.survivorStateChange.from).toBe(input.survivor.state);
        }
      })
    );
  });

  it('logText is always a non-empty string', () => {
    fc.assert(
      fc.property(arbInput(), (input) => {
        const result = resolveEncounter(input);
        expect(typeof result.logText).toBe('string');
        expect(result.logText.length).toBeGreaterThan(0);
      })
    );
  });

  it('higher difficulty never increases success rate (monte carlo, large sample)', () => {
    fc.assert(
      fc.property(
        arbSeed,
        arbEncounter(),
        arbStats,
        (seed, encounter, stats) => {
          const wins = (difficulty: number) =>
            Array.from({ length: 40 }, (_, i) =>
              resolveEncounter(
                makeInput({ seed: `${seed}-${i}`, difficulty, encounter, survivor: {
                  id: '00000000-0000-4000-a000-000000000002',
                  state: 'active',
                  stats,
                  perkSlots: [],
                  hookCount: 0,
                } })
              ).success
            ).filter(Boolean).length;

          const easy = wins(1);
          const hard = wins(3);
          // Over 40 trials, difficulty 3 should not outperform difficulty 1 by more than 10 wins
          expect(hard - easy).toBeLessThanOrEqual(10);
        }
      ),
      { numRuns: 50 }
    );
  });

  it('all modifiersApplied entries reference perks in the input perkSlots', () => {
    const arbPerk: fc.Arbitrary<Perk> = fc.record({
      id: fc.uuid(),
      key: fc.string({ minLength: 1 }),
      name: fc.string({ minLength: 1 }),
      description: fc.string(),
      tags: fc.array(fc.string()),
      modifiers: fc.array(
        fc.record({
          target: fc.constantFrom('successChance' as const),
          type: fc.constantFrom('additive' as const, 'multiplicative' as const),
          amount: fc.float({ min: Math.fround(-0.3), max: Math.fround(0.3), noNaN: true }),
          condition: fc.option(
            fc.record({ encounterTags: fc.array(fc.constantFrom('generator', 'totem'), { minLength: 1 }) }),
            { nil: undefined }
          ),
        }),
        { maxLength: 3 }
      ),
    });

    fc.assert(
      fc.property(
        arbInput().chain((base) =>
          fc.array(arbPerk, { maxLength: 4 }).map((perks) => ({
            ...base,
            survivor: { ...base.survivor, perkSlots: perks },
          }))
        ),
        (input) => {
          const result = resolveEncounter(input);
          const perkKeys = new Set(input.survivor.perkSlots.map((p) => p.key));
          for (const mod of result.modifiersApplied) {
            expect(perkKeys.has(mod.perkKey)).toBe(true);
          }
        }
      )
    );
  });
});

// ── Monte Carlo balance snapshot ─────────────────────────────────────────────

describe('encounter resolver — Monte Carlo balance snapshot', () => {
  const RUNS = 2000;

  function winRate(overrides: Partial<ResolverInput>): number {
    return (
      Array.from({ length: RUNS }, (_, i) =>
        resolveEncounter(makeInput({ seed: `mc-${i}`, ...overrides })).success
      ).filter(Boolean).length / RUNS
    );
  }

  it('difficulty 1, objectives 5, p=0.5 → win rate 55–75%', () => {
    const rate = winRate({ difficulty: 1, encounter: { key: 'gen', baseProbability: 0.5, tags: [] } });
    // objectives 5 adds 0.10, so effective p ≈ 0.6
    expect(rate).toBeGreaterThanOrEqual(0.55);
    expect(rate).toBeLessThanOrEqual(0.75);
  });

  it('difficulty 3, objectives 5, p=0.5 → win rate 30–55%', () => {
    const rate = winRate({ difficulty: 3, encounter: { key: 'gen', baseProbability: 0.5, tags: [] } });
    // objectives 5 adds 0.10, difficulty penalty -0.24 → effective p ≈ 0.36
    expect(rate).toBeGreaterThanOrEqual(0.30);
    expect(rate).toBeLessThanOrEqual(0.55);
  });

  it('near-impossible encounter (p=0, min stats) win rate stays below 5%', () => {
    // With baseProbability=0 and objectives=1: effective p = 0 + 1*0.02 = 0.02
    const rate = winRate({ encounter: { key: 'impossible', baseProbability: 0, tags: [] },
      survivor: { id: '00000000-0000-4000-a000-000000000002', state: 'active',
        stats: { objectives: 1, survival: 5, altruism: 1 }, perkSlots: [], hookCount: 0 },
    });
    expect(rate).toBeLessThan(0.05);
  });

  it('certain encounter (p=1) always succeeds', () => {
    const rate = winRate({ encounter: { key: 'certain', baseProbability: 1, tags: [] },
      survivor: { id: '00000000-0000-4000-a000-000000000002', state: 'active',
        stats: { objectives: 1, survival: 1, altruism: 1 }, perkSlots: [], hookCount: 0 },
      difficulty: 3,
    });
    // Even at difficulty 3 with min stats, effective p ≥ 0 — but p=1 with difficulty penalty:
    // 1 - 0.24 + 0.02 = 0.78, so not always 100%. Test that it's at least 70%.
    expect(rate).toBeGreaterThanOrEqual(0.70);
  });

  it('injury rate decreases as survival stat increases', () => {
    function injuryRate(survivalStat: number): number {
      let injuries = 0;
      for (let i = 0; i < RUNS; i++) {
        const result = resolveEncounter(
          makeInput({
            seed: `inj-${i}`,
            encounter: { key: 'hook', baseProbability: 0, tags: [] },
            survivor: {
              id: '00000000-0000-4000-a000-000000000002',
              state: 'active',
              stats: { objectives: 1, survival: survivalStat, altruism: 1 },
              perkSlots: [],
              hookCount: 0,
            },
          })
        );
        if (result.survivorStateChange?.to === 'injured') injuries++;
      }
      return injuries / RUNS;
    }

    const lowSurvival = injuryRate(1);
    const highSurvival = injuryRate(10);
    expect(highSurvival).toBeLessThan(lowSurvival);
    // At survival=10 the injury floor is 0.3, so rate should be around 0.3
    expect(highSurvival).toBeLessThanOrEqual(0.40);
    // At survival=1 the injury rate should be around 0.75
    expect(lowSurvival).toBeGreaterThanOrEqual(0.65);
  });
});
