/**
 * Intelligence-driven promotion tests (T007).
 *
 * Before T007, promotion performance was random + focus-only and read NO stat,
 * so the intelligence trajectory built by Wave 1's performActivity never paid
 * off. Two mechanisms now make intelligence MATTER at the promotion lever:
 *
 *   1. PERF BONUS — at/above INTELLIGENCE_PERF_BONUS_THRESHOLD the per-tick
 *      performance roll range shifts up by +1 on both ends, so a sharp worker
 *      climbs toward the >90 ceiling faster/more reliably.
 *   2. TOP-TIER FLOOR — the single top rung of an elite (prestige-gated TOP)
 *      ladder also requires INTELLIGENCE_GATE_TOP. Gates only the very top of an
 *      elite career — never a milestone or an ordinary career.
 *
 * RNG is injected into handleJob so the random roll is deterministic here.
 */
import { describe, it, expect, beforeEach } from 'vitest';
import { Person } from '../../../src/models/Person.js';
import { Player } from '../../../src/models/Player.js';
import {
  getOccupationByTitle,
  setJob,
  handleJob,
  getJobRecord,
  clearJobCaches,
  PRESTIGE_GATE_TOP,
  INTELLIGENCE_PERF_BONUS_THRESHOLD,
  INTELLIGENCE_GATE_TOP,
  type JobRecord,
} from '../../../src/services/jobs/job_manager.js';

function makePlayer(opts: { intelligence: number; prestige?: number }): Player {
  const character = new Person({
    id: 'char-1',
    firstname: 'Test',
    lastname: 'Player',
    ageYears: 35,
    education: 'doctorate_degree',
    intelligence: opts.intelligence,
    prestige: opts.prestige ?? 0,
  });
  return new Player({
    userId: 'user-1',
    character,
    r: [],
    status: 'playing',
    date: '2024-06-15',
    hourOfDay: 10,
    minuteOfHour: 30,
  });
}

describe('Intelligence performance bonus', () => {
  beforeEach(() => clearJobCaches());

  // With the SAME seeded RNG (rng() === 0 -> lowest roll) and the SAME focus,
  // a high-intelligence worker's performance update must be measurably higher
  // than a low-intelligence worker's. This is the "smart people climb faster"
  // bias made concrete and deterministic.
  it('gives a high-intelligence worker a higher performance update than a low-intelligence one', () => {
    const lowRng = () => 0; // always the minimum of the (shifted) range

    const smart = makePlayer({ intelligence: INTELLIGENCE_PERF_BONUS_THRESHOLD + 20 });
    const swe = getOccupationByTitle('Software Engineer')!;
    setJob(smart.c, swe, smart.date);
    getJobRecord(smart.c)!.performance = 50;
    handleJob(smart, smart.c, lowRng);
    const smartPerf = getJobRecord(smart.c)!.performance;

    const dull = makePlayer({ intelligence: INTELLIGENCE_PERF_BONUS_THRESHOLD - 20 });
    setJob(dull.c, swe, dull.date);
    getJobRecord(dull.c)!.performance = 50;
    handleJob(dull, dull.c, lowRng);
    const dullPerf = getJobRecord(dull.c)!.performance;

    // Both rolled the minimum, but the smart worker's range is shifted +1.
    expect(smartPerf).toBeGreaterThan(dullPerf);
    expect(smartPerf - dullPerf).toBe(1);
  });

  // Concretely: at the ceiling-adjacent performance with the lowest roll, a
  // high-intelligence worker still tips OVER the >90 promotion threshold while a
  // low-intelligence worker stalls below it. Same RNG, same focus, opposite
  // outcome — proving intelligence affects whether a promotion fires.
  //
  // Balanced focus + rng()===0 (minimum roll): smart range [0,2] -> +0, dull
  // range [-1,1] -> -1. Starting at performance 91, the smart worker lands at 91
  // (>90, promoted) while the dull worker drops to 90 (not >90, stalls).
  it('lets a smart worker promote on a low roll where a dull worker stalls', () => {
    const lowRng = () => 0;
    const swe = getOccupationByTitle('Software Engineer')!;

    const smart = makePlayer({ intelligence: INTELLIGENCE_PERF_BONUS_THRESHOLD + 10 });
    setJob(smart.c, swe, smart.date);
    getJobRecord(smart.c)!.level = swe.levels[0]; // entry -> 2nd, ungated tier
    getJobRecord(smart.c)!.performance = 91; // one tick from promotion
    const smartResult = handleJob(smart, smart.c, lowRng);

    const dull = makePlayer({ intelligence: INTELLIGENCE_PERF_BONUS_THRESHOLD - 10 });
    setJob(dull.c, swe, dull.date);
    getJobRecord(dull.c)!.level = swe.levels[0];
    getJobRecord(dull.c)!.performance = 91;
    const dullResult = handleJob(dull, dull.c, lowRng);

    expect(smartResult?.promoted).toBe(true);
    expect(dullResult?.promoted).toBe(false);
  });
});

describe('Intelligence top-tier floor on elite ladders', () => {
  beforeEach(() => clearJobCaches());

  // Player has enough prestige to clear the prestige gate, performance is
  // promotion-eligible, but intelligence is below the floor -> held, not promoted.
  it('blocks the top elite rung when intelligence is below the floor', () => {
    const player = makePlayer({
      intelligence: INTELLIGENCE_GATE_TOP - 1,
      prestige: PRESTIGE_GATE_TOP, // clears the prestige gate
    });
    const lawyer = getOccupationByTitle('Lawyer')!;
    setJob(player.c, lawyer, player.date);

    const record = getJobRecord(player.c) as JobRecord;
    // Senior Partner (index len-2): NEXT promotion is the TOP rung (Managing Partner).
    record.level = lawyer.levels[lawyer.levels.length - 2];
    record.performance = 95;

    const result = handleJob(player, player.c, () => 0);
    expect(result?.promoted).toBe(false);
    expect(getJobRecord(player.c)!.performance).toBe(90); // held at ceiling, not fired
    expect(getJobRecord(player.c)!.level.level).toBe(
      lawyer.levels[lawyer.levels.length - 2].level
    );
    // A one-time intelligence-gate message should have been queued.
    expect(player.messageQueue.some((m) => /intelligence/i.test(m))).toBe(true);
  });

  it('allows the top elite rung once intelligence meets the floor', () => {
    const player = makePlayer({
      intelligence: INTELLIGENCE_GATE_TOP,
      prestige: PRESTIGE_GATE_TOP,
    });
    const lawyer = getOccupationByTitle('Lawyer')!;
    setJob(player.c, lawyer, player.date);

    const record = getJobRecord(player.c) as JobRecord;
    record.level = lawyer.levels[lawyer.levels.length - 2];
    record.performance = 95;

    const result = handleJob(player, player.c, () => 0);
    expect(result?.promoted).toBe(true);
    expect(getJobRecord(player.c)!.level.level).toBe(
      lawyer.levels[lawyer.levels.length - 1].level
    );
  });

  // Guard: the floor must NOT touch ordinary careers — they have no top-tier
  // intelligence requirement, so a dull worker still climbs an ungated ladder.
  it('does not gate ordinary (non-elite) careers on intelligence', () => {
    const player = makePlayer({ intelligence: 10, prestige: 0 });
    const janitor = getOccupationByTitle('Janitor')!;
    setJob(player.c, janitor, player.date);

    const record = getJobRecord(player.c) as JobRecord;
    record.level = janitor.levels[0];
    record.performance = 95;

    const result = handleJob(player, player.c, () => 0);
    expect(result?.promoted).toBe(true);
  });
});
