import { resolveEncounter } from './encounter-resolver';
import type {
  EncounterDefinition,
  Perk,
  SurvivorState,
  SurvivorStats,
} from '@fog-explorer/api-interfaces';

// ── Constants ─────────────────────────────────────────────────────────────────

const TICKS_PER_MISSION = 20;
const MISSIONS_PER_SCENARIO = 2000; // 5 scenarios × 2000 = 10 000 total

// ── Types ─────────────────────────────────────────────────────────────────────

interface SimScenario {
  readonly name: string;
  readonly difficulty: number;
  readonly encounter: EncounterDefinition;
  readonly stats: SurvivorStats;
  readonly perks: Perk[];
}

interface MissionOutcome {
  readonly successCount: number;
  readonly wasInjured: boolean;
  readonly wasSacrificed: boolean;
}

interface DistributionSnapshot {
  successRate: number;
  injuryRate: number;
  sacrificeRate: number;
}

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

function makePerk(
  key: string,
  amount: number,
  type: 'additive' | 'multiplicative'
): Perk {
  return {
    id: '00000000-0000-4000-a000-000000000001',
    key,
    name: key,
    description: '',
    tags: [],
    modifiers: [{ target: 'successChance', type, amount }],
  };
}

function simulateMission(
  scenario: SimScenario,
  missionIndex: number
): MissionOutcome {
  let state: SurvivorState = 'active';
  let hookCount = 0;
  let successCount = 0;
  let wasInjured = false;
  let wasSacrificed = false;

  for (let tick = 0; tick < TICKS_PER_MISSION; tick++) {
    const result = resolveEncounter({
      seed: `mc-${scenario.name}-${missionIndex}-${tick}`,
      missionId: `mc-mission-${missionIndex}`,
      tickIndex: tick,
      difficulty: scenario.difficulty,
      encounter: scenario.encounter,
      survivor: {
        id: '00000000-0000-4000-a000-000000000002',
        state,
        stats: scenario.stats,
        perkSlots: scenario.perks,
        hookCount,
      },
    });

    if (result.survivorStateChange) {
      const sc = result.survivorStateChange;
      if (sc.to === 'injured') {
        state = 'injured';
        wasInjured = true;
      } else if (sc.to === 'downed') {
        // Stay downed for the next tick so downed+hookCount≥2 → sacrifice can fire.
        hookCount++;
        state = 'downed';
        wasInjured = true;
      } else if (sc.to === 'sacrificed') {
        wasSacrificed = true;
        break;
      }
    } else {
      // No state change: success, or failure with no consequence (e.g. downed+hookCount<2).
      // If currently downed (and not sacrificed), the encounter resolves without harm — rescued.
      if (state === 'downed') {
        state = 'active';
      }
      if (result.success) {
        successCount++;
      }
    }
  }

  return { successCount, wasInjured, wasSacrificed };
}

function runScenario(scenario: SimScenario): DistributionSnapshot {
  let totalSuccess = 0;
  let injuredCount = 0;
  let sacrificedCount = 0;

  for (let i = 0; i < MISSIONS_PER_SCENARIO; i++) {
    const { successCount, wasInjured, wasSacrificed } = simulateMission(
      scenario,
      i
    );
    totalSuccess += successCount;
    if (wasInjured) injuredCount++;
    if (wasSacrificed) sacrificedCount++;
  }

  const round = (n: number): number => Math.round(n * 1000) / 1000;
  return {
    successRate: round(totalSuccess / (MISSIONS_PER_SCENARIO * TICKS_PER_MISSION)),
    injuryRate: round(injuredCount / MISSIONS_PER_SCENARIO),
    sacrificeRate: round(sacrificedCount / MISSIONS_PER_SCENARIO),
  };
}

// ── Scenarios ─────────────────────────────────────────────────────────────────

const STANDARD_ENCOUNTER: EncounterDefinition = {
  key: 'generator',
  baseProbability: 0.6,
  tags: [],
};

const BASE_STATS: SurvivorStats = { objectives: 5, survival: 5, altruism: 5 };

const SCENARIOS = {
  baseline: {
    name: 'baseline',
    difficulty: 1,
    encounter: STANDARD_ENCOUNTER,
    stats: BASE_STATS,
    perks: [],
  },
  hard: {
    name: 'hard',
    difficulty: 3,
    encounter: STANDARD_ENCOUNTER,
    stats: BASE_STATS,
    perks: [],
  },
  objectivesPerk: {
    name: 'objectives-perk',
    difficulty: 1,
    encounter: STANDARD_ENCOUNTER,
    stats: BASE_STATS,
    perks: [makePerk('objectives-boost', 0.15, 'additive')],
  },
  survivalPerk: {
    name: 'survival-perk',
    difficulty: 1,
    encounter: STANDARD_ENCOUNTER,
    stats: { ...BASE_STATS, survival: 10 },
    perks: [],
  },
  stacked: {
    // objectives=7 → +0.14, add +0.05, mul ×1.10 → ~87% final probability.
    // Represents a well-optimised build that is strong but not degenerate.
    name: 'stacked',
    difficulty: 1,
    encounter: STANDARD_ENCOUNTER,
    stats: { objectives: 7, survival: 7, altruism: 5 },
    perks: [
      makePerk('add-boost', 0.05, 'additive'),
      makePerk('mul-boost', 0.1, 'multiplicative'),
    ],
  },
} satisfies Record<string, SimScenario>;

// ── Distribution snapshot tests ───────────────────────────────────────────────
// Seeds are deterministic strings → results are reproducible across runs.
// On first run, vitest writes the inline snapshots; CI then guards regressions.

describe('Monte Carlo balance harness — distribution snapshots', () => {
  let results: Record<keyof typeof SCENARIOS, DistributionSnapshot>;

  beforeAll(() => {
    results = {
      baseline: runScenario(SCENARIOS.baseline),
      hard: runScenario(SCENARIOS.hard),
      objectivesPerk: runScenario(SCENARIOS.objectivesPerk),
      survivalPerk: runScenario(SCENARIOS.survivalPerk),
      stacked: runScenario(SCENARIOS.stacked),
    };
  });

  it('baseline distribution', () => {
    expect(results.baseline).toMatchInlineSnapshot(`
      {
        "injuryRate": 0.973,
        "sacrificeRate": 0.194,
        "successRate": 0.67,
      }
    `);
  });

  it('hard difficulty distribution', () => {
    expect(results.hard).toMatchInlineSnapshot(`
      {
        "injuryRate": 0.999,
        "sacrificeRate": 0.72,
        "successRate": 0.341,
      }
    `);
  });

  it('objectives-perk distribution', () => {
    expect(results.objectivesPerk).toMatchInlineSnapshot(`
      {
        "injuryRate": 0.815,
        "sacrificeRate": 0.018,
        "successRate": 0.849,
      }
    `);
  });

  it('survival-perk distribution', () => {
    expect(results.survivalPerk).toMatchInlineSnapshot(`
      {
        "injuryRate": 0.846,
        "sacrificeRate": 0.093,
        "successRate": 0.686,
      }
    `);
  });

  it('stacked perk loadout distribution', () => {
    expect(results.stacked).toMatchInlineSnapshot(`
      {
        "injuryRate": 0.711,
        "sacrificeRate": 0.007,
        "successRate": 0.866,
      }
    `);
  });
});

// ── Balance envelope assertions ───────────────────────────────────────────────
// These fire on top of snapshots to catch degenerate configurations even if
// a snapshot was accidentally committed with broken numbers.

describe('Monte Carlo balance harness — balance envelopes', () => {
  let results: Record<keyof typeof SCENARIOS, DistributionSnapshot>;

  beforeAll(() => {
    results = {
      baseline: runScenario(SCENARIOS.baseline),
      hard: runScenario(SCENARIOS.hard),
      objectivesPerk: runScenario(SCENARIOS.objectivesPerk),
      survivalPerk: runScenario(SCENARIOS.survivalPerk),
      stacked: runScenario(SCENARIOS.stacked),
    };
  });

  it('hard difficulty has lower success rate than baseline', () => {
    expect(results.hard.successRate).toBeLessThan(results.baseline.successRate);
  });

  it('hard difficulty has higher sacrifice rate than baseline', () => {
    expect(results.hard.sacrificeRate).toBeGreaterThan(
      results.baseline.sacrificeRate
    );
  });

  it('objectives perk increases success rate over baseline', () => {
    expect(results.objectivesPerk.successRate).toBeGreaterThan(
      results.baseline.successRate
    );
  });

  it('stacked loadout does not produce degenerate success rate (>0.95)', () => {
    expect(results.stacked.successRate).toBeLessThan(0.95);
  });

  it('baseline sacrifice rate is meaningful (>1%) and not excessive (<25%)', () => {
    expect(results.baseline.sacrificeRate).toBeGreaterThan(0.01);
    expect(results.baseline.sacrificeRate).toBeLessThan(0.25);
  });

  it('every scenario produces some successes (not locked out)', () => {
    for (const dist of Object.values(results)) {
      expect(dist.successRate).toBeGreaterThan(0);
    }
  });

  it('every scenario has a chance of injury (game has stakes)', () => {
    for (const dist of Object.values(results)) {
      expect(dist.injuryRate).toBeGreaterThan(0);
    }
  });
});
