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';
import { applyEventEffects } from '../../src/events/v2/engine/effects.js';
import { awardDiamonds, spendDiamonds } from '../../src/monetization/diamondEconomy.js';

// FU-A regression: event choices that cost diamonds were applied even when the
// player could not afford them, driving player.c.diamonds NEGATIVE. Diamonds are
// the premium/IAP currency, so a negative balance is a real correctness bug.
//
// The fix lives in applyEventEffects (effects.ts), the single chokepoint that
// respond.ts routes choice.diamondCost through. Guard rules:
//   - A negative diamond delta (a SPEND) that would go below 0 is no-oped
//     (affordability gate) — non-diamond effects still apply.
//   - Any diamond mutation is clamped at >= 0.
//   - Positive diamond deltas (grants/rewards) are always applied.
// The diamondEconomy IAP/grant path (awardDiamonds) is untouched.

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('Diamond spend never goes negative (FU-A)', () => {
  describe('applyEventEffects (unit / chokepoint)', () => {
    it('does NOT drive diamonds below 0 when the spend is unaffordable', () => {
      const player = { c: { diamonds: 3, happiness: 50 } as Record<string, unknown> };

      applyEventEffects(player, {
        // -10 diamond cost the player cannot afford (only has 3)
        resources: { diamonds: -10 },
        stats: { happiness: 5 },
      });

      // Diamond spend no-ops (unaffordable) instead of going to -7.
      expect(player.c.diamonds).toBe(3);
      expect(player.c.diamonds).toBeGreaterThanOrEqual(0);
      // Non-diamond effects still apply: only the diamond spend is gated.
      expect(player.c.happiness).toBe(55);
    });

    it('deducts an AFFORDABLE diamond spend correctly', () => {
      const player = { c: { diamonds: 20 } as Record<string, unknown> };

      applyEventEffects(player, { resources: { diamonds: -8 } });

      expect(player.c.diamonds).toBe(12);
    });

    it('applies a diamond GRANT (positive delta) from an event choice', () => {
      const player = { c: { diamonds: 5 } as Record<string, unknown> };

      applyEventEffects(player, { resources: { diamonds: 15 } });

      expect(player.c.diamonds).toBe(20);
    });

    it('clamps an exactly-affordable spend to 0 without going negative', () => {
      const player = { c: { diamonds: 10 } as Record<string, unknown> };

      applyEventEffects(player, { resources: { diamonds: -10 } });

      expect(player.c.diamonds).toBe(0);
    });
  });

  describe('EventResponder (full ask -> answer -> resolve via diamondCost)', () => {
    it('an unaffordable diamondCost choice does not push diamonds below 0', async () => {
      const definitions: EventDefinition[] = [
        {
          id: 'premium_choice',
          category: 'test',
          prompt: 'Spend diamonds to skip ahead?',
          minAge: 1,
          choices: [
            {
              choiceId: 'pay',
              text: 'Pay 25 diamonds',
              diamondCost: 25,
              resolutionText: 'You tried to pay.',
              effects: { stats: { happiness: 3 } },
            },
          ],
        },
      ];
      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: 22, diamonds: 5, happiness: 50 },
      };

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

      expect(resolved).toMatchObject({ type: 'event_resolved', eventId: 'premium_choice' });
      // Unaffordable: diamonds untouched (NOT -20), never negative.
      expect(player.c.diamonds).toBe(5);
      expect(player.c.diamonds).toBeGreaterThanOrEqual(0);
      // Non-diamond effect of the choice still lands.
      expect(player.c.happiness).toBe(53);
    });

    it('an affordable diamondCost choice deducts the diamonds', async () => {
      const definitions: EventDefinition[] = [
        {
          id: 'premium_choice_2',
          category: 'test',
          prompt: 'Spend diamonds to skip ahead?',
          minAge: 1,
          choices: [
            {
              choiceId: 'pay',
              text: 'Pay 25 diamonds',
              diamondCost: 25,
              resolutionText: 'You paid.',
            },
          ],
        },
      ];
      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: 22, diamonds: 100 },
      };

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

      expect(resolved).toMatchObject({ type: 'event_resolved' });
      expect(player.c.diamonds).toBe(75);
    });
  });

  describe('IAP / grant path (diamondEconomy) is unaffected', () => {
    it('awardDiamonds still adds to the balance', () => {
      const player = { userId: 'iap-player', c: { diamonds: 10 } };

      const ok = awardDiamonds(player, 'iap_purchase', 50);

      expect(ok).toBe(true);
      expect(player.c.diamonds).toBe(60);
    });

    it('spendDiamonds still rejects an unaffordable spend without going negative', () => {
      const player = { userId: 'iap-spender', c: { diamonds: 5 } };

      const result = spendDiamonds(player, 'time_skip', 40);

      expect(result.success).toBe(false);
      expect(player.c.diamonds).toBe(5);
      expect(player.c.diamonds).toBeGreaterThanOrEqual(0);
    });

    it('spendDiamonds deducts an affordable spend correctly', () => {
      const player = { userId: 'iap-affordable', c: { diamonds: 30 } };

      const result = spendDiamonds(player, 'energy_refill', 18);

      expect(result.success).toBe(true);
      expect(player.c.diamonds).toBe(12);
    });
  });
});
