/**
 * Regression tests for NPC/player birthday dedup in the OFFLINE path:
 * GameEngine.updateAge(), driven every 60s by the `iterate_offline_games`
 * background job (LoopManager.initLifeSim -> engine.updateAge).
 *
 * Bug: the offline emitter had no `player.events` guard and keyed the NPC
 * birthday on the mutable, non-unique `firstname`. Unlike PlayerSession, the
 * offline loop does NOT record the returned id in player.events, so a
 * re-evaluation of the same birthday (e.g. a stale reload by the 60s job)
 * re-delivered it. The dedup must therefore live inside updateAge itself, and
 * must share its key namespace with the online path (stats_manager.updateAge).
 */
import { describe, it, expect } from 'vitest';
import { Person } from '../../src/models/Person.js';
import { Player } from '../../src/models/Player.js';
import { getGameEngine } from '../../src/game/engine/GameEngine.js';

const engine = getGameEngine();

/** A titled, alive NPC poised one daily tick away from a yearly birthday. */
function makeRelative(
  overrides: Partial<{ id: string; firstname: string; ageYears: number; title: string }> = {},
): Person {
  const ageYears = overrides.ageYears ?? 30;
  const person = new Person({
    id: overrides.id ?? 'npc-1',
    firstname: overrides.firstname ?? 'Mom',
    lastname: 'Doe',
    sex: 'female',
    status: 'alive',
    relationships: [overrides.title ?? 'mother'],
    ageYears,
    ageDays: 364, // +1 daily tick -> a multiple of 365 -> birthday
    affinity: 80,
    deathChance: 0,
    health: 0, // deathChance * health == 0 -> cannot randomly die mid-test
  } as never);
  (person as unknown as { title?: string }).title = overrides.title ?? 'mother';
  return person;
}

/** Player poised so a single updateAge() runs the daily block but is NOT itself
 * having a birthday (ageDays + 1 is not a multiple of 365). */
function makePlayer(relatives: Person[]): Player {
  const player = new Player({
    userId: 'player-1',
    character: new Person({
      id: 'me',
      firstname: 'You',
      lastname: 'Me',
      sex: 'Male',
      ageYears: 20,
      ageDays: 7300,
      status: 'alive',
    } as never),
    status: 'playing',
    controller: 'active',
    r: relatives,
  });
  player.c.ageHours = 23; // +1 -> 24 -> daily block runs
  return player;
}

/** Re-arm the daily block + NPC birthday boundary to simulate the offline job
 * re-processing the SAME birthday from a stale reload. */
function rearmSameBirthday(player: Player, npc: Person): void {
  player.c.ageHours = 23;
  player.c.ageDays = 7300;
  npc.ageDays = 364;
  npc.ageYears = 30;
}

describe('GameEngine.updateAge — offline birthday dedup', () => {
  it('fires an NPC birthday and records the dedup key inside updateAge', () => {
    const npc = makeRelative({ id: 'npc-1', firstname: 'Mom' });
    const player = makePlayer([npc]);

    const event = engine.updateAge(player);

    expect(event?.id).toBe('birthday_npc_npc-1_31');
    // Critical: updateAge itself records the key (the offline caller does not).
    expect(player.events.has('birthday_npc_npc-1_31')).toBe(true);
  });

  it('does not re-deliver a birthday already recorded in player.events', () => {
    const npc = makeRelative({ id: 'npc-1', firstname: 'Mom' });
    const player = makePlayer([npc]);

    const first = engine.updateAge(player);
    expect(first?.id).toBe('birthday_npc_npc-1_31');

    // Stale reload: the 60s offline job re-evaluates the same birthday day.
    rearmSameBirthday(player, npc);
    const second = engine.updateAge(player);

    expect(second).toBeNull();
  });

  it('survives a save/load: the dedup key round-trips and suppresses re-fire', () => {
    const npc = makeRelative({ id: 'npc-1', firstname: 'Mom' });
    let player = makePlayer([npc]);

    expect(engine.updateAge(player)?.id).toBe('birthday_npc_npc-1_31');

    // Serialize/rebuild as the offline queue does between passes.
    player = new Player(JSON.parse(JSON.stringify(player.toJSON())));
    // person.title is NOT serialized by Person.toJSON(); re-apply it so the
    // post-reload birthday block actually runs. Otherwise this assertion would
    // pass simply because the reloaded NPC has no title (block skipped),
    // masking whether the player.events dedup guard is what suppresses re-fire.
    (player.r[0] as unknown as { title?: string }).title = 'mother';
    expect(player.events.has('birthday_npc_npc-1_31')).toBe(true);

    rearmSameBirthday(player, player.r[0]);
    expect(engine.updateAge(player)).toBeNull();
  });

  it('keys on stable id so two same-named NPCs each fire once', () => {
    const mom = makeRelative({ id: 'mom', firstname: 'Sam', title: 'mother' });
    const dad = makeRelative({ id: 'dad', firstname: 'Sam', title: 'father' });
    const player = makePlayer([mom, dad]);

    // updateAge returns at the first birthday in the loop (Mom).
    const first = engine.updateAge(player);
    expect(first?.id).toBe('birthday_npc_mom_31');
    // Dad's key is distinct (id-based, not 'birthday_Sam_31'), so Mom's firing
    // does not suppress him.
    expect(player.events.has('birthday_npc_dad_31')).toBe(false);

    // Next daily tick processes Dad, who still fires his own birthday.
    player.c.ageHours = 23;
    const second = engine.updateAge(player);
    expect(second?.id).toBe('birthday_npc_dad_31');
  });
});
