import { describe, it, expect, afterEach } from 'vitest';
import { createState as createIntroState, startGame, tick, buyUpgrade, applyOfflineProgress, type State } from './engine.ts';

function createState(): State {
  return startGame(createIntroState());
}
import { UPGRADES } from './upgrades.ts';

// Deterministic PRNG so simulation tests aren't flaky, but still exercise
// the real event-selection and random-jitter code paths.
function seededRandom(seed: number): () => number {
  let s = seed;
  return () => {
    s = (s * 1103515245 + 12345) & 0x7fffffff;
    return s / 0x7fffffff;
  };
}

let restoreRandom: (() => void) | null = null;
function seedRandom(seed: number): void {
  const rnd = seededRandom(seed);
  const original = Math.random;
  Math.random = rnd;
  restoreRandom = () => { Math.random = original; };
}

afterEach(() => {
  restoreRandom?.();
  restoreRandom = null;
});

/** Buy every upgrade whose id is in `ids`, in order, if affordable and available. */
function tryBuy(state: State, ids: string[]): State {
  let s = state;
  for (const id of ids) {
    s = buyUpgrade(s, id);
  }
  return s;
}

describe('resource invariants', () => {
  it('keeps hemlighet, kaos, and ritual within [0, 100] over a long random run', () => {
    seedRandom(42);
    let s = createState();
    for (let i = 0; i < 30000 && s.phase === 'playing'; i++) {
      s = tick(s);
      // occasionally recruit / exert influence to exercise those paths too
      if (i % 500 === 0) s = tryBuy(s, ['recruit_neighbor', 'exert_influence']);
      expect(s.hemlighet).toBeGreaterThanOrEqual(0);
      expect(s.hemlighet).toBeLessThanOrEqual(100);
      expect(s.kaos).toBeGreaterThanOrEqual(0);
      expect(s.kaos).toBeLessThanOrEqual(100);
      expect(s.ritual).toBeGreaterThanOrEqual(0);
      expect(s.ritual).toBeLessThanOrEqual(100);
      expect(s.troende).toBeGreaterThanOrEqual(1);
    }
  });

  it('never lets a kaos event push kaos negative or leave it stuck at 100', () => {
    seedRandom(7);
    let s = createState();
    s.kaos = 99;
    for (let i = 0; i < 5000 && s.phase === 'playing'; i++) {
      s = tick(s);
    }
    expect(s.kaos).toBeGreaterThanOrEqual(0);
  });
});

describe('the ritual is winnable', () => {
  it('reaches phase "won" within a reasonable number of ticks under a sensible strategy', () => {
    seedRandom(1);
    let s = createState();

    const storyPath = [
      'first_meeting',
      'the_manuscript',
      'blood_pact',
      'idol_under_bridge',
      'varnhem_circle',
      'torso_beacon',
      'grand_ritual_prep',
    ];

    const MAX_TICKS = 400_000; // ~3.7h of real playtime at 30fps — generous upper bound
    for (let i = 0; i < MAX_TICKS && s.phase === 'playing'; i++) {
      s = tick(s);
      // Defend secrecy proactively every tick — reactive defense (only once
      // hemlighet is already low) is too slow against cascading events.
      if (s.hemlighet < 85) s = tryBuy(s, ['exert_influence']);
      if (i % 30 === 0) {
        s = tryBuy(s, storyPath);
        s = tryBuy(s, ['recruit_neighbor']);
        if (s.upgrades.has('grand_ritual_prep')) s = tryBuy(s, ['deepen_ritual']);
      }
    }

    expect(s.phase).toBe('won');
  });
});

describe('neglecting secrecy is punishable', () => {
  it('can lose the game if hemlighet is never defended while growing the cult', () => {
    seedRandom(3);
    let s = createState();

    const MAX_TICKS = 400_000;
    for (let i = 0; i < MAX_TICKS && s.phase === 'playing'; i++) {
      s = tick(s);
      // Grow aggressively but NEVER spend on exert_influence or story upgrades
      // that unlock ritual — just recruit, to stress secrecy drain.
      if (i % 30 === 0) s = tryBuy(s, ['recruit_neighbor']);
    }

    // A cult that only ever recruits and never manages its secrecy should
    // eventually be exposed, not idle forever or win by accident.
    expect(s.phase).toBe('lost');
  });
});

describe('recruit_neighbor cost scaling', () => {
  it('grows monotonically with troende so andakt stays a meaningful sink', () => {
    const recruit = UPGRADES.find(u => u.id === 'recruit_neighbor')!;
    let s = createState();
    let lastCost = 0;
    for (let i = 0; i < 15; i++) {
      const cost = recruit.cost(s).andakt!;
      expect(cost).toBeGreaterThan(lastCost);
      lastCost = cost;
      s = { ...s, troende: s.troende + 1 };
    }
  });
});

describe('deepen_ritual cost scaling', () => {
  it('grows monotonically with each purchase, giving andakt a late-game sink', () => {
    let s = createState();
    s.upgrades.add('grand_ritual_prep');
    s.ritualRate = 0.5; // baseline after torso_beacon (0.15) + grand_ritual_prep (0.35)
    s.andakt = 1_000_000;
    s.dread = 1_000_000;

    const deepen = UPGRADES.find(u => u.id === 'deepen_ritual')!;
    let lastCost = 0;
    for (let i = 0; i < 8; i++) {
      const cost = deepen.cost(s).andakt!;
      expect(cost).toBeGreaterThan(lastCost);
      lastCost = cost;
      s = buyUpgrade(s, 'deepen_ritual');
    }
  });
});

describe('inflytande (influence) accumulation', () => {
  it('scales faster than linearly with troende (a network effect)', () => {
    function inflytandeAfter(troende: number, ticks: number): number {
      let s = createState();
      s.troende = troende;
      s.andaktRate = 0; // isolate influence growth
      for (let i = 0; i < ticks; i++) s = tick(s);
      return s.inflytande;
    }

    const at2 = inflytandeAfter(2, 300);
    const at4 = inflytandeAfter(4, 300);
    const at8 = inflytandeAfter(8, 300);

    // Doubling troende should more than double influence gained (superlinear).
    expect(at4).toBeGreaterThan(at2 * 2);
    expect(at8).toBeGreaterThan(at4 * 2);
  });
});

describe('exert_influence', () => {
  it('restores hemlighet at the cost of inflytande, and refuses purchase when inflytande is insufficient', () => {
    let s = createState();
    s.troende = 2;
    s.hemlighet = 50;
    s.inflytande = 0;

    const blocked = buyUpgrade(s, 'exert_influence');
    expect(blocked.inflytande).toBe(0);
    expect(blocked.hemlighet).toBe(50);

    s.inflytande = 100;
    const after = buyUpgrade(s, 'exert_influence');
    expect(after.hemlighet).toBeGreaterThan(50);
    expect(after.inflytande).toBeLessThan(100);
  });
});

describe('buyUpgrade immutability', () => {
  it('does not mutate the input state', () => {
    const s = createState();
    s.andakt = 1000;
    s.troende = 2;
    const frozenTroende = s.troende;
    const frozenAndakt = s.andakt;
    buyUpgrade(s, 'recruit_neighbor');
    expect(s.troende).toBe(frozenTroende);
    expect(s.andakt).toBe(frozenAndakt);
  });
});

describe('applyOfflineProgress', () => {
  it('is a no-op under the one-minute threshold', () => {
    const s = createState();
    const after = applyOfflineProgress(s, 30_000);
    expect(after.andakt).toBe(s.andakt);
    expect(after.log.length).toBe(s.log.length);
  });

  it('accrues andakt, dread, and inflytande roughly matching the per-second rates', () => {
    let s = createState();
    s.andaktRate = 1;
    s.troende = 2;
    s.dreadRate = 0.5;
    const before = { andakt: s.andakt, dread: s.dread };
    const after = applyOfflineProgress(s, 60 * 60 * 1000); // 1 hour
    expect(after.andakt).toBeCloseTo(before.andakt + 1 * 2 * 3600, 0);
    expect(after.dread).toBeCloseTo(before.dread + 0.5 * 3600, 0);
  });

  it('caps the catch-up window so a long-forgotten tab does not simulate forever', () => {
    let s = createState();
    s.andaktRate = 1;
    s.troende = 1;
    // Seed identically before each call so both draw the same flavor-event
    // sequence — otherwise unseeded event selection alone (not the cap)
    // could account for a small difference in the two totals.
    seedRandom(11);
    const cappedAt24h = applyOfflineProgress(s, 24 * 3600 * 1000);
    seedRandom(11);
    const overAWeek = applyOfflineProgress(s, 30 * 24 * 3600 * 1000);
    expect(overAWeek.andakt).toBeCloseTo(cappedAt24h.andakt, 0);
  });

  it('can end the game in a loss if hemlighet would have run out while away', () => {
    let s = createState();
    s.troende = 50; // heavy secrecy drain
    s.hemlighet = 5;
    // Kept under 2h so no flavor events roll — a lucky +hemlighet event
    // rescuing the cult is legitimate (it mirrors tick()'s own ordering),
    // just not what this test is trying to isolate.
    const after = applyOfflineProgress(s, 3600 * 1000);
    expect(after.phase).toBe('lost');
  });

  it('never mutates the input state', () => {
    const s = createState();
    const frozenAndakt = s.andakt;
    applyOfflineProgress(s, 2 * 3600 * 1000);
    expect(s.andakt).toBe(frozenAndakt);
  });

  it('does nothing outside the "playing" phase', () => {
    const s = createIntroState(); // phase: 'intro'
    const after = applyOfflineProgress(s, 6 * 3600 * 1000);
    expect(after).toBe(s);
  });
});
