import { describe, it, expect, afterEach } from 'vitest';
import { Person } from '../../../src/models/Person.js';
import { Player } from '../../../src/models/Player.js';
import {
  loadPlayerLifeGoals,
  clearAllLifeGoals,
} from '../../../src/services/retention/lifeGoals.js';

describe('Player', () => {
  const createTestCharacter = (overrides: Partial<Parameters<typeof Person>[0]> = {}) => {
    return new Person({
      id: 'char-1',
      firstname: 'Test',
      lastname: 'Character',
      sex: 'Male',
      ...overrides,
    });
  };

  it('should create a player with required fields', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
    });

    expect(player.userId).toBe('user-123');
    expect(player.character).toBe(character);
    expect(player.c).toBe(character); // Alias
  });

  it('should have default values for optional fields', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
    });

    expect(player.status).toBe('creating');
    expect(player.controller).toBe('inactive');
    expect(player.connection).toBe('connected');
    expect(player.hourOfDay).toBe(8);
    expect(player.minuteOfHour).toBe(0);
    expect(player.season).toBe('spring');
    expect(player.gameSpeed).toBe(10000);
    expect(player.diamonds).toBe(0);
    expect(player.money).toBe(0);
    expect(player.r).toEqual([]);
    expect(player.relationships).toEqual([]);
    expect(player.conversations).toEqual([]);
  });

  it('should handle game status transitions', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
    });

    expect(player.status).toBe('creating');

    player.status = 'playing';
    expect(player.status).toBe('playing');

    player.status = 'paused';
    expect(player.status).toBe('paused');

    player.status = 'dead';
    expect(player.status).toBe('dead');
  });

  it('should calculate isActive correctly', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      controller: 'active',
      status: 'playing',
    });

    expect(player.isActive).toBe(true);

    player.status = 'paused';
    expect(player.isActive).toBe(false);

    player.status = 'playing';
    player.controller = 'inactive';
    expect(player.isActive).toBe(false);
  });

  it('should handle time properties', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      hourOfDay: 14,
      minuteOfHour: 30,
      dayOfYear: 100,
      dayOfWeek: 3,
      monthOfYear: 4,
    });

    expect(player.hourOfDay).toBe(14);
    expect(player.minuteOfHour).toBe(30);
    expect(player.dayOfYear).toBe(100);
    expect(player.dayOfWeek).toBe(3);
    expect(player.monthOfYear).toBe(4);
  });

  it('should handle season transitions', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
    });

    const seasons: Array<'spring' | 'summer' | 'autumn' | 'winter'> = ['spring', 'summer', 'autumn', 'winter'];
    seasons.forEach(season => {
      player.season = season;
      expect(player.season).toBe(season);
    });
  });

  it('should handle game speed settings', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      gameSpeed: 1000,
    });

    expect(player.gameSpeed).toBe(1000);
    expect(player.getTicksForSpeed()).toBe(1000);

    player.gameSpeed = 500;
    expect(player.getTicksForSpeed()).toBe(500);
  });

  it('should track relationships array', () => {
    const friend = createTestCharacter({ id: 'friend-1', firstname: 'Friend' });
    const partner = createTestCharacter({ id: 'partner-1', firstname: 'Partner' });

    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      r: [friend, partner],
    });

    expect(player.r).toHaveLength(2);
    expect(player.r[0].firstname).toBe('Friend');
    expect(player.r[1].firstname).toBe('Partner');
  });

  it('should handle conversations array', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      conversations: [
        { id: 'conv-1', conversation: [], question: 0, unread: false },
        { id: 'conv-2', conversation: [], question: 0, unread: true },
      ],
    });

    expect(player.conversations).toHaveLength(2);
    expect(player.conversations[1].unread).toBe(true);
  });

  it('should serialize to JSON correctly', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      status: 'playing',
      hourOfDay: 12,
      season: 'summer',
    });

    const json = player.toJSON();

    expect(json.userId).toBe('user-123');
    expect(json.status).toBe('playing');
    expect(json.hourOfDay).toBe(12);
    expect(json.season).toBe('summer');
    expect(json.c).toBeDefined();
  });

  it('should include conversation type in playerObject serialization for legacy clients', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      conversations: [
        {
          id: 'conv-1',
          character: 'npc-1',
          cType: 'chat',
          conversation: [],
          question: 0,
          unread: false,
        },
      ],
    });

    const json = player.toJSON() as {
      conversations?: Array<{ type?: string }>;
    };

    expect(json.conversations).toHaveLength(1);
    expect(json.conversations?.[0].type).toBe('conversationEvent');
  });

  it('should provide relData defaults required by legacy iOS decoding', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      relData: [
        {
          id: 'rel-1',
          person1: 'player-1',
          person2: 'npc-1',
          startDate: '2026-02-27',
          relationshipStatus: 'Dating',
        },
      ],
    });

    const json = player.toJSON() as {
      relData?: Array<{
        relationshipNotes?: string;
        eventsLog?: string[];
        commonInterests?: string[];
        challenges?: string[];
        futurePlans?: string[];
      }>;
    };

    expect(json.relData).toHaveLength(1);
    expect(json.relData?.[0].relationshipNotes).toBe('');
    expect(json.relData?.[0].eventsLog).toEqual([]);
    expect(json.relData?.[0].commonInterests).toEqual([]);
    expect(json.relData?.[0].challenges).toEqual([]);
    expect(json.relData?.[0].futurePlans).toEqual([]);
  });

  it('should track events and asked questions', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
    });

    expect(player.events).toBeInstanceOf(Set);
    expect(player.askedQuestions).toBeInstanceOf(Set);

    player.events.add('event-1');
    player.askedQuestions.add('question-1');

    expect(player.events.has('event-1')).toBe(true);
    expect(player.askedQuestions.has('question-1')).toBe(true);
  });

  it('should handle game loop properties', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      ticks: 100,
      weekDayText: 'Wednesday',
      weekend: true,
      summerVacation: true,
    });

    expect(player.ticks).toBe(100);
    expect(player.weekDayText).toBe('Wednesday');
    expect(player.weekend).toBe(true);
    expect(player.summerVacation).toBe(true);
  });

  it('should handle offline stats', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      offlineStats: { minutesOffline: 60, lastOnline: new Date() },
    });

    expect(player.offlineStats.minutesOffline).toBe(60);
    expect(player.offlineStats.lastOnline).toBeDefined();
  });

  it('should handle message queue and log', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
    });

    player.messageQueue.push('message-1');
    player.messageLog.push('log-1');

    expect(player.messageQueue).toHaveLength(1);
    expect(player.messageLog).toHaveLength(1);
  });

  it('should handle device token', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      deviceToken: 'abc123token',
    });

    expect(player.deviceToken).toBe('abc123token');
  });

  it('should handle locations', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      l: [
        { id: 'home', type: 'residence', name: 'Home' },
        { id: 'work', type: 'workplace', name: 'Office' },
      ],
    });

    expect(player.l).toHaveLength(2);
    expect(player.l[0].name).toBe('Home');
    expect(player.l[1].type).toBe('workplace');
  });

  it('should migrate legacy messaging modifier relData entries to messagingModifiers', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      relData: [
        {
          characterId: 'char-1',
          messaging_modifiers: {
            verbosity: 70,
            formality: 30,
          },
        },
        {
          id: 'rel-1',
          person1: 'char-1',
          person2: 'partner-1',
          startDate: '2026-01-01',
          relationshipStatus: 'Dating',
          description: 'You are dating.',
          relationshipScore: 50,
          eventsLog: [],
        },
      ],
    });

    expect(player.relData).toHaveLength(1);
    expect((player as unknown as { messagingModifiers: Array<{ characterId: string; messaging_modifiers?: { verbosity?: number } }> }).messagingModifiers).toHaveLength(1);
    expect(
      (player as unknown as { messagingModifiers: Array<{ characterId: string; messaging_modifiers?: { verbosity?: number } }> }).messagingModifiers[0].messaging_modifiers?.verbosity
    ).toBe(70);

    const json = player.toJSON() as {
      relData?: unknown[];
      messagingModifiers?: Array<{ characterId: string }>;
    };
    expect(json.relData).toHaveLength(1);
    expect(json.messagingModifiers).toHaveLength(1);
    expect(json.messagingModifiers?.[0].characterId).toBe('char-1');
  });

  it('should handle active dilemmas', () => {
    const character = createTestCharacter();
    const player = new Player({
      userId: 'user-123',
      character,
      activeDilemmas: [{ id: 'dilemma-1', title: 'Test Dilemma' }],
    });

    expect(player.activeDilemmas).toHaveLength(1);
  });

  it('should use c alias for character', () => {
    const character = createTestCharacter({ firstname: 'Original' });
    const player = new Player({
      userId: 'user-123',
      character,
    });

    expect(player.c.firstname).toBe('Original');

    const newCharacter = createTestCharacter({ firstname: 'New' });
    player.c = newCharacter;

    expect(player.character.firstname).toBe('New');
    expect(player.c.firstname).toBe('New');
  });

  describe('toJSON lifeGoals embed', () => {
    afterEach(() => {
      clearAllLifeGoals();
    });

    it('embeds FORMATTED active goals (with non-empty title + progressPercent) so iOS does not cache blank cells', () => {
      const character = createTestCharacter();
      const player = new Player({ userId: 'user-goals-1', character });

      // Seed an active goal the way the persisted blob would: just {id, current,
      // progressPercent} — no title/icon. The toJSON embed must enrich it via
      // formatActiveGoals so the cached playerObject matches lifeGoalsUpdate.
      loadPlayerLifeGoals('user-goals-1', {
        active: [{ id: 'make_three_friends', current: 1, progressPercent: 33 }],
        completed: [],
        lifeScore: 30,
      });

      const json = player.toJSON() as {
        lifeGoals: {
          active: Array<{
            id: string;
            title: string;
            description: string;
            icon: string;
            target: number;
            reward: number;
            lifeScore: number;
            current: number;
            progressPercent: number;
          }>;
          completed: unknown[];
          lifeScore: number;
        };
      };

      expect(json.lifeGoals.active).toHaveLength(1);
      const goal = json.lifeGoals.active[0];
      expect(goal.id).toBe('make_three_friends');
      // The bug: title was "" because the raw persisted blob lacked it.
      expect(goal.title).toBe('Build Your Circle');
      expect(goal.title.length).toBeGreaterThan(0);
      expect(goal.progressPercent).toBe(33);
      expect(goal.current).toBe(1);
      // Full lifeGoalsUpdate field shape present so iOS LifeGoalsSnapshot decodes.
      expect(typeof goal.description).toBe('string');
      expect(typeof goal.icon).toBe('string');
      expect(goal.target).toBe(3);
      expect(json.lifeGoals.lifeScore).toBe(30);
    });
  });
});
