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

import {
  childhoodCatalog,
  resolveChildhoodChoice,
} from '../../src/events/v2/catalog/childhood.js';
import { eventCatalog } from '../../src/events/v2/catalog/index.js';
import { createEventRegistry } from '../../src/events/v2/registry.js';
import { isEventEligible } from '../../src/events/v2/engine/selector.js';
import type { EventDefinition, EventPlayerContext } from '../../src/events/v2/types.js';

function makeChild(ageYears: number, overrides: Partial<EventPlayerContext> = {}): EventPlayerContext {
  return {
    userId: 'child-1',
    c: { ageYears, happiness: 50, intelligence: 50, social: 50, creativity: 50, health: 80, stress: 10 },
    askedQuestions: new Set<string>(),
    eventLastFired: {},
    flags: new Set<string>(),
    ...overrides,
  };
}

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

// Stats every childhood effect is allowed to touch (Person roster; NO `looks`).
const ALLOWED_STATS = new Set([
  'intelligence',
  'social',
  'creativity',
  'happiness',
  'health',
  'stress',
  'prestige',
]);

describe('events v2 childhood catalog: structure', () => {
  it('registers a non-trivial set of unique, well-formed childhood events', () => {
    expect(childhoodCatalog.length).toBeGreaterThanOrEqual(8);

    const ids = childhoodCatalog.map((e) => e.id);
    // ids are unique within the catalog...
    expect(new Set(ids).size).toBe(ids.length);
    // ...and all namespaced under `childhood.`
    for (const id of ids) {
      expect(id.startsWith('childhood.')).toBe(true);
    }

    for (const def of childhoodCatalog) {
      // reuses an existing EventCategory bucket (family), not an invented type
      expect(def.category).toBe('family');
      expect(def.choices.length).toBeGreaterThan(0);

      const choiceIds = def.choices.map((c) => c.choiceId);
      expect(new Set(choiceIds).size).toBe(choiceIds.length);

      // every event stays inside the childhood window (capped by maxAge <= 9)
      expect(def.maxAge ?? Infinity).toBeLessThanOrEqual(9);
      expect(def.minAge ?? 0).toBeGreaterThanOrEqual(2);

      for (const c of def.choices) {
        // static resolutionText round-trips through the resolver
        expect(c.resolutionText, `${def.id}/${c.choiceId} has resolutionText`).toBeTruthy();
        expect(resolveChildhoodChoice(def.id, c.choiceId)).toBe(c.resolutionText);

        // effects are well-formed: only allowed stats, finite numbers
        const stats = c.effects?.stats;
        if (stats) {
          for (const [key, value] of Object.entries(stats)) {
            expect(ALLOWED_STATS.has(key), `${def.id}/${c.choiceId} stat ${key} is valid`).toBe(true);
            expect(Number.isFinite(value)).toBe(true);
            // childhood deltas are deliberately small/age-appropriate
            expect(Math.abs(value)).toBeLessThanOrEqual(5);
          }
        }
      }
    }
  });

  it('is wired into the global event catalog with globally unique ids', () => {
    for (const def of childhoodCatalog) {
      expect(eventCatalog.some((e) => e.id === def.id)).toBe(true);
    }
    // The registry throws on duplicate ids — building it proves global uniqueness.
    expect(() => createEventRegistry(eventCatalog)).not.toThrow();
    const ids = eventCatalog.map((e) => e.id);
    expect(new Set(ids).size).toBe(ids.length);
  });

  it('includes both passive milestones and interactive choices', () => {
    const passive = childhoodCatalog.filter((e) => e.kind === 'passive');
    const interactive = childhoodCatalog.filter((e) => e.kind !== 'passive');
    expect(passive.length).toBeGreaterThan(0);
    expect(interactive.length).toBeGreaterThan(0);
    // passive milestones auto-resolve via a single choice
    for (const def of passive) {
      expect(def.choices.length).toBe(1);
    }
    // interactive beats offer a real choice
    for (const def of interactive) {
      expect(def.choices.length).toBeGreaterThanOrEqual(2);
    }
  });
});

describe('events v2 childhood catalog: eligible at young ages', () => {
  it('makes at least one event reachable BEFORE age 5 (minAge < 5)', () => {
    const earlyEvents = childhoodCatalog.filter((e) => (e.minAge ?? 0) < 5);
    expect(earlyEvents.length).toBeGreaterThan(0);

    // A 2-year-old is eligible for the toddler beats.
    const toddler = makeChild(2);
    expect(isEventEligible(findChildhood('childhood.firstWords'), toddler, 1)).toBe(true);
    expect(isEventEligible(findChildhood('childhood.firstSteps'), toddler, 1)).toBe(true);
    expect(isEventEligible(findChildhood('childhood.bedtimeStory'), toddler, 1)).toBe(true);
  });

  it('makes a 6-year-old eligible for the core early-childhood beats', () => {
    const kid = makeChild(6);
    for (const id of [
      'childhood.firstDayOfSchool',
      'childhood.learnToRideBike',
      'childhood.firstPet',
      'childhood.familyTrip',
      'childhood.lostTooth',
      'childhood.scarySlide',
      'childhood.shareTheToy',
    ]) {
      expect(isEventEligible(findChildhood(id), kid, 1), `${id} eligible at 6`).toBe(true);
    }
  });

  it('gates childhood content out for an adult (maxAge cap)', () => {
    const adult = makeChild(30);
    for (const def of childhoodCatalog) {
      expect(isEventEligible(def, adult, 1), `${def.id} ineligible at 30`).toBe(false);
    }
  });

  it('makes the recurring bedtime-story beat repeatable on a cooldown', () => {
    const bedtime = findChildhood('childhood.bedtimeStory');
    expect(bedtime.repeatable).toBe(true);
    expect(bedtime.cooldownDays).toBeGreaterThan(0);

    const child = makeChild(3);
    expect(isEventEligible(bedtime, child, 100)).toBe(true);
    // Just fired — within cooldown, ineligible despite being repeatable.
    child.eventLastFired = { 'childhood.bedtimeStory': 100 };
    expect(isEventEligible(bedtime, child, 110)).toBe(false);
    // After the cooldown, eligible again.
    expect(isEventEligible(bedtime, child, 100 + (bedtime.cooldownDays ?? 0) + 1)).toBe(true);
  });

  it('does not gate any beat behind unreachable conditions (fires for a plain young character)', () => {
    // A fresh young character with no special flags/relations should see every
    // non-repeatable childhood beat in its window — none are gated behind an
    // unreachable isEligible.
    for (const def of childhoodCatalog) {
      const mid = Math.floor(((def.minAge ?? 2) + (def.maxAge ?? 9)) / 2);
      const child = makeChild(mid);
      expect(isEventEligible(def, child, 1), `${def.id} fires for a plain ${mid}yo`).toBe(true);
    }
  });
});
