/**
 * Regression: tutorial tips must fire exactly ONCE per life, regardless of
 * which caller polls them.
 *
 * The tip generators gate on `!player.askedQuestions.has(fname)`, but
 * historically only ONE caller (stats_manager.checkTutorialEvents) marked the
 * fired id. Any other consumer that polls the generators (HeadlessGame's
 * hourly parseEvents, future parsers) re-fired every tip on every poll — a
 * 6-year headless playthrough produced ~44k repeats of single tips, and the
 * reward-carrying tips (tutorialFirstMilestone +5, tutorialComplete +25)
 * duped diamonds on every fire.
 *
 * Fix: generators self-mark `askedQuestions` at creation, making dedup
 * intrinsic instead of caller-dependent.
 */
import { describe, expect, it } from 'vitest';
import { Person } from '../../src/models/Person.js';
import { Player } from '../../src/models/Player.js';
import {
  firstConversation,
  firstActivityChoice,
  tutorialComplete,
  tutorialEnergyExplained,
  tutorialMoneyExplained,
  tutorialRelationshipExplained,
  tutorialStatsExplained,
  tutorialDiamondsEarned,
  tutorialGameSpeedExplained,
  tutorialSchedulesIntro,
  tutorialEventsIntro,
  tutorialOneTimeEvents,
  tutorialFirstMilestone,
} from '../../src/events/tutorial/onboarding.js';

/** A newborn-in-tutorial player who qualifies for every tip at once. */
function makeTutorialPlayer(): Player {
  const character = new Person({
    id: 'char-tut-1',
    firstname: 'Tut',
    lastname: 'Orial',
    sex: 'Female',
    status: 'alive',
    ageYears: 5,
    ageHours: 24, // tutorialComplete window [24, 25); others need >= 1 or 2
    energy: 10, // tutorialEnergyExplained needs < 20
    diamonds: 5, // tutorialDiamondsEarned needs > 0
  } as never);
  character.schedules = [{ id: 'sched-1' } as never]; // tutorialSchedulesIntro
  character.oneTimeEvents = [{ id: 'ote-1' } as never]; // tutorialOneTimeEvents

  const player = new Player({
    userId: 'tut-player',
    character,
    status: 'playing',
  });
  // tutorialRelationshipExplained needs an NPC with affinity;
  // firstConversation needs a living friendly NPC.
  player.r = [
    new Person({
      id: 'npc-tut-1',
      firstname: 'Pal',
      lastname: 'Friendly',
      sex: 'Male',
      status: 'alive',
      affinity: 50,
    } as never),
  ];
  // tutorialFirstMilestone needs a completed milestone;
  // tutorialEventsIntro needs askedQuestions.size >= 3.
  player.askedQuestions.add('learnedWalk');
  player.askedQuestions.add('learnedBike');
  player.askedQuestions.add('learnedSwim');
  return player;
}

const MESSAGE_TIPS: Array<[string, (player: Player) => unknown]> = [
  ['tutorialComplete', tutorialComplete],
  ['tutorialEnergyExplained', tutorialEnergyExplained],
  ['tutorialMoneyExplained', tutorialMoneyExplained],
  ['tutorialRelationshipExplained', tutorialRelationshipExplained],
  ['tutorialStatsExplained', tutorialStatsExplained],
  ['tutorialDiamondsEarned', tutorialDiamondsEarned],
  ['tutorialGameSpeedExplained', tutorialGameSpeedExplained],
  ['tutorialSchedulesIntro', tutorialSchedulesIntro],
  ['tutorialEventsIntro', tutorialEventsIntro],
  ['tutorialOneTimeEvents', tutorialOneTimeEvents],
  ['tutorialFirstMilestone', tutorialFirstMilestone],
];

describe('tutorial tips fire exactly once (self-marking)', () => {
  it.each(MESSAGE_TIPS)('%s fires once then never again', (fname, generator) => {
    const player = makeTutorialPlayer();

    const first = generator(player);
    expect(first, `${fname} should fire for a qualifying player`).not.toBeNull();
    expect(player.askedQuestions.has(fname)).toBe(true);

    const second = generator(player);
    expect(second, `${fname} re-fired — dedup is caller-dependent again`).toBeNull();
  });

  it('tutorialFirstMilestone awards its 5 diamonds exactly once', () => {
    const player = makeTutorialPlayer();
    const before = player.c.diamonds ?? 0;

    tutorialFirstMilestone(player);
    tutorialFirstMilestone(player);
    tutorialFirstMilestone(player);

    expect(player.c.diamonds).toBe(before + 5);
  });

  it('tutorialComplete awards its 25 diamonds exactly once', () => {
    const player = makeTutorialPlayer();
    const before = player.c.diamonds ?? 0;

    tutorialComplete(player);
    tutorialComplete(player);

    expect(player.c.diamonds).toBe(before + 25);
  });

  it('firstConversation question fires once, and the answer path still works after marking', () => {
    const player = makeTutorialPlayer();
    player.c.ageHours = 5; // question events additionally require tutorial mode (< 24h)

    const question = firstConversation(player, 'question');
    expect(question).not.toBeNull();
    expect(player.askedQuestions.has('firstConversation')).toBe(true);

    // No re-fire while the question is pending.
    expect(firstConversation(player, 'question')).toBeNull();

    // The answer branch is gate-independent and must still resolve.
    const diamondsBefore = player.c.diamonds ?? 0;
    const result = firstConversation(player, 'answer', { option: 'Say hello!' });
    expect(result).not.toBeNull();
    expect(player.c.diamonds).toBe(diamondsBefore + 5);
  });

  it('firstActivityChoice question fires once, and the answer path still works after marking', () => {
    const player = makeTutorialPlayer();
    player.c.ageHours = 5; // question events additionally require tutorial mode (< 24h)

    const question = firstActivityChoice(player, 'question');
    expect(question).not.toBeNull();
    expect(player.askedQuestions.has('firstActivityChoice')).toBe(true);
    expect(firstActivityChoice(player, 'question')).toBeNull();

    const intelligenceBefore = player.c.intelligence ?? 50;
    const result = firstActivityChoice(player, 'answer', { option: 'Study (Gain Intelligence)' });
    expect(result).not.toBeNull();
    expect(player.c.intelligence).toBe(Math.min(100, intelligenceBefore + 10));
  });
});
