/**
 * Proof-it-fires regression test for the `straight_a` achievement
 * ("Graduate with a 4.0 GPA").
 *
 * Bug (T003, baolife-release-hardening): the graduating student's GPA was
 * dropped through the entire graduation lifecycle chain, so `straight_a` could
 * never unlock. The fix threads the GPA from the education record:
 *   schoolMilestones.graduationDay  (reads current_education.gpa, 0-100 scale,
 *     converts to 0-4.0 scale at the source) -> lifecycleQueue.data.gpa
 *   -> PlayerSession dispatch: onGraduation(session, level, event.data?.gpa)
 *   -> integration.onGraduation forwards gpa into checkAchievementsAsync
 *   -> achievements.checkAchievements: `straight_a` unlocks when gpa >= 4.0
 *
 * Scale reconciliation: GPA lives on a 0-100 scale on the education record
 * (default 50, clamped to 100 in education_manager.handleEducation). We convert
 * to the 0-4.0 scale AT THE SOURCE (graduationDay), so the achievement's
 * `>= 4.0` check stays literally correct: a true straight-A graduate has the
 * max GPA (100/100 => 4.0), while a mid-GPA graduate (75/100 => 3.0) does not.
 *
 * This test exercises the REAL source code at both load-bearing hops:
 *   1. graduationDay computes and queues the correctly-scaled gpa.
 *   2. checkAchievementsAsync (the exact call PlayerSession makes via
 *      onGraduation) unlocks `straight_a` only for the 4.0 graduate.
 *
 * It FAILS if gpa is dropped anywhere in the chain (queued data missing gpa =>
 * positive case stops unlocking) OR if the scale is mishandled (negative case
 * would wrongly unlock for every graduate).
 *
 * Online-only: this is the connected-player lifecycle path (PlayerSession ->
 * lifecycleQueue). The offline LoopManager/GameEngine path does not run this
 * fast-mode school-milestone event catalog, so there is no offline mirror.
 */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

import { Player } from '../../../src/models/Player.js';
import { Person } from '../../../src/models/Person.js';
import { graduationDay } from '../../../src/events/education/schoolMilestones.js';
import * as eventBase from '../../../src/events/base.js';
import {
  checkAchievementsAsync,
  hasUnlockedAchievement,
  clearAllAchievements,
} from '../../../src/services/retention/achievements.js';

/**
 * Build a graduating high-school student whose education record carries the
 * given GPA on the native 0-100 scale.
 */
function makeGraduatingPlayer(userId: string, gpa100: number): Player {
  const character = new Person({
    id: `char-${userId}`,
    firstname: 'Grad',
    ageYears: 17,
    occupation: 'student',
  });
  // current_education is the record graduationDay reads the GPA off of.
  character.current_education = {
    id: 'edu-hs',
    educationLevel: 'High School',
    gpa: gpa100,
    focus: 'Work Hard',
  };

  const player = new Player({
    userId,
    character,
    monthOfYear: 6, // graduation month gate
    status: 'playing',
  });
  return player;
}

describe('straight_a achievement (graduation GPA threading)', () => {
  beforeEach(() => {
    clearAllAchievements();
    // Force graduationDay's probability gate so the lifecycle event always queues.
    vi.spyOn(eventBase, 'checkProbability').mockReturnValue(true);
  });

  afterEach(() => {
    vi.restoreAllMocks();
    clearAllAchievements();
  });

  it('SOURCE: graduationDay queues the gpa on the 0-4.0 scale (100 => 4.0)', () => {
    const player = makeGraduatingPlayer('grad-a', 100);

    graduationDay(player);

    const event = player.lifecycleQueue.find((e) => e.type === 'graduation');
    expect(event, 'graduation lifecycle event should be queued').toBeDefined();
    expect(event!.data?.level).toBe('high school');
    // 100/100 * 4.0 === 4.0 — the gpa MUST be present and correctly scaled.
    expect(event!.data?.gpa).toBe(4.0);
  });

  it('POSITIVE: a 4.0 (GPA 100) graduate unlocks straight_a through the real chain', async () => {
    const playerId = 'grad-positive';
    const player = makeGraduatingPlayer(playerId, 100);

    // Hop 1: source queues the lifecycle event with the scaled gpa.
    graduationDay(player);
    const event = player.lifecycleQueue.find((e) => e.type === 'graduation')!;

    // Hop 2: replicate PlayerSession's exact dispatch payload. PlayerSession
    // calls onGraduation(this, level, event.data?.gpa); integration.onGraduation
    // forwards { level, gpa } into checkAchievementsAsync('graduate', ...).
    const level = (event.data?.level as string) ?? 'high school';
    const gpa = event.data?.gpa as number | undefined;
    const unlocked = await checkAchievementsAsync(
      player.userId,
      'graduate',
      { level, gpa },
    );

    expect(hasUnlockedAchievement(playerId, 'straight_a')).toBe(true);
    expect(unlocked.map((u) => u.key)).toContain('straight_a');
    // The HS graduation achievement should also unlock alongside it.
    expect(hasUnlockedAchievement(playerId, 'graduate_hs')).toBe(true);
  });

  it('NEGATIVE: a mid-GPA graduate (GPA 75 => 3.0) does NOT unlock straight_a', async () => {
    const playerId = 'grad-negative';
    const player = makeGraduatingPlayer(playerId, 75);

    graduationDay(player);
    const event = player.lifecycleQueue.find((e) => e.type === 'graduation')!;

    // 75/100 * 4.0 === 3.0 — below the 4.0 threshold.
    expect(event.data?.gpa).toBe(3.0);

    const level = (event.data?.level as string) ?? 'high school';
    const gpa = event.data?.gpa as number | undefined;
    await checkAchievementsAsync(player.userId, 'graduate', { level, gpa });

    // Catches the "unlocks for everyone" regression if the raw 0-100 GPA were
    // threaded against a `>= 4.0` check, or if the scale were otherwise broken.
    expect(hasUnlockedAchievement(playerId, 'straight_a')).toBe(false);
    // The graduate still earns the plain HS graduation achievement.
    expect(hasUnlockedAchievement(playerId, 'graduate_hs')).toBe(true);
  });

  it('REGRESSION GUARD: a dropped gpa (undefined in event data) does NOT unlock straight_a', async () => {
    // Simulates the original bug where data.gpa never made it to the achievement
    // check — confirms the test would have caught the dropped-gpa state.
    const playerId = 'grad-dropped';
    await checkAchievementsAsync(playerId, 'graduate', { level: 'high school' });

    expect(hasUnlockedAchievement(playerId, 'straight_a')).toBe(false);
    expect(hasUnlockedAchievement(playerId, 'graduate_hs')).toBe(true);
  });
});
