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 money affordability gate', () => {
  // The lone un-gated money path: a broke player choosing a money-cost option
  // would previously go negative (observed -$5,000 in the real app). The server
  // gate rejects the whole choice — nothing answered/applied/resolved — matching
  // the shop's "Insufficient funds" rejection and the diamond gate.
  it('rejects an unaffordable money choice when an affordable alternative exists', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'pricey_dilemma',
        category: 'test',
        prompt: 'A risky investment opportunity.',
        minAge: 18,
        choices: [
          {
            choiceId: 'invest',
            text: 'Invest 5000',
            resolutionText: 'You invested.',
            moneyCost: 5000,
          },
          // Affordable alternative: the gate should still reject the
          // unaffordable choice because the player can pick this one instead.
          {
            choiceId: 'walk_away',
            text: 'Walk away',
            resolutionText: 'You walked away.',
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'broke-player',
      c: {
        ageYears: 30,
        money: 0,
        energy: 50,
        diamonds: 0,
      },
    };

    await engine.promptNext(player);
    const result = await responder.respond(player, {
      eventId: 'pricey_dilemma',
      choiceId: 'invest',
    });

    expect(result).toMatchObject({
      type: 'event_error',
      code: 'INSUFFICIENT_FUNDS',
      eventId: 'pricey_dilemma',
    });
    // Player money unchanged; the instance was never answered or resolved.
    expect(player.c.money).toBe(0);
    expect(store.answerEventInstance).not.toHaveBeenCalled();
    expect(store.resolveEventInstance).not.toHaveBeenCalled();
    expect(store.listInstances().some((instance) => instance.status === 'pending')).toBe(true);
  });

  it('resolves an affordable money choice and deducts the cost', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'pricey_dilemma',
        category: 'test',
        prompt: 'A risky investment opportunity.',
        minAge: 18,
        choices: [
          {
            choiceId: 'invest',
            text: 'Invest 5000',
            resolutionText: 'You invested.',
            moneyCost: 5000,
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'rich-player',
      c: {
        ageYears: 30,
        money: 10000,
        energy: 50,
        diamonds: 0,
      },
    };

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

    expect(resolved).toMatchObject({
      type: 'event_resolved',
      eventId: 'pricey_dilemma',
    });
    expect(player.c.money).toBe(5000);
    expect(store.answerEventInstance).toHaveBeenCalledTimes(1);
    expect(store.resolveEventInstance).toHaveBeenCalledTimes(1);
  });

  it('does NOT block a free / non-money choice while broke', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'energy_dilemma',
        category: 'test',
        prompt: 'Help a neighbor move some boxes.',
        minAge: 18,
        choices: [
          {
            choiceId: 'help',
            text: 'Lend a hand',
            resolutionText: 'You helped out.',
            energyCost: 10,
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'broke-but-energetic',
      c: {
        ageYears: 30,
        money: 0,
        energy: 50,
        diamonds: 0,
      },
    };

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

    expect(resolved).toMatchObject({
      type: 'event_resolved',
      eventId: 'energy_dilemma',
    });
    expect(player.c.money).toBe(0);
    expect(player.c.energy).toBe(40);
    expect(store.resolveEventInstance).toHaveBeenCalledTimes(1);
  });

  it('does NOT block a positive money reward while broke', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'reward_dilemma',
        category: 'test',
        prompt: 'You found a wallet and returned it. The owner is grateful.',
        minAge: 18,
        choices: [
          {
            choiceId: 'accept_reward',
            text: 'Accept the reward',
            resolutionText: 'You accepted a generous reward.',
            effects: {
              resources: { money: 250 },
            },
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'broke-rewardee',
      c: {
        ageYears: 30,
        money: 0,
        energy: 50,
        diamonds: 0,
      },
    };

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

    expect(resolved).toMatchObject({
      type: 'event_resolved',
      eventId: 'reward_dilemma',
    });
    expect(player.c.money).toBe(250);
    expect(store.resolveEventInstance).toHaveBeenCalledTimes(1);
  });

  // Soft-lock prevention / debt fallback: the "family reunion" dilemma has ONLY
  // money options. When the player can't afford ANY of them, gating every choice
  // would dead-end the paused question. Instead, when no affordable alternative
  // exists, the chosen money spend is ALLOWED to resolve and money goes negative.
  it('allows an unaffordable money choice when ALL choices are unaffordable (debt fallback)', async () => {
    const definitions: EventDefinition[] = [
      {
        id: 'family_reunion',
        category: 'test',
        prompt: 'Your family wants to host a reunion. How much do you chip in?',
        minAge: 18,
        choices: [
          {
            choiceId: 'host_big',
            text: 'Host the big reunion',
            resolutionText: 'You footed the bill.',
            effects: {
              resources: { money: -200 },
            },
          },
          {
            choiceId: 'host_small',
            text: 'Host a small gathering',
            resolutionText: 'You kept it modest.',
            effects: {
              resources: { money: -50 },
            },
          },
        ],
      },
    ];
    const registry = createEventRegistry(definitions);
    const store = createInMemoryStore();
    const engine = new EventEngine(registry, store);
    const responder = new EventResponder(registry, store);
    const player = {
      userId: 'broke-family',
      c: {
        ageYears: 30,
        money: 0,
        energy: 50,
        diamonds: 0,
      },
    };

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

    expect(resolved).toMatchObject({
      type: 'event_resolved',
      eventId: 'family_reunion',
    });
    // No affordable alternative existed, so the spend went through and money
    // went negative — the sanctioned debt fallback, not a soft-lock.
    expect(player.c.money).toBe(-50);
    expect(store.answerEventInstance).toHaveBeenCalledTimes(1);
    expect(store.resolveEventInstance).toHaveBeenCalledTimes(1);
  });
});
