import { describe, expect, it } from 'vitest';

import { careerCatalog, resolveCareerChoice, CAREER_FLAGS } from '../../src/events/v2/catalog/career.js';
import {
  isEventEligible,
  selectNextEligibleEvent,
} from '../../src/events/v2/engine/selector.js';
import { applyEventEffects } from '../../src/events/v2/engine/effects.js';
import type { EventDefinition, EventPlayerContext } from '../../src/events/v2/types.js';

function makePlayer(overrides: Partial<EventPlayerContext> = {}): EventPlayerContext {
  return {
    userId: 'player-1',
    c: { ageYears: 30, occupation: 'work', money: 8000, happiness: 50, stress: 20, energy: 100, social: 50 },
    askedQuestions: new Set<string>(),
    eventLastFired: {},
    flags: new Set<string>(),
    ...overrides,
  };
}

function findEvent(id: string): EventDefinition {
  const def = careerCatalog.find((event) => event.id === id);
  expect(def, `career event ${id} should exist`).toBeDefined();
  return def!;
}

describe('events v2 career catalog: structure', () => {
  it('registers a non-trivial set of career events with unique ids and choices', () => {
    expect(careerCatalog.length).toBeGreaterThanOrEqual(10);

    const ids = careerCatalog.map((e) => e.id);
    expect(new Set(ids).size).toBe(ids.length);

    for (const def of careerCatalog) {
      expect(def.category).toBe('career');
      expect(def.id).toMatch(/^[a-zA-Z][a-zA-Z0-9]+$/);
      expect(def.choices.length).toBeGreaterThan(0);
      const choiceIds = def.choices.map((c) => c.choiceId);
      expect(new Set(choiceIds).size).toBe(choiceIds.length);
      for (const c of def.choices) {
        expect(resolveCareerChoice(def.id, c.choiceId)).toBe(c.resolutionText);
      }
    }
  });
});

describe('events v2 career catalog: eligibility gating', () => {
  it('makes the core employed milestones eligible for a working 30-year-old', () => {
    const player = makePlayer();
    for (const id of ['performanceReview', 'promotionOffer', 'gotPassedOver', 'layoffNotice', 'competitorCounterOffer', 'workplaceConflict']) {
      expect(isEventEligible(findEvent(id), player, 1), `${id} eligible`).toBe(true);
    }
  });

  it('gates employed milestones out for the unemployed', () => {
    const player = makePlayer({ c: { ageYears: 30, occupation: 'unemployed', money: 8000 } });
    expect(isEventEligible(findEvent('promotionOffer'), player, 1)).toBe(false);
    expect(isEventEligible(findEvent('layoffNotice'), player, 1)).toBe(false);
  });

  it('respects age windows (promotionOffer needs 22+, gotPassedOver 25+)', () => {
    const young = makePlayer({ c: { ageYears: 20, occupation: 'work', money: 8000 } });
    expect(isEventEligible(findEvent('promotionOffer'), young, 1)).toBe(false);
    expect(isEventEligible(findEvent('gotPassedOver'), young, 1)).toBe(false);
    expect(isEventEligible(findEvent('performanceReview'), young, 1)).toBe(true); // minAge 18
  });

  it('gates startOwnBusiness on having savings (>= 5000)', () => {
    const broke = makePlayer({ c: { ageYears: 30, occupation: 'work', money: 1000 } });
    expect(isEventEligible(findEvent('startOwnBusiness'), broke, 1)).toBe(false);
    const flush = makePlayer({ c: { ageYears: 30, occupation: 'work', money: 9000 } });
    expect(isEventEligible(findEvent('startOwnBusiness'), flush, 1)).toBe(true);
  });
});

describe('events v2 career catalog: repeatable performance review + cooldown', () => {
  it('re-fires only after the ~yearly cooldown elapses', () => {
    const def = findEvent('performanceReview');
    expect(def.repeatable).toBe(true);
    expect(def.cooldownDays).toBe(360);

    const player = makePlayer();
    // First firing: eligible.
    expect(isEventEligible(def, player, 100)).toBe(true);

    // Simulate it firing on day 100 (recorded in eventLastFired) and being answered.
    player.eventLastFired = { performanceReview: 100 };
    (player.askedQuestions as Set<string>).add('performanceReview');

    // Mid-window: still ineligible despite being repeatable.
    expect(isEventEligible(def, player, 300)).toBe(false);
    // After cooldown: eligible again (repeatable skips once-ever gate).
    expect(isEventEligible(def, player, 460)).toBe(true);
  });
});

describe('events v2 career catalog: effects apply', () => {
  it('applies promotion accept effects (happiness + money)', () => {
    const player = makePlayer({ c: { ageYears: 30, occupation: 'work', money: 0, happiness: 50, stress: 20 } });
    const accept = findEvent('promotionOffer').choices.find((c) => c.choiceId === 'accept')!;
    applyEventEffects(player as { c: Record<string, unknown> }, accept.effects);
    expect(player.c.money).toBe(600);
    expect(player.c.happiness).toBe(55);
    expect(player.c.stress).toBe(30);
  });

  it('applies the all-in business cost via moneyCost path', () => {
    const player = makePlayer({ c: { ageYears: 30, occupation: 'work', money: 12000, stress: 10 } });
    const allin = findEvent('startOwnBusiness').choices.find((c) => c.choiceId === 'allin')!;
    // Mirror respond.ts: fold moneyCost into resources.money as a negative.
    applyEventEffects(player as { c: Record<string, unknown> }, {
      ...allin.effects,
      resources: { money: -(allin.moneyCost ?? 0), ...allin.effects?.resources },
    });
    expect(player.c.money).toBe(2000);
    expect(player.c.stress).toBe(30);
  });
});
