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

import { createEventRegistry } from '../../src/events/v2/registry.js';
import type { EventDefinition } from '../../src/events/v2/types.js';
import { EventEngine } from '../../src/events/v2/engine/EventEngine.js';
import { EventResponder } from '../../src/events/v2/engine/respond.js';

interface InMemoryEventInstance {
  instanceId: string;
  playerId: string;
  eventId: string;
  status: 'pending' | 'answered' | 'resolved';
  prompt: string;
  choices: EventDefinition['choices'];
  context: Record<string, unknown> | null;
  selectedChoiceId: string | null;
  createdAt: string;
  answeredAt: string | null;
  resolvedAt: string | null;
}

function createInMemoryStore() {
  const instances = new Map<string, InMemoryEventInstance>();

  return {
    getPendingEventInstances: vi.fn(async (playerId: string) => {
      return Array.from(instances.values()).filter(
        (instance) => instance.playerId === playerId && instance.status === 'pending'
      );
    }),
    createEventInstance: vi.fn(async (input: Omit<InMemoryEventInstance, 'status' | 'context' | 'selectedChoiceId' | 'createdAt' | 'answeredAt' | 'resolvedAt'> & { context?: Record<string, unknown> }) => {
      const createdAt = new Date().toISOString();
      const instance: InMemoryEventInstance = {
        ...input,
        status: 'pending',
        context: input.context ?? null,
        selectedChoiceId: null,
        createdAt,
        answeredAt: null,
        resolvedAt: null,
      };
      instances.set(instance.instanceId, instance);
      return instance;
    }),
    answerEventInstance: vi.fn(async (instanceId: string, choiceId: string) => {
      const instance = instances.get(instanceId);
      if (!instance || instance.status !== 'pending') {
        return false;
      }

      instance.status = 'answered';
      instance.selectedChoiceId = choiceId;
      instance.answeredAt = new Date().toISOString();
      instances.set(instanceId, instance);
      return true;
    }),
    resolveEventInstance: vi.fn(async (instanceId: string, resolution: Record<string, unknown>) => {
      const instance = instances.get(instanceId);
      if (!instance || (instance.status !== 'pending' && instance.status !== 'answered')) {
        return false;
      }

      instance.status = 'resolved';
      instance.context = resolution;
      instance.resolvedAt = new Date().toISOString();
      instances.set(instanceId, instance);
      return true;
    }),
    listInstances: () => Array.from(instances.values()),
  };
}

describe('EventResponder deterministic resolution', () => {
  it('completes ask -> answer -> resolve without random polling', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'event_adult',
        category: 'test',
        prompt: 'Adult prompt',
        minAge: 18,
        choices: [
          {
            choiceId: 'help',
            text: 'Help',
            resolutionText: 'You stepped in and helped.',
            effects: {
              resources: {
                energy: -5,
                money: 10,
              },
            },
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'player-1',
      c: {
        ageYears: 22,
        energy: 50,
        money: 20,
        diamonds: 0,
      },
    };

    const prompt = await engine.promptNext(player);
    expect(prompt?.type).toBe('event_prompt');

    const resolved = await responder.respond(player, {
      eventId: 'event_adult',
      choiceId: 'help',
    });

    expect(resolved).toMatchObject({
      type: 'event_resolved',
      eventId: 'event_adult',
      resolutionText: 'You stepped in and helped.',
      metadata: {
        choiceId: 'help',
      },
    });
    expect(store.answerEventInstance).toHaveBeenCalledTimes(1);
    expect(store.resolveEventInstance).toHaveBeenCalledTimes(1);
    expect(player.c.energy).toBe(45);
    expect(player.c.money).toBe(30);
    expect(store.listInstances().every((instance) => instance.status !== 'pending')).toBe(true);
  });

  // Regression test for issue C1 (documented in project memory):
  // "Function-based question events can't deliver positive rewards. Only
  //  energy/money/diamond costs from createAnswerOption are auto-deducted.
  //  Positive stat changes (happiness, affinity, etc.) in function-based events
  //  NEVER APPLY on answer."
  //
  // C1 was diagnosed against the legacy (pre-v2) function-based event path.
  // v2 uses EventResponder + applyEventEffects, which should apply BOTH
  // resource changes and stat deltas. This test pins that behavior so we
  // cannot regress back to broken rewards when refactoring.
  it('applies positive stat rewards from choice.effects.stats (C1 regression)', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'c1_positive_reward',
        category: 'test',
        prompt: 'A stranger compliments your outfit. How do you respond?',
        minAge: 18,
        choices: [
          {
            choiceId: 'say_thanks',
            text: 'Thank them warmly',
            resolutionText: 'You felt genuinely uplifted.',
            effects: {
              stats: {
                happiness: 8,
                mood: 5,
              },
              resources: {
                energy: 3, // positive energy reward, not a cost
              },
            },
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'c1-player',
      c: {
        ageYears: 30,
        happiness: 50,
        mood: 40,
        energy: 60,
        money: 0,
        diamonds: 0,
      },
    };

    await engine.promptNext(player);
    const resolved = await responder.respond(player, {
      eventId: 'c1_positive_reward',
      choiceId: 'say_thanks',
    });

    expect(resolved).toMatchObject({
      type: 'event_resolved',
      eventId: 'c1_positive_reward',
    });
    // C1 fix: all three positive deltas must land on player.c
    expect(player.c.happiness).toBe(58);
    expect(player.c.mood).toBe(45);
    expect(player.c.energy).toBe(63);
  });

  // Regression test: interactive events must be added to player.askedQuestions
  // after they are resolved, otherwise the same event is selected every hour
  // forever by the selector (which dedupes on askedQuestions.has(eventId)).
  //
  // The simulator caught this on 2026-04-13 — a 1-year run fired only ONE
  // unique event, because the engine marked only passive events as asked.
  // This test pins the fix: interactive events must be marked too.
  it('marks interactive events as asked after resolution so they dedupe', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'event_one_shot',
        category: 'test',
        kind: 'interactive',
        prompt: 'A repeatable prompt',
        minAge: 5,
        choices: [
          {
            choiceId: 'pick_it',
            text: 'Pick it',
            resolutionText: 'Resolved.',
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player: { userId: string; c: { ageYears: number }; askedQuestions: Set<string> } = {
      userId: 'player-asked-1',
      c: { ageYears: 20 },
      askedQuestions: new Set<string>(),
    };

    // First prompt: event fires, instance pending.
    const first = await engine.promptNext(player as any);
    expect(first?.type).toBe('event_prompt');

    // Answer it.
    const resolved = await responder.respond(player as any, {
      eventId: 'event_one_shot',
      choiceId: 'pick_it',
    });
    expect(resolved).toMatchObject({ type: 'event_resolved' });

    // CRITICAL: after resolution, the event id must be in askedQuestions so
    // the selector won't pick it again.
    expect(player.askedQuestions.has('event_one_shot')).toBe(true);

    // And the next promptNext call must NOT return this same event.
    const next = await engine.promptNext(player as any);
    if (next && 'eventId' in next) {
      expect(next.eventId).not.toBe('event_one_shot');
    }
  });

  // Confirmation-screen support: a choice that changes affinity toward an NPC
  // must (a) actually move that NPC's affinity (clamped -100..100) and
  // (b) surface the change attributed to the NPC's NAME in resolvedRelationships
  // so iOS can render "Mom +5 affinity" on the decision-confirmation screen.
  it('applies relationship affinity deltas and surfaces named changes', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'family_call',
        category: 'family',
        prompt: 'Mom calls to check in. How do you respond?',
        minAge: 10,
        choices: [
          {
            choiceId: 'warm',
            text: 'Chat warmly',
            resolutionText: 'You and Mom had a great talk.',
            effects: {
              stats: { happiness: 4 },
              relationships: [{ personId: 'mom-1', affinityDelta: 5 }],
            },
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'rel-player',
      c: { ageYears: 16, happiness: 50 },
      r: [
        { id: 'mom-1', firstname: 'Mom', lastname: '', affinity: 70 },
        { id: 'dad-1', firstname: 'Dad', lastname: '', affinity: 60 },
      ],
    };

    await engine.promptNext(player as any);
    const resolved = await responder.respond(player as any, {
      eventId: 'family_call',
      choiceId: 'warm',
    });

    expect(resolved).toMatchObject({
      type: 'event_resolved',
      resolvedRelationships: [{ personId: 'mom-1', name: 'Mom', affinityDelta: 5 }],
    });
    // Mom's affinity moved; Dad untouched.
    expect(player.r[0].affinity).toBe(75);
    expect(player.r[1].affinity).toBe(60);
    expect(player.c.happiness).toBe(54);
  });

  it('clamps affinity to 100 and skips unknown person ids', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'family_clamp',
        category: 'family',
        prompt: 'A heartfelt moment.',
        minAge: 10,
        choices: [
          {
            choiceId: 'bond',
            text: 'Bond',
            resolutionText: 'Closer than ever.',
            effects: {
              relationships: [
                { personId: 'mom-1', affinityDelta: 50 },
                { personId: 'ghost-1', affinityDelta: 10 },
              ],
            },
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'clamp-player',
      c: { ageYears: 16 },
      r: [{ id: 'mom-1', firstname: 'Mom', affinity: 80 }],
    };

    await engine.promptNext(player as any);
    const resolved = (await responder.respond(player as any, {
      eventId: 'family_clamp',
      choiceId: 'bond',
    })) as { resolvedRelationships?: unknown[] };

    // Mom clamps at 100; the unknown person id is dropped.
    expect(player.r[0].affinity).toBe(100);
    expect(resolved.resolvedRelationships).toEqual([
      { personId: 'mom-1', name: 'Mom', affinityDelta: 50 },
    ]);
  });

  it('returns event_error for invalid choice and does not resolve', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'event_adult',
        category: 'test',
        prompt: 'Adult prompt',
        minAge: 18,
        choices: [{ choiceId: 'help', text: 'Help' }],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'player-1',
      c: {
        ageYears: 22,
      },
    };

    await engine.promptNext(player);

    const result = await responder.respond(player, {
      eventId: 'event_adult',
      choiceId: 'unknown_choice',
    });

    expect(result).toMatchObject({
      type: 'event_error',
      code: 'INVALID_CHOICE',
      eventId: 'event_adult',
    });
    expect(store.answerEventInstance).not.toHaveBeenCalled();
    expect(store.resolveEventInstance).not.toHaveBeenCalled();
  });
});
