/**
 * Env-gated IAP receipt validation regression tests.
 *
 * The in-app purchase handler grants diamonds. Historically it did so with NO
 * server-side receipt validation, so a forged `productId` yielded free
 * diamonds. We added a config flag `IAP_VALIDATION_ENABLED` (default OFF):
 *
 *   - OFF (default): legacy behavior, grant via purchaseInAppItem.
 *   - ON: require receiptData + transactionId, validate with Apple via
 *     validateIAPReceipt (which awards diamonds + enforces idempotency itself),
 *     and fail closed when the receipt is missing.
 *
 * These tests mock both the config flag and validateIAPReceipt so NO real
 * network call to Apple is ever made.
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';

// Mutable config mock so each test can flip the flag.
const mockConfig = { IAP_VALIDATION_ENABLED: false };
vi.mock('../../src/config.js', () => ({
  get config() {
    return mockConfig;
  },
}));

// Mock the validator so no Apple network call happens.
const validateIAPReceipt = vi.fn();
vi.mock('../../src/monetization/index.js', () => ({
  validateIAPReceipt: (...args: unknown[]) => validateIAPReceipt(...(args as [])),
}));

// Mock retention barrel (handlePurchaseItem path imports it; keep it inert).
vi.mock('../../src/services/retention/index.js', () => ({
  checkAchievementsAsync: vi.fn(async () => []),
  updateQuestProgress: vi.fn(async () => null),
  trackMoneySpent: vi.fn(),
  sendAchievementsUnlocked: vi.fn(),
  sendQuestProgress: vi.fn(),
}));

import { Person } from '../../src/models/Person';
import { Player } from '../../src/models/Player';
import { handlePurchaseInAppItem } from '../../src/handlers/purchases';
import { getInAppPurchaseItems } from '../../src/services/shop/shop_manager.js';

class MockPlayerSession {
  player: Player;
  sentMessages: any[] = [];
  saveCount = 0;

  constructor(player: Player) {
    this.player = player;
  }

  send(message: any) {
    this.sentMessages.push(message);
  }

  sendPlayerObject() {
    this.sentMessages.push({ type: 'playerObject', player: this.player });
  }

  async savePlayer() {
    this.saveCount += 1;
    return undefined;
  }
}

function makeSession(): MockPlayerSession {
  const character = new Person({
    id: 'char-1',
    firstname: 'Test',
    lastname: 'Player',
    sex: 'Female',
    ageYears: 25,
    money: 1000,
    diamonds: 0,
  });
  const player = new Player({
    userId: 'user-1',
    character,
    r: [],
    status: 'playing',
    date: '2024-06-15',
    hourOfDay: 10,
    minuteOfHour: 30,
  });
  return new MockPlayerSession(player);
}

/** A real in-app diamond pack id + its diamond amount. */
function firstPack() {
  const item = getInAppPurchaseItems()[0];
  if (!item) throw new Error('expected at least one in-app purchase item');
  return item;
}

describe('handlePurchaseInAppItem — env-gated receipt validation', () => {
  beforeEach(() => {
    validateIAPReceipt.mockReset();
    mockConfig.IAP_VALIDATION_ENABLED = false;
  });

  it('flag OFF: legacy grant path still works (no validator call)', async () => {
    const pack = firstPack();
    const session = makeSession();

    await handlePurchaseInAppItem({ productId: pack.id }, session as any);

    // Legacy path awarded diamonds via purchaseInAppItem.
    expect(session.player.c.diamonds).toBe(pack.diamonds);
    expect(validateIAPReceipt).not.toHaveBeenCalled();
    const complete = session.sentMessages.find((m) => m.type === 'inAppPurchaseComplete');
    expect(complete?.success).toBe(true);
  });

  it('flag ON + missing receiptData/transactionId: rejects, grants nothing', async () => {
    mockConfig.IAP_VALIDATION_ENABLED = true;
    const pack = firstPack();
    const session = makeSession();

    // Only productId — no receiptData / transactionId (the current iOS shape).
    await handlePurchaseInAppItem({ productId: pack.id }, session as any);

    expect(validateIAPReceipt).not.toHaveBeenCalled();
    expect(session.player.c.diamonds).toBe(0);
    const error = session.sentMessages.find((m) => m.type === 'error');
    expect(error).toBeDefined();
    expect(error.errorCode).toBe('MISSING_RECEIPT');
    // No purchase-complete message on a fail-closed rejection.
    expect(session.sentMessages.find((m) => m.type === 'inAppPurchaseComplete')).toBeUndefined();
  });

  it('flag ON + receipt present: dispatches to validateIAPReceipt and grants on success', async () => {
    mockConfig.IAP_VALIDATION_ENABLED = true;
    const pack = firstPack();
    const session = makeSession();

    validateIAPReceipt.mockResolvedValueOnce({
      success: true,
      diamondsAwarded: pack.diamonds,
      message: `Successfully purchased ${pack.diamonds} diamonds`,
    });

    await handlePurchaseInAppItem(
      { productId: pack.id, receiptData: 'base64-receipt', transactionId: 'txn-abc-123' },
      session as any
    );

    expect(validateIAPReceipt).toHaveBeenCalledTimes(1);
    // Arg order: (playerId, receiptData, transactionId, productId)
    expect(validateIAPReceipt).toHaveBeenCalledWith(
      'user-1',
      'base64-receipt',
      'txn-abc-123',
      pack.id
    );
    // validateIAPReceipt awards diamonds itself (mocked here), so the handler
    // must NOT also call purchaseInAppItem — player.c.diamonds stays 0 in this
    // mock, proving no double-grant via the local shop path.
    expect(session.player.c.diamonds).toBe(0);
    const complete = session.sentMessages.find((m) => m.type === 'inAppPurchaseComplete');
    expect(complete?.success).toBe(true);
    expect(complete?.diamondsAwarded).toBe(pack.diamonds);
    expect(session.saveCount).toBeGreaterThanOrEqual(1);
  });

  it('flag ON + validator failure: sends error with errorCode, grants nothing', async () => {
    mockConfig.IAP_VALIDATION_ENABLED = true;
    const pack = firstPack();
    const session = makeSession();

    validateIAPReceipt.mockResolvedValueOnce({
      success: false,
      message: 'Receipt could not be authenticated',
      errorCode: 'AUTH_FAILED',
    });

    await handlePurchaseInAppItem(
      { productId: pack.id, receiptData: 'bad-receipt', transactionId: 'txn-bad' },
      session as any
    );

    expect(validateIAPReceipt).toHaveBeenCalledTimes(1);
    expect(session.player.c.diamonds).toBe(0);
    const error = session.sentMessages.find((m) => m.type === 'error');
    expect(error?.errorCode).toBe('AUTH_FAILED');
    expect(session.sentMessages.find((m) => m.type === 'inAppPurchaseComplete')).toBeUndefined();
  });
});
