/**
 * Notification Manager Tests
 * Tests throttling logic, shouldSendNotification, and queueRealtimeNotification
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
  getRemainingBudget,
  clearThrottle,
  clearAllThrottles,
  shouldSendNotification,
  queueRealtimeNotification,
  notify,
  type NotifyResult,
} from '../../../src/services/notifications/notificationManager.js';
import type { Player } from '../../../src/models/Player.js';

// Mock the push notification service so we don't actually send
vi.mock('../../../src/services/notifications/pushNotificationService.js', () => ({
  sendPushNotification: vi.fn().mockResolvedValue({ success: true }),
}));

/** Create a minimal mock player for testing */
function createMockPlayer(overrides: Partial<Player> = {}): Player {
  return {
    userId: 'test-user-1',
    deviceToken: 'abc123token',
    connection: 'disconnected',
    gameSpeed: 10000,
    ...overrides,
  } as Player;
}

describe('Notification Manager', () => {
  beforeEach(() => {
    clearAllThrottles();
  });

  describe('Throttling', () => {
    it('should allow notifications when under the limit', () => {
      expect(getRemainingBudget('user1')).toBe(4);
    });

    it('should track remaining budget after sends', async () => {
      const player = createMockPlayer({ userId: 'budget-test' });

      // Send first notification
      await notify({
        userId: player.userId,
        deviceToken: player.deviceToken,
        isBackgrounded: true,
        payload: { title: 'Test', body: 'Body' },
      });

      expect(getRemainingBudget('budget-test')).toBe(3);
    });

    it('should throttle non-milestone after 3 (reserving 1 slot for milestones)', async () => {
      const userId = 'throttle-test';
      const opts = {
        userId,
        deviceToken: 'token123',
        isBackgrounded: true,
        payload: { title: 'Test', body: 'Body', category: 'life_event' as const },
      };

      // Send 3 non-milestone notifications — all should succeed
      for (let i = 0; i < 3; i++) {
        const result = await notify(opts);
        expect(result.sent).toBe(true);
      }

      // 4th non-milestone should be throttled (reserved for milestone)
      const result = await notify(opts);
      expect(result.sent).toBe(false);
      expect(result.reason).toBe('throttled');

      // But a milestone notification can still use the reserved slot
      const milestoneOpts = {
        ...opts,
        payload: { title: 'Milestone', body: 'You turned 18!', category: 'milestone' as const },
      };
      const milestoneResult = await notify(milestoneOpts);
      expect(milestoneResult.sent).toBe(true);

      expect(getRemainingBudget(userId)).toBe(0);

      // Now even milestone is throttled (all 4 slots used)
      const finalResult = await notify(milestoneOpts);
      expect(finalResult.sent).toBe(false);
      expect(finalResult.reason).toBe('throttled');
    });

    it('should clear throttle for a specific user', async () => {
      const opts = {
        userId: 'clear-test',
        deviceToken: 'token123',
        isBackgrounded: true,
        payload: { title: 'Test', body: 'Body' },
      };

      // Send some notifications
      await notify(opts);
      await notify(opts);
      expect(getRemainingBudget('clear-test')).toBe(2);

      // Clear throttle
      clearThrottle('clear-test');
      expect(getRemainingBudget('clear-test')).toBe(4);
    });

    it('should not affect other users when clearing throttle', async () => {
      const opts1 = {
        userId: 'user-a',
        deviceToken: 'tokenA',
        isBackgrounded: true,
        payload: { title: 'Test', body: 'Body' },
      };
      const opts2 = {
        userId: 'user-b',
        deviceToken: 'tokenB',
        isBackgrounded: true,
        payload: { title: 'Test', body: 'Body' },
      };

      await notify(opts1);
      await notify(opts2);
      await notify(opts2);

      clearThrottle('user-a');

      expect(getRemainingBudget('user-a')).toBe(4);
      expect(getRemainingBudget('user-b')).toBe(2);
    });

    it('should clear all throttles', async () => {
      const opts = {
        userId: 'user-x',
        deviceToken: 'tokenX',
        isBackgrounded: true,
        payload: { title: 'Test', body: 'Body' },
      };

      await notify(opts);
      expect(getRemainingBudget('user-x')).toBe(3);

      clearAllThrottles();
      expect(getRemainingBudget('user-x')).toBe(4);
    });
  });

  describe('shouldSendNotification', () => {
    it('should return true when player is disconnected with device token', () => {
      const player = createMockPlayer();
      expect(shouldSendNotification(player)).toBe(true);
    });

    it('should return false when player has no device token', () => {
      const player = createMockPlayer({ deviceToken: '' });
      expect(shouldSendNotification(player)).toBe(false);
    });

    it('should return false when player is connected (not backgrounded)', () => {
      const player = createMockPlayer({ connection: 'connected' as any });
      expect(shouldSendNotification(player)).toBe(false);
    });

    it('should return false when player is throttled', async () => {
      const player = createMockPlayer({ userId: 'throttle-check' });

      // Exhaust throttle budget
      for (let i = 0; i < 4; i++) {
        await notify({
          userId: player.userId,
          deviceToken: player.deviceToken,
          isBackgrounded: true,
          payload: { title: 'Test', body: 'Body' },
        });
      }

      expect(shouldSendNotification(player)).toBe(false);
    });
  });

  describe('queueRealtimeNotification', () => {
    it('should send when player meets all conditions', async () => {
      const player = createMockPlayer({ userId: 'queue-test' });
      const result = await queueRealtimeNotification(player, {
        title: 'Birthday!',
        body: 'Your character turned 18!',
        type: 'milestone',
        id: 'birthday_18',
      });
      expect(result.sent).toBe(true);
    });

    it('should not send when player has no device token', async () => {
      const player = createMockPlayer({ deviceToken: '' });
      const result = await queueRealtimeNotification(player, {
        title: 'Test',
        body: 'Test body',
        type: 'life_event',
      });
      expect(result.sent).toBe(false);
      expect(result.reason).toBe('no_device_token');
    });

    it('should not send when player is connected', async () => {
      const player = createMockPlayer({ connection: 'connected' as any });
      const result = await queueRealtimeNotification(player, {
        title: 'Test',
        body: 'Test body',
        type: 'life_event',
      });
      expect(result.sent).toBe(false);
      expect(result.reason).toBe('app_in_foreground');
    });

    it('should decrease remaining budget on successful send', async () => {
      const player = createMockPlayer({ userId: 'budget-queue' });
      expect(getRemainingBudget('budget-queue')).toBe(4);

      await queueRealtimeNotification(player, {
        title: 'Test',
        body: 'Test body',
        type: 'life_event',
      });

      expect(getRemainingBudget('budget-queue')).toBe(3);
    });
  });

  describe('notify edge cases', () => {
    it('should reject when no device token', async () => {
      const result = await notify({
        userId: 'no-token',
        deviceToken: '',
        isBackgrounded: true,
        payload: { title: 'Test', body: 'Body' },
      });
      expect(result.sent).toBe(false);
      expect(result.reason).toBe('no_device_token');
    });

    it('should reject when app is in foreground', async () => {
      const result = await notify({
        userId: 'foreground',
        deviceToken: 'token123',
        isBackgrounded: false,
        payload: { title: 'Test', body: 'Body' },
      });
      expect(result.sent).toBe(false);
      expect(result.reason).toBe('app_in_foreground');
    });
  });
});
