/**
 * Message Handlers Tests
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Person } from '../../src/models/Person';
import { Player } from '../../src/models/Player';
import { handleDeviceToken } from '../../src/handlers/character';
import { handleCompleteOnboarding, handleTutorialStepComplete } from '../../src/handlers/retention';
import {
  handleQuitHabit,
  handleStopQuitHabit,
  handleApplyJob,
  handleQuitJob,
} from '../../src/handlers/activities';
import { handlePurchaseItem } from '../../src/handlers/purchases';
import { createHabit } from '../../src/services/health/habit_manager.js';
import { getStoreItems } from '../../src/services/shop/shop_manager.js';

// Mock PlayerSession for testing handlers
class MockPlayerSession {
  player: Player;
  sentMessages: any[] = [];

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

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

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

  async savePlayer() {
    return undefined;
  }

  getLastMessage() {
    return this.sentMessages[this.sentMessages.length - 1];
  }

  clearMessages() {
    this.sentMessages = [];
  }
}

describe('Message Handlers', () => {
  let mockPlayer: Player;
  let mockSession: MockPlayerSession;

  beforeEach(() => {
    const character = new Person({
      id: 'char-1',
      firstname: 'Test',
      lastname: 'Player',
      ageYears: 25,
      money: 1000,
      diamonds: 50,
      energy: 80,
      health: 90,
      happiness: 70,
    });

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

    mockSession = new MockPlayerSession(mockPlayer);
  });

  describe('Handler response format', () => {
    it('should send error messages with correct format', () => {
      mockSession.send({
        type: 'error',
        message: 'Something went wrong',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('error');
      expect(msg.message).toBeDefined();
    });

    it('should send success messages with correct format', () => {
      mockSession.send({
        type: 'success',
        message: 'Action completed',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('success');
      expect(msg.message).toBeDefined();
    });

    it('should send player object updates', () => {
      mockSession.sendPlayerObject();

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('playerObject');
      expect(msg.player).toBeDefined();
    });
  });

  describe('Handler player access', () => {
    it('should access player character data', () => {
      const character = mockSession.player.c;

      expect(character.firstname).toBe('Test');
      expect(character.lastname).toBe('Player');
      expect(character.ageYears).toBe(25);
    });

    it('should access player resources', () => {
      const character = mockSession.player.c;

      expect(character.money).toBe(1000);
      expect(character.diamonds).toBe(50);
      expect(character.energy).toBe(80);
    });

    it('should access player relationships', () => {
      expect(mockSession.player.r).toEqual([]);
    });

    it('should access game state', () => {
      expect(mockSession.player.status).toBe('playing');
      expect(mockSession.player.date).toBe('2024-06-15');
      expect(mockSession.player.hourOfDay).toBe(10);
    });
  });

  describe('Tutorial handlers', () => {
    it('accepts tutorial step completion for UUID-style user IDs', async () => {
      mockPlayer.userId = 'ios-device-id-longer-than-thirty-six-characters';

      await handleTutorialStepComplete({ step: 3 }, mockSession as any);

      expect(mockSession.getLastMessage()).toEqual({
        type: 'tutorialStepUpdated',
        step: 3,
      });
    });

    it('completes onboarding for UUID-style user IDs', async () => {
      mockPlayer.userId = 'ios-device-id-longer-than-thirty-six-characters';

      await handleCompleteOnboarding({}, mockSession as any);

      expect(mockSession.getLastMessage()).toMatchObject({
        type: 'onboardingComplete',
      });
    });

    it('does not award onboarding twice after server-side completion is persisted', async () => {
      mockPlayer.userId = 'ios-device-id-longer-than-thirty-six-characters';
      mockPlayer.onboardingComplete = true;

      await handleCompleteOnboarding({}, mockSession as any);

      expect(mockSession.getLastMessage()).toEqual({
        type: 'onboardingAlreadyComplete',
        isComplete: true,
      });
      expect(mockPlayer.c.diamonds).toBe(50);
    });
  });

  describe('Device token handler', () => {
    it('accepts the legacy iOS string payload', async () => {
      await handleDeviceToken('ios-token-123', mockSession as any);

      expect(mockPlayer.deviceToken).toBe('ios-token-123');
    });

    it('accepts the nested object payload', async () => {
      await handleDeviceToken({ token: 'object-token-456' }, mockSession as any);

      expect(mockPlayer.deviceToken).toBe('object-token-456');
    });
  });

  describe('Handler message queue', () => {
    it('should accumulate sent messages', () => {
      mockSession.send({ type: 'test1' });
      mockSession.send({ type: 'test2' });
      mockSession.send({ type: 'test3' });

      expect(mockSession.sentMessages).toHaveLength(3);
    });

    it('should clear messages correctly', () => {
      mockSession.send({ type: 'test' });
      mockSession.clearMessages();

      expect(mockSession.sentMessages).toHaveLength(0);
    });
  });

  describe('Purchase handler patterns', () => {
    it('should handle successful purchase response', () => {
      mockSession.send({
        type: 'purchaseComplete',
        success: true,
        itemId: 'item-123',
        message: 'Purchase successful',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('purchaseComplete');
      expect(msg.success).toBe(true);
      expect(msg.itemId).toBeDefined();
    });

    it('should handle failed purchase response', () => {
      mockSession.send({
        type: 'error',
        message: 'Not enough money',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('error');
    });
  });

  describe('Activity handler patterns', () => {
    it('should handle activity enrollment response', () => {
      mockSession.send({
        type: 'extracurricularApplied',
        success: true,
        message: 'Successfully enrolled',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('extracurricularApplied');
      expect(msg.success).toBe(true);
    });

    it('should handle job application response', () => {
      mockSession.send({
        type: 'jobApplied',
        success: true,
        message: 'You got the job!',
        job: { id: 'job-1', title: 'Developer' },
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('jobApplied');
      expect(msg.job).toBeDefined();
    });
  });

  describe('Conversation handler patterns', () => {
    it('should handle conversation event response', () => {
      mockSession.send({
        type: 'conversationEvent',
        id: 'conv-123',
        character: 'char-456',
        conversation: [],
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('conversationEvent');
      expect(msg.id).toBeDefined();
    });

    it('should handle message sent response', () => {
      mockSession.send({
        type: 'messageSent',
        success: true,
        conversationId: 'conv-123',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('messageSent');
      expect(msg.success).toBe(true);
    });
  });

  describe('Romance handler patterns', () => {
    it('should handle date request response', () => {
      mockSession.send({
        type: 'dateRequested',
        success: true,
        message: 'Date scheduled!',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('dateRequested');
      expect(msg.success).toBe(true);
    });

    it('should handle match response', () => {
      mockSession.send({
        type: 'matchFound',
        match: {
          id: 'match-123',
          firstname: 'Alex',
          compatibilityScore: 85,
        },
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('matchFound');
      expect(msg.match).toBeDefined();
      expect(msg.match.compatibilityScore).toBeDefined();
    });
  });

  describe('Retention handler patterns', () => {
    it('should handle daily reward claim response', () => {
      mockSession.send({
        type: 'dailyRewardClaimed',
        success: true,
        reward: {
          day: 1,
          diamonds: 10,
          money: 100,
        },
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('dailyRewardClaimed');
      expect(msg.reward).toBeDefined();
    });

    it('should handle achievement unlock response', () => {
      mockSession.send({
        type: 'achievementUnlocked',
        achievement: {
          id: 'first_steps',
          title: 'First Steps',
          description: 'Start your journey',
        },
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('achievementUnlocked');
      expect(msg.achievement).toBeDefined();
    });

    it('should handle quest completion response', () => {
      mockSession.send({
        type: 'questRewardClaimed',
        success: true,
        questId: 'quest-123',
        reward: { diamonds: 5 },
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('questRewardClaimed');
      expect(msg.questId).toBeDefined();
    });
  });

  describe('Data handler patterns', () => {
    it('should handle statistics response', () => {
      mockSession.send({
        type: 'playerStatistics',
        statistics: {
          gamesPlayed: 10,
          totalPlayTime: 3600,
        },
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('playerStatistics');
      expect(msg.statistics).toBeDefined();
    });

    it('should handle data export response', () => {
      mockSession.send({
        type: 'dataExportComplete',
        success: true,
        data: {},
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('dataExportComplete');
      expect(msg.success).toBe(true);
    });

    it('should handle account deletion response', () => {
      mockSession.send({
        type: 'accountDeletionScheduled',
        success: true,
        scheduledAt: '2024-07-15T00:00:00Z',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('accountDeletionScheduled');
      expect(msg.scheduledAt).toBeDefined();
    });
  });

  describe('Monetization handler patterns', () => {
    it('should handle energy refill response', () => {
      mockSession.send({
        type: 'energyRefillComplete',
        success: true,
        newEnergy: 100,
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('energyRefillComplete');
      expect(msg.newEnergy).toBeDefined();
    });

    it('should handle time skip response', () => {
      mockSession.send({
        type: 'timeSkipComplete',
        success: true,
        newDate: '2024-06-20',
        events: [],
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('timeSkipComplete');
      expect(msg.newDate).toBeDefined();
    });

    it('should handle tier info response', () => {
      mockSession.send({
        type: 'energyRefillTiers',
        tiers: [
          { id: 'small', energy: 50, diamonds: 10 },
          { id: 'medium', energy: 100, diamonds: 18 },
        ],
      });

      const msg = mockSession.getLastMessage();

      expect(msg.type).toBe('energyRefillTiers');
      expect(msg.tiers).toHaveLength(2);
    });
  });

  describe('Habit handlers', () => {
    beforeEach(() => {
      const personAny = mockPlayer.c as unknown as { habits?: ReturnType<typeof createHabit>[] };
      personAny.habits = [createHabit('smoking', 'The habit of smoking is taking a toll on your health.')];
    });

    it('starts quitting when iOS sends the habit name as a string payload', async () => {
      await handleQuitHabit('smoking', mockSession as any);

      const habit = (mockPlayer.c as unknown as { habits?: ReturnType<typeof createHabit>[] }).habits?.[0];
      expect(habit?.status).toBe('quitting');
      expect(mockSession.sentMessages.some((msg) => msg.type === 'playerObject')).toBe(true);
      expect(mockSession.getLastMessage()).toMatchObject({
        type: 'habitQuitting',
        success: true,
        habitId: 'smoking',
      });
    });

    it('starts quitting when Android sends a habitId object payload', async () => {
      await handleQuitHabit({ habitId: 'smoking' }, mockSession as any);

      const habit = (mockPlayer.c as unknown as { habits?: ReturnType<typeof createHabit>[] }).habits?.[0];
      expect(habit?.status).toBe('quitting');
    });

    it('stops quitting when iOS sends the habit name as a string payload', async () => {
      await handleQuitHabit('smoking', mockSession as any);
      mockSession.clearMessages();

      await handleStopQuitHabit('smoking', mockSession as any);

      const habit = (mockPlayer.c as unknown as { habits?: ReturnType<typeof createHabit>[] }).habits?.[0];
      expect(habit?.status).toBe('active');
      expect(habit?.quitProgress).toBe(0);
      expect(mockSession.getLastMessage()).toMatchObject({
        type: 'habitQuitStopped',
        success: true,
        habitId: 'smoking',
      });
    });
  });

  describe('Job and purchase payload normalization', () => {
    it('rejects applyForJob when payload has no job id', async () => {
      await handleApplyJob({}, mockSession as any);

      expect(mockSession.getLastMessage()).toMatchObject({
        type: 'error',
        message: 'Job not found',
      });
    });

    it('rejects quitJob when payload has no job id', async () => {
      await handleQuitJob({}, mockSession as any);

      expect(mockSession.getLastMessage()).toMatchObject({
        type: 'error',
        message: 'Job not found',
      });
    });

    it('rejects purchaseItem when payload has no item id', async () => {
      await handlePurchaseItem({}, mockSession as any);

      expect(mockSession.getLastMessage()).toMatchObject({
        type: 'error',
        message: 'Failed to purchase item',
      });
    });

    it('accepts iOS string payload for purchaseItem', async () => {
      const item = getStoreItems()[0];
      mockPlayer.c.money = item.price + 500;
      const moneyBefore = mockPlayer.c.money;

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

      expect(
        mockSession.sentMessages.some(
          (m) => m.type === 'purchaseComplete' && m.success === true && m.itemId === item.id
        )
      ).toBe(true);
      expect(mockPlayer.c.money).toBe(moneyBefore - item.price);
    });
  });

  describe('Error handling patterns', () => {
    it('should include error type', () => {
      mockSession.send({
        type: 'error',
        errorType: 'validation',
        message: 'Invalid input',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.errorType).toBe('validation');
    });

    it('should include error code', () => {
      mockSession.send({
        type: 'error',
        code: 'INSUFFICIENT_FUNDS',
        message: 'Not enough money',
      });

      const msg = mockSession.getLastMessage();

      expect(msg.code).toBe('INSUFFICIENT_FUNDS');
    });
  });
});
