/**
 * Regression: a permanently-rejected Live Activity token must be PERSISTED as
 * deregistered by the offline loop.
 *
 * Bug: iterateGames calls initLifeSim (which saves the player) and only THEN
 * runs queueLiveActivityUpdate. When APNS rejected the token (BadDeviceToken),
 * the registration was cleared in memory but never saved — the next pass
 * reloaded the dead token from the DB and retried, forever (23k+ log entries
 * from two stale tokens in prod).
 *
 * The fix: after the live-activity step, if the registration was cleared
 * (token rejected or activity ended), iterateGames saves the player again.
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';

import { Player } from '../../src/models/Player.js';
import { Person } from '../../src/models/Person.js';
import { iterateGames, unregisterPlayer } from '../../src/game/engine/LoopManager.js';
import { sendLiveActivityUpdate } from '../../src/services/notifications/pushNotificationService.js';

vi.mock('../../src/services/notifications/pushNotificationService.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../src/services/notifications/pushNotificationService.js')>();
  return {
    ...actual,
    sendLiveActivityUpdate: vi.fn(),
  };
});

const mockSend = vi.mocked(sendLiveActivityUpdate);

function makeOfflinePlayer(userId: string): Player {
  const character = new Person({
    id: `char-${userId}`,
    firstname: 'Off',
    lastname: 'Line',
    sex: 'Female',
    status: 'alive',
    ageYears: 18,
    ageDays: 100,
    occupation: 'student',
    deathChance: 0,
    health: 0,
  } as never);

  const player = new Player({
    userId,
    character,
    status: 'playing',
    connection: 'disconnected',
    controller: 'inactive',
    // Park mid-hour so a single initLifeSim tick does NOT cross an hour
    // boundary: we are testing live-activity persistence, not the event loop.
    minuteOfHour: 10,
    hourOfDay: 12,
  });
  player.liveActivity = {
    activityId: 'activity-1',
    characterId: character.id,
    pushToken: 'dead-token-1234',
    startedAt: '2026-06-10T11:00:00Z',
  };
  return player;
}

describe('offline loop persists Live Activity deregistration', () => {
  beforeEach(() => {
    mockSend.mockReset();
  });

  it('saves the player with the registration cleared after a permanent token rejection', async () => {
    mockSend.mockResolvedValue({ success: false, error: 'BadDeviceToken', statusCode: 400 });

    const playerId = 'offline-dead-token';
    const player = makeOfflinePlayer(playerId);

    const savedLiveActivitySnapshots: Array<unknown> = [];
    const loadGames = vi.fn(async () => [{ playerId }]);
    const loadGameAsync = vi.fn(async () => player);
    const saveGameAsync = vi.fn(async (p: Player) => {
      savedLiveActivitySnapshots.push(p.liveActivity);
    });

    try {
      await iterateGames(loadGames, loadGameAsync, saveGameAsync);
    } finally {
      unregisterPlayer(playerId);
    }

    expect(mockSend).toHaveBeenCalledTimes(1);
    expect(player.liveActivity).toBeUndefined();
    // The deregistration must reach the DB: at least one save observed the
    // cleared registration. Without the post-live-activity save, every save
    // happens before the rejection and this FAILS.
    expect(savedLiveActivitySnapshots).toContain(undefined);
  });

  it('does not add an extra save when the send succeeds', async () => {
    mockSend.mockResolvedValue({ success: true, statusCode: 200 });

    const playerId = 'offline-healthy-token';
    const player = makeOfflinePlayer(playerId);

    const loadGames = vi.fn(async () => [{ playerId }]);
    const loadGameAsync = vi.fn(async () => player);
    const saveGameAsync = vi.fn(async () => {});

    try {
      await iterateGames(loadGames, loadGameAsync, saveGameAsync);
    } finally {
      unregisterPlayer(playerId);
    }

    expect(player.liveActivity).toBeDefined();
    const baselineSaves = saveGameAsync.mock.calls.length;

    mockSend.mockClear();
    saveGameAsync.mockClear();
    try {
      await iterateGames(loadGames, loadGameAsync, saveGameAsync);
    } finally {
      unregisterPlayer(playerId);
    }

    // Same pass shape as the first run: a healthy registration adds no
    // dereg-persistence save on top of whatever initLifeSim does.
    expect(saveGameAsync.mock.calls.length).toBeLessThanOrEqual(baselineSaves);
  });
});
