/**
 * performActivity handler tests
 *
 * Verifies the player-initiated activity command:
 *  - applies the expected stat/energy/money deltas
 *  - rejects when energy is insufficient
 *  - enforces age-appropriateness
 *  - requires a free daily-plan slot in immediate mode
 *  - queues an evening override in override mode (persists via plannedActivity)
 *  - is registered/dispatchable through the COMMAND_REGISTRY
 */
import { describe, it, expect, beforeEach } from 'vitest';
import { Person } from '../../src/models/Person';
import { Player } from '../../src/models/Player';
import { handlePerformActivity } from '../../src/handlers/performActivity';
import { COMMAND_REGISTRY } from '../../src/handlers/index';
import { PLAYER_ACTIVITIES } from '../../src/game/engine/intradayActivity';

class MockPlayerSession {
  player: Player;
  sentMessages: any[] = [];
  saveCount = 0;

  constructor(player: Player) {
    this.player = player;
  }

  send(message: any) {
    this.sentMessages.push(message);
  }

  sendPlayerObject() {
    this.sentMessages.push({ type: 'playerObject', player: this.player });
  }

  async savePlayer() {
    this.saveCount += 1;
    return undefined;
  }

  getLastMessage() {
    return this.sentMessages[this.sentMessages.length - 1];
  }

  byType(type: string) {
    return this.sentMessages.filter((m) => m?.type === type);
  }
}

function makeSession(overrides: Partial<ConstructorParameters<typeof Person>[0]> = {}) {
  const character = new Person({
    id: 'char-1',
    firstname: 'Test',
    lastname: 'Player',
    sex: 'Male',
    ageYears: 25,
    money: 1000,
    energy: 80,
    health: 90,
    happiness: 70,
    social: 50,
    intelligence: 50,
    creativity: 50,
    stress: 10,
    affinity: 50,
    // A daily plan with a free evening slot the player can claim.
    dailyPlan: [
      { time: 8, location: 'work', title: 'You arrive at work', name: 'work' },
      { time: 20, location: 'home', title: 'You relax at home', name: 'home' },
    ] as any,
    ...overrides,
  });

  const player = new Player({
    userId: 'user-1',
    character,
    r: [],
    status: 'playing',
    date: '2024-06-15',
    hourOfDay: 10,
    minuteOfHour: 0,
  });

  return new MockPlayerSession(player);
}

describe('performActivity handler', () => {
  let session: MockPlayerSession;

  beforeEach(() => {
    session = makeSession();
  });

  it('is registered in the command registry', () => {
    expect(COMMAND_REGISTRY['performActivity']).toBe(handlePerformActivity);
  });

  it('applies energy cost and skill-scaled stat gains for a study activity', async () => {
    // T006: intelligence is no longer a flat +3. At skill 0 (novice, 1.6x) on a
    // "normal" outcome (1.0x) the +3 base scales to round(3 * 1.6) = 5. The energy
    // cost stays flat (predictable energy gate). A fixed RNG keeps this deterministic.
    const startEnergy = session.player.c.energy;
    const startInt = session.player.c.intelligence;
    const def = PLAYER_ACTIVITIES.study;

    // rng = 0.5 -> normal outcome tier (0.15 <= 0.5 < 0.85).
    await handlePerformActivity({ activityId: 'study' }, session as any, () => 0.5);

    expect(session.player.c.energy).toBe(startEnergy - def.energyCost);
    // round(3 * 1.6 * 1.0) = 5 (a real progression gain, not the old flat +3).
    expect(session.player.c.intelligence).toBe(startInt + 5);

    const performed = session.byType('activityPerformed');
    expect(performed.length).toBe(1);
    expect(performed[0].activityId).toBe('study');
    expect(performed[0].outcomeTier).toBe('normal');
    expect(performed[0].skillLevel).toBe(1); // skill incremented from 0 -> 1
    expect(performed[0].deltas.intelligence).toBe(5);
    expect(session.saveCount).toBe(1);
  });

  it('applies money gain for the side-hustle activity', async () => {
    // sideHustle base money is 40; at skill 0 (1.6x) normal (1.0x) -> round(64) = 64.
    const startMoney = session.player.c.money;

    await handlePerformActivity({ activityId: 'sideHustle' }, session as any, () => 0.5);

    expect(session.player.c.money).toBe(startMoney + 64);
    expect(session.byType('activityPerformed').length).toBe(1);
  });

  it('rewrites the chosen free daily-plan slot to the activity', async () => {
    await handlePerformActivity({ activityId: 'exercise' }, session as any);
    const slot = session.player.c.dailyPlan.find((e: any) => e.time === 20) as any;
    expect(slot.name).toBe(PLAYER_ACTIVITIES.exercise.name);
    expect(slot.title).toBe(PLAYER_ACTIVITIES.exercise.title);
  });

  it('rejects the activity when energy is insufficient', async () => {
    session.player.c.energy = 2; // below every activity's energyCost
    await handlePerformActivity({ activityId: 'exercise' }, session as any);

    const last = session.getLastMessage();
    expect(last.type).toBe('error');
    expect(last.message).toMatch(/energy/i);
    expect(session.byType('activityPerformed').length).toBe(0);
    // Energy unchanged on rejection.
    expect(session.player.c.energy).toBe(2);
  });

  it('rejects an unknown activity id', async () => {
    await handlePerformActivity({ activityId: 'nope' }, session as any);
    const last = session.getLastMessage();
    expect(last.type).toBe('error');
    expect(last.availableActivities).toContain('study');
  });

  it('enforces age-appropriateness for the side-hustle', async () => {
    const young = makeSession({ ageYears: 8 });
    await handlePerformActivity({ activityId: 'sideHustle' }, young as any);
    const last = young.getLastMessage();
    expect(last.type).toBe('error');
    expect(young.byType('activityPerformed').length).toBe(0);
  });

  it('rejects when there is no free slot at or after the current hour', async () => {
    // Only a past free slot and a future fixed obligation remain.
    const noSlot = makeSession({
      dailyPlan: [
        { time: 7, location: 'home', title: 'You relax at home', name: 'home' },
        { time: 22, location: 'home', title: 'You go to bed', name: 'bed' },
      ] as any,
    });
    await handlePerformActivity({ activityId: 'hobby' }, noSlot as any);
    const last = noSlot.getLastMessage();
    expect(last.type).toBe('error');
    expect(last.message).toMatch(/no free time slot/i);
  });

  it('queues an evening override and persists it on plannedActivity', async () => {
    await handlePerformActivity({ activityId: 'hobby', override: true }, session as any);

    expect(session.player.c.plannedActivity).toBe('hobby');
    const planned = session.byType('activityPlanned');
    expect(planned.length).toBe(1);
    expect(planned[0].activityId).toBe('hobby');
    expect(session.saveCount).toBe(1);
    // Override mode does NOT apply effects immediately.
    expect(session.player.c.energy).toBe(80);
  });
});
