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

import { resolveText } from '../../src/events/v2/engine/selector.js';
import { eventCatalog } from '../../src/events/v2/catalog/index.js';
import { holidayCatalog } from '../../src/events/v2/catalog/holidays.js';
import { randomCatalog } from '../../src/events/v2/catalog/random.js';
import { familyCatalog } from '../../src/events/v2/catalog/family.js';
import { careerCatalog } from '../../src/events/v2/catalog/career.js';
import { healthCatalog } from '../../src/events/v2/catalog/health.js';
import { familyArcCatalog } from '../../src/events/v2/catalog/family-arcs.js';
import type { EventDefinition, EventPlayerContext } from '../../src/events/v2/types.js';

function playerOnDay(dayOfYear: number, ageYears = 30): EventPlayerContext {
  return {
    userId: 'p1',
    c: { ageYears },
    dayOfYear,
    askedQuestions: new Set<string>(),
    eventLastFired: {},
    flags: new Set<string>(),
  };
}

/** Render an event's prompt for a given player (fn override or static). */
function renderPrompt(def: EventDefinition, player: EventPlayerContext): string {
  return resolveText(def.promptFn, def.prompt, player);
}

/** Render a choice's resolution for a given player (fn override or static). */
function renderResolution(
  def: EventDefinition,
  choiceId: string,
  player: EventPlayerContext
): string {
  const choice = def.choices.find((c) => c.choiceId === choiceId)!;
  return resolveText(choice.resolutionTextFn, choice.resolutionText ?? choice.text, player);
}

function findIn(catalog: EventDefinition[], id: string): EventDefinition {
  const def = catalog.find((e) => e.id === id);
  expect(def, `event ${id}`).toBeDefined();
  return def!;
}

describe('text variation: determinism within a firing', () => {
  it('produces identical text for the same player/day (same seed)', () => {
    const review = findIn(careerCatalog, 'performanceReview');
    const a = renderPrompt(review, playerOnDay(120, 40));
    const b = renderPrompt(review, playerOnDay(120, 40));
    expect(a).toBe(b);

    const ra = renderResolution(review, 'prepared', playerOnDay(120, 40));
    const rb = renderResolution(review, 'prepared', playerOnDay(120, 40));
    expect(ra).toBe(rb);
  });

  it('every variant rendered is one of the declared variants (non-empty string)', () => {
    const holiday = findIn(holidayCatalog, 'holiday_new_year');
    for (let day = 1; day <= 40; day++) {
      const text = renderPrompt(holiday, playerOnDay(day, 20 + (day % 30)));
      expect(typeof text).toBe('string');
      expect(text.length).toBeGreaterThan(0);
    }
  });
});

describe('text variation: variety across firings', () => {
  function distinctRenders(
    def: EventDefinition,
    render: (p: EventPlayerContext) => string
  ): number {
    const seen = new Set<string>();
    for (let day = 1; day <= 200; day++) {
      seen.add(render(playerOnDay(day, 20 + (day % 40))));
    }
    return seen.size;
  }

  it('holiday prompts and resolutions vary across years', () => {
    const newYear = findIn(holidayCatalog, 'holiday_new_year');
    expect(distinctRenders(newYear, (p) => renderPrompt(newYear, p))).toBeGreaterThan(1);
    expect(distinctRenders(newYear, (p) => renderResolution(newYear, 'acknowledge', p))).toBeGreaterThan(1);
  });

  it('random event prompts vary across firings', () => {
    const kindness = findIn(randomCatalog, 'random_small_kindness');
    expect(distinctRenders(kindness, (p) => renderPrompt(kindness, p))).toBeGreaterThan(1);
    const setback = findIn(randomCatalog, 'random_minor_setback');
    expect(distinctRenders(setback, (p) => renderResolution(setback, 'absorb_setback', p))).toBeGreaterThan(1);
  });

  it('family passive prompts vary across firings', () => {
    const checkin = findIn(familyCatalog, 'family_weekly_checkin');
    expect(distinctRenders(checkin, (p) => renderPrompt(checkin, p))).toBeGreaterThan(1);
    const meal = findIn(familyCatalog, 'family_shared_meal');
    expect(distinctRenders(meal, (p) => renderResolution(meal, 'meal_done', p))).toBeGreaterThan(1);
  });

  it('the repeatable performance review varies its prompt and per-choice resolutions', () => {
    const review = findIn(careerCatalog, 'performanceReview');
    expect(distinctRenders(review, (p) => renderPrompt(review, p))).toBeGreaterThan(1);
    for (const choiceId of ['prepared', 'feedback', 'wing']) {
      expect(distinctRenders(review, (p) => renderResolution(review, choiceId, p))).toBeGreaterThan(1);
    }
  });

  it('repeatable family child-milestones resolutions vary', () => {
    const child = findIn(familyArcCatalog, 'familyArc.childMilestones');
    for (const choiceId of ['celebrate', 'warm', 'busy']) {
      expect(distinctRenders(child, (p) => renderResolution(child, choiceId, p))).toBeGreaterThan(1);
    }
  });

  it('medical follow-up passives vary their resolution text', () => {
    const recovery = findIn(healthCatalog, 'health.emergencyRecovery');
    expect(distinctRenders(recovery, (p) => renderResolution(recovery, 'recover', p))).toBeGreaterThan(1);
  });
});

describe('text variation: static events unaffected', () => {
  it('events without a *Fn return their static string verbatim every time', () => {
    // A dilemma catalog event has no promptFn; resolveText must pass the string through.
    const anyStatic = eventCatalog.find((e) => !e.promptFn && typeof e.prompt === 'string')!;
    expect(anyStatic).toBeDefined();
    const p = playerOnDay(50);
    expect(renderPrompt(anyStatic, p)).toBe(anyStatic.prompt);
    expect(renderPrompt(anyStatic, playerOnDay(99))).toBe(anyStatic.prompt);
  });
});
