/**
 * Purchase -> Retention progression regression tests.
 *
 * Bug: buying an item did not unlock the related achievement, and the
 * diamond reward from achievements/quests was lost because the player was
 * saved BEFORE the retention checks ran.
 *
 * Root cause 1: handlePurchaseItem computed itemCount as
 *   (player.c.items.length ?? 0) + 1, but shop_manager.purchaseItem already
 *   pushes permanent items into player.c.items before the handler runs, so
 *   the first purchase reported itemCount=2 and the `first_purchase`
 *   achievement (which requires itemCount === 1) never fired.
 * Root cause 2: savePlayer() ran before the achievement/quest checks, so the
 *   diamonds awarded by those checks were never persisted or re-sent.
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';

// Mock the retention barrel so we can observe exactly what the handler passes.
const checkAchievementsAsync = vi.fn(async () => []);
const updateQuestProgress = vi.fn(async () => null);
const trackMoneySpent = vi.fn();
const sendAchievementsUnlocked = vi.fn();
const sendQuestProgress = vi.fn();

vi.mock('../../src/services/retention/index.js', () => ({
  checkAchievementsAsync: (...args: unknown[]) => checkAchievementsAsync(...(args as [])),
  updateQuestProgress: (...args: unknown[]) => updateQuestProgress(...(args as [])),
  trackMoneySpent: (...args: unknown[]) => trackMoneySpent(...(args as [])),
  sendAchievementsUnlocked: (...args: unknown[]) => sendAchievementsUnlocked(...(args as [])),
  sendQuestProgress: (...args: unknown[]) => sendQuestProgress(...(args as [])),
}));

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

class MockPlayerSession {
  player: Player;
  sentMessages: any[] = [];
  saveCount = 0;
  /** Snapshot of items.length at each savePlayer() call. */
  saveItemCounts: number[] = [];

  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;
    this.saveItemCounts.push(this.player.c.items?.length ?? 0);
    return undefined;
  }
}

function firstPermanentItem() {
  // A permanent (collection) item has energyBoost === 0 so it lands in inventory.
  const item = getStoreItems().find((i) => i.energyBoost === 0);
  if (!item) throw new Error('expected at least one permanent store item');
  return item;
}

describe('handlePurchaseItem retention progression', () => {
  let mockPlayer: Player;
  let mockSession: MockPlayerSession;

  beforeEach(() => {
    checkAchievementsAsync.mockClear();
    updateQuestProgress.mockClear();
    trackMoneySpent.mockClear();
    sendAchievementsUnlocked.mockClear();
    sendQuestProgress.mockClear();

    const character = new Person({
      id: 'char-1',
      firstname: 'Test',
      lastname: 'Player',
      ageYears: 25,
      money: 1_000_000,
      diamonds: 0,
    });

    mockPlayer = new Player({
      userId: 'user-1',
      character,
      r: [],
      status: 'playing',
      date: '2024-06-15',
      hourOfDay: 10,
      minuteOfHour: 30,
    });
    mockSession = new MockPlayerSession(mockPlayer);
  });

  it('reports itemCount === 1 to achievements on the first permanent purchase', async () => {
    const item = firstPermanentItem();

    await handlePurchaseItem(item.id, mockSession as any);

    expect(checkAchievementsAsync).toHaveBeenCalledTimes(1);
    // Signature: (playerId, eventType, eventData, playerData, stats, player)
    const playerData = checkAchievementsAsync.mock.calls[0][3] as { itemCount?: number };
    expect(playerData.itemCount).toBe(1);
  });

  it('always advances the buy_item quest by 1 on a successful purchase', async () => {
    const item = firstPermanentItem();

    await handlePurchaseItem(item.id, mockSession as any);

    expect(updateQuestProgress).toHaveBeenCalledTimes(1);
    const [playerId, questType, amount] = updateQuestProgress.mock.calls[0] as [
      string,
      string,
      number,
    ];
    expect(playerId).toBe('user-1');
    expect(questType).toBe('buy_item');
    expect(amount).toBe(1);
  });

  it('persists the player AFTER retention checks so awarded diamonds survive', async () => {
    const item = firstPermanentItem();

    // Simulate an achievement/quest awarding diamonds during the checks.
    checkAchievementsAsync.mockImplementationOnce(async () => {
      mockPlayer.c.diamonds = (mockPlayer.c.diamonds ?? 0) + 5;
      return [];
    });

    await handlePurchaseItem(item.id, mockSession as any);

    // At least one save must happen after the diamonds were granted.
    expect(mockSession.saveCount).toBeGreaterThanOrEqual(2);
    expect(mockPlayer.c.diamonds).toBe(5);
  });
});
