/**
 * Regression tests for relationship (NPC) and player birthday dedup in
 * stats_manager.updateAge().
 *
 * Bug: a single NPC's birthday "messageEvent" was delivered many times. The
 * birthday emission relied solely on the `ageDays % 365 === 0` guard and was
 * NOT recorded in `player.events`; the dedup `.add()` lived only in the caller
 * (PlayerSession.processHourTick), and the id was keyed on the NPC's mutable,
 * non-unique `firstname`. These tests pin the contract: each birthday fires
 * exactly once per person, per year, surviving (a) the ~24 hour ticks within
 * the birthday day and (b) a save/load cycle.
 */
import { describe, it, expect } from 'vitest';
import { Person } from '../../src/models/Person.js';
import { Player } from '../../src/models/Player.js';
import { updateAge } from '../../src/stats/stats_manager.js';

/** Build a titled, alive NPC whose birthday lands one daily-tick from now. */
function makeRelative(
  overrides: Partial<{ id: string; firstname: string; ageYears: number; title: string }> = {},
): Person {
  const ageYears = overrides.ageYears ?? 45;
  const person = new Person({
    id: overrides.id ?? 'rel-1',
    firstname: overrides.firstname ?? 'Jane',
    lastname: 'Doe',
    sex: 'female',
    status: 'alive',
    relationships: [overrides.title ?? 'mother'],
    ageYears,
    // 364 days into the year -> the next daily tick rolls over to a multiple of 365.
    ageDays: ageYears * 365 + 364,
    affinity: 80,
    deathChance: 0,
    health: 0, // health * deathChance == 0 -> NPC cannot randomly die during the test
  } as never);
  (person as unknown as { title?: string }).title = overrides.title ?? 'mother';
  return person;
}

function makePlayer(relatives: Person[]): Player {
  const player = new Player({
    userId: 'player-1',
    character: new Person({
      id: 'me',
      firstname: 'You',
      lastname: 'Me',
      sex: 'Male',
      ageYears: 25,
      ageDays: 25 * 365 + 100, // not a birthday during the window
      status: 'alive',
    } as never),
    status: 'playing',
    controller: 'active',
    r: relatives,
  });
  // Align ageHours so the daily tick lands within the first 24 hourly ticks.
  player.c.ageHours = player.c.ageDays * 24;
  return player;
}

interface SentEvent {
  id: string;
  message: string;
}

/**
 * Replicate PlayerSession.processHourTick's exact handling of the updateAge
 * result (it adds the returned id to player.events and "sends" the event).
 */
function tickOneHour(player: Player, sent: SentEvent[]): void {
  const result = updateAge(player);
  if (result && 'id' in result && typeof result.id === 'string') {
    player.events.add(result.id);
    sent.push({ id: result.id, message: (result as { message?: string }).message ?? '' });
  }
}

/**
 * Match birthday events for a named person via the message text (the message
 * still names the person; the id is keyed on the stable person id, not the
 * firstname, so we deliberately don't match on the id here).
 */
function npcBirthdaysFor(sent: SentEvent[], firstname: string): SentEvent[] {
  return sent.filter(
    (e) => e.id.startsWith('birthday_npc_') && e.message.includes(`${firstname} `),
  );
}

describe('stats_manager.updateAge — birthday dedup', () => {
  it('fires an NPC birthday exactly once across the full birthday day (24 hour ticks)', () => {
    const player = makePlayer([makeRelative({ firstname: 'Jane' })]);
    const sent: SentEvent[] = [];

    // Tick a few full days hour-by-hour straddling the birthday.
    for (let hour = 0; hour < 72; hour++) {
      tickOneHour(player, sent);
    }

    const janeBirthdays = npcBirthdaysFor(sent, 'Jane');
    expect(janeBirthdays).toHaveLength(1);
    expect(player.r[0].ageYears).toBe(46);
  });

  it('fires an NPC birthday exactly once even with a save/load mid birthday-day', () => {
    let player = makePlayer([makeRelative({ firstname: 'Jane' })]);
    const sent: SentEvent[] = [];

    // Tick until the birthday has fired once.
    let firedHour = -1;
    for (let hour = 0; hour < 48; hour++) {
      const before = sent.length;
      tickOneHour(player, sent);
      if (sent.length > before) {
        firedHour = hour;
        break;
      }
    }
    expect(firedHour).toBeGreaterThanOrEqual(0);
    expect(npcBirthdaysFor(sent, 'Jane')).toHaveLength(1);

    // Simulate a disconnect/reconnect: serialize and rebuild the Player.
    player = new Player(JSON.parse(JSON.stringify(player.toJSON())));
    expect(player.events.size).toBeGreaterThan(0); // dedup set must round-trip

    // Keep ticking through the rest of the day and into following days.
    for (let hour = 0; hour < 72; hour++) {
      tickOneHour(player, sent);
    }

    // Still exactly one — the save/load did not forget the dedup.
    expect(npcBirthdaysFor(sent, 'Jane')).toHaveLength(1);
  });

  it('does not let two same-named NPCs suppress each other (keyed on stable id)', () => {
    const player = makePlayer([
      makeRelative({ id: 'mom', firstname: 'Sam', title: 'mother' }),
      makeRelative({ id: 'dad', firstname: 'Sam', title: 'father' }),
    ]);
    const sent: SentEvent[] = [];

    for (let hour = 0; hour < 72; hour++) {
      tickOneHour(player, sent);
    }

    // Both relatives named "Sam" must each get their own birthday event.
    const samBirthdays = npcBirthdaysFor(sent, 'Sam');
    expect(samBirthdays).toHaveLength(2);
    expect(new Set(samBirthdays.map((e) => e.id)).size).toBe(2); // distinct ids
    expect(player.r[0].ageYears).toBe(46);
    expect(player.r[1].ageYears).toBe(46);
  });

  it("fires the player's own birthday exactly once and keeps the birthday_<ageYears> id", () => {
    const player = makePlayer([]);
    // Put the PLAYER one daily-tick from a birthday.
    player.c.ageYears = 30;
    player.c.ageDays = 30 * 365 + 364;
    player.c.ageHours = player.c.ageDays * 24;
    const sent: SentEvent[] = [];

    for (let hour = 0; hour < 72; hour++) {
      tickOneHour(player, sent);
    }

    const playerBirthdays = sent.filter((e) => e.id === 'birthday_31');
    expect(playerBirthdays).toHaveLength(1);
    expect(player.c.ageYears).toBe(31);
  });
});
