const { EcastShardClient } = require('../../backend/utils/ecast-shard-client');

describe('EcastShardClient', () => {
  describe('parsePlayersFromHere', () => {
    test('counts only player roles, excludes host and shard', () => {
      const here = {
        '1': { id: 1, roles: { host: {} } },
        '2': { id: 2, roles: { player: { name: 'Alice' } } },
        '3': { id: 3, roles: { player: { name: 'Bob' } } },
        '5': { id: 5, roles: { shard: {} } },
      };
      const result = EcastShardClient.parsePlayersFromHere(here);
      expect(result.playerCount).toBe(2);
      expect(result.playerNames).toEqual(['Alice', 'Bob']);
    });

    test('returns zero for empty here or host-only', () => {
      const here = { '1': { id: 1, roles: { host: {} } } };
      const result = EcastShardClient.parsePlayersFromHere(here);
      expect(result.playerCount).toBe(0);
      expect(result.playerNames).toEqual([]);
    });

    test('handles null or undefined here', () => {
      expect(EcastShardClient.parsePlayersFromHere(null).playerCount).toBe(0);
      expect(EcastShardClient.parsePlayersFromHere(undefined).playerCount).toBe(0);
    });
  });

  describe('parseRoomEntity', () => {
    test('extracts lobby state from room entity val', () => {
      const roomVal = {
        state: 'Lobby',
        lobbyState: 'CanStart',
        gameCanStart: true,
        gameIsStarting: false,
        gameFinished: false,
      };
      const result = EcastShardClient.parseRoomEntity(roomVal);
      expect(result.gameState).toBe('Lobby');
      expect(result.lobbyState).toBe('CanStart');
      expect(result.gameCanStart).toBe(true);
      expect(result.gameStarted).toBe(false);
      expect(result.gameFinished).toBe(false);
    });

    test('detects game started from Gameplay state', () => {
      const roomVal = { state: 'Gameplay', lobbyState: 'Countdown', gameCanStart: true, gameIsStarting: false, gameFinished: false };
      const result = EcastShardClient.parseRoomEntity(roomVal);
      expect(result.gameStarted).toBe(true);
    });

    test('detects game started from non-Lobby state (Pack 7 Logo)', () => {
      const roomVal = { state: 'Logo', locale: 'en', platformId: 'PS4' };
      const result = EcastShardClient.parseRoomEntity(roomVal);
      expect(result.gameStarted).toBe(true);
      expect(result.gameState).toBe('Logo');
      expect(result.lobbyState).toBeNull();
      expect(result.gameFinished).toBe(false);
    });

    test('detects game finished', () => {
      const roomVal = { state: 'Gameplay', lobbyState: '', gameCanStart: true, gameIsStarting: false, gameFinished: true };
      const result = EcastShardClient.parseRoomEntity(roomVal);
      expect(result.gameFinished).toBe(true);
    });
  });

  describe('parsePlayerJoinFromTextDescriptions', () => {
    test('extracts player name from join description', () => {
      const val = {
        latestDescriptions: [
          { category: 'TEXT_DESCRIPTION_PLAYER_JOINED', text: 'Charlie joined.' }
        ]
      };
      const result = EcastShardClient.parsePlayerJoinFromTextDescriptions(val);
      expect(result).toEqual([{ name: 'Charlie', isVIP: false }]);
    });

    test('extracts VIP join', () => {
      const val = {
        latestDescriptions: [
          { category: 'TEXT_DESCRIPTION_PLAYER_JOINED_VIP', text: 'Alice joined and is the VIP.' }
        ]
      };
      const result = EcastShardClient.parsePlayerJoinFromTextDescriptions(val);
      expect(result).toEqual([{ name: 'Alice', isVIP: true }]);
    });

    test('returns empty array for no joins', () => {
      const val = { latestDescriptions: [] };
      expect(EcastShardClient.parsePlayerJoinFromTextDescriptions(val)).toEqual([]);
    });

    test('handles null/undefined val', () => {
      expect(EcastShardClient.parsePlayerJoinFromTextDescriptions(null)).toEqual([]);
      expect(EcastShardClient.parsePlayerJoinFromTextDescriptions(undefined)).toEqual([]);
    });
  });

  describe('constructor', () => {
    test('initializes with correct defaults', () => {
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8, onEvent: () => {}
      });
      expect(client.sessionId).toBe(1);
      expect(client.gameId).toBe(5);
      expect(client.roomCode).toBe('TEST');
      expect(client.maxPlayers).toBe(8);
      expect(client.playerCount).toBe(0);
      expect(client.playerNames).toEqual([]);
      expect(client.gameStarted).toBe(false);
      expect(client.gameFinished).toBe(false);
      expect(client.appTag).toBeNull();
      expect(client.ws).toBeNull();
    });
  });

  describe('handleWelcome', () => {
    test('parses welcome message and sets internal state', () => {
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8, onEvent: () => {}
      });

      client.handleWelcome({
        id: 7,
        secret: 'abc-123',
        reconnect: false,
        entities: {
          room: ['object', { key: 'room', val: { state: 'Lobby', lobbyState: 'CanStart', gameCanStart: true, gameIsStarting: false, gameFinished: false }, version: 0, from: 1 }, { locked: false }]
        },
        here: {
          '1': { id: 1, roles: { host: {} } },
          '2': { id: 2, roles: { player: { name: 'Alice' } } },
          '3': { id: 3, roles: { player: { name: 'Bob' } } },
        }
      });

      expect(client.shardId).toBe(7);
      expect(client.secret).toBe('abc-123');
      expect(client.playerCount).toBe(2);
      expect(client.playerNames).toEqual(['Alice', 'Bob']);
      expect(client.gameState).toBe('Lobby');
      expect(client.lobbyState).toBe('CanStart');
      expect(client.gameStarted).toBe(false);
    });

    test('parses bc:room entity when room key is absent', () => {
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8, onEvent: () => {}
      });

      client.handleWelcome({
        id: 5,
        secret: 'xyz-789',
        reconnect: false,
        entities: {
          'bc:room': ['object', { key: 'bc:room', val: { state: 'Lobby', lobbyState: 'CanStart', gameCanStart: true, gameIsStarting: false, gameFinished: false }, version: 0, from: 1 }, { locked: false }],
          audience: ['crdt/pn-counter', [], { locked: false }],
        },
        here: {
          '1': { id: 1, roles: { host: {} } },
          '3': { id: 3, roles: { player: { name: 'HÂM' } } },
          '4': { id: 4, roles: { player: { name: 'FGHFGHY' } } },
        }
      });

      expect(client.playerCount).toBe(2);
      expect(client.playerNames).toEqual(['HÂM', 'FGHFGHY']);
      expect(client.gameState).toBe('Lobby');
      expect(client.lobbyState).toBe('CanStart');
    });
  });

  describe('handleEntityUpdate', () => {
    test('updates room state on room entity update', () => {
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8, onEvent: () => {}
      });
      client.gameState = 'Lobby';
      client.lobbyState = 'WaitingForMore';

      client.handleEntityUpdate({
        key: 'room',
        val: { state: 'Gameplay', lobbyState: 'Countdown', gameCanStart: true, gameIsStarting: true, gameFinished: false },
        version: 5,
        from: 1
      });

      expect(client.gameState).toBe('Gameplay');
      expect(client.gameStarted).toBe(true);
    });

    test('handles bc:room key as room update', () => {
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8, onEvent: () => {}
      });

      client.handleEntityUpdate({
        key: 'bc:room',
        val: { state: 'Lobby', lobbyState: 'CanStart', gameCanStart: true, gameIsStarting: false, gameFinished: false },
        version: 1,
        from: 1
      });

      expect(client.lobbyState).toBe('CanStart');
    });
  });

  describe('event broadcasting', () => {
    let events;
    let client;

    beforeEach(() => {
      events = [];
      client = new EcastShardClient({
        sessionId: 1,
        gameId: 5,
        roomCode: 'TEST',
        maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
    });

    describe('handleWelcome broadcasts room.connected', () => {
      test('broadcasts room.connected with initial state', () => {
        client.appTag = 'drawful2international';
        client.handleWelcome({
          id: 7,
          secret: 'abc',
          reconnect: false,
          entities: {
            room: ['object', { key: 'room', val: { state: 'Lobby', lobbyState: 'CanStart', gameCanStart: true, gameIsStarting: false, gameFinished: false }, version: 0, from: 1 }, { locked: false }]
          },
          here: {
            '1': { id: 1, roles: { host: {} } },
            '2': { id: 2, roles: { player: { name: 'Alice' } } },
          }
        });

        expect(events).toHaveLength(1);
        expect(events[0].type).toBe('room.connected');
        expect(events[0].data.playerCount).toBe(1);
        expect(events[0].data.players).toEqual(['Alice']);
        expect(events[0].data.lobbyState).toBe('CanStart');
      });
    });

    describe('handleEntityUpdate broadcasts events', () => {
      test('broadcasts lobby.updated on lobbyState change', () => {
        client.lobbyState = 'WaitingForMore';
        client.gameState = 'Lobby';

        client.handleEntityUpdate({
          key: 'room',
          val: { state: 'Lobby', lobbyState: 'CanStart', gameCanStart: true, gameIsStarting: false, gameFinished: false },
          version: 2, from: 1,
        });

        expect(events).toHaveLength(1);
        expect(events[0].type).toBe('lobby.updated');
        expect(events[0].data.lobbyState).toBe('CanStart');
      });

      test('broadcasts game.started on state transition to Gameplay', () => {
        client.lobbyState = 'Countdown';
        client.gameState = 'Lobby';
        client.gameStarted = false;
        client.playerCount = 4;
        client.playerNames = ['A', 'B', 'C', 'D'];

        client.handleEntityUpdate({
          key: 'room',
          val: { state: 'Gameplay', lobbyState: 'Countdown', gameCanStart: true, gameIsStarting: true, gameFinished: false },
          version: 5, from: 1,
        });

        const startEvents = events.filter(e => e.type === 'game.started');
        expect(startEvents).toHaveLength(1);
        expect(startEvents[0].data.playerCount).toBe(4);
        expect(startEvents[0].data.players).toEqual(['A', 'B', 'C', 'D']);
      });

      test('broadcasts game.started on state transition to Logo (Pack 7)', () => {
        client.lobbyState = 'Countdown';
        client.gameState = 'Lobby';
        client.gameStarted = false;
        client.playerCount = 4;
        client.playerNames = ['A', 'B', 'C', 'D'];

        client.handleEntityUpdate({
          key: 'room',
          val: { state: 'Logo', locale: 'en', platformId: 'PS4' },
          version: 14, from: 1,
        });

        const startEvents = events.filter(e => e.type === 'game.started');
        expect(startEvents).toHaveLength(1);
        expect(startEvents[0].data.playerCount).toBe(4);
        expect(client.gameState).toBe('Logo');
        expect(client.gameStarted).toBe(true);
      });

      test('does not broadcast game.started if already started', () => {
        client.gameStarted = true;
        client.gameState = 'Gameplay';

        client.handleEntityUpdate({
          key: 'room',
          val: { state: 'Gameplay', lobbyState: '', gameCanStart: true, gameIsStarting: false, gameFinished: false },
          version: 10, from: 1,
        });

        expect(events.filter(e => e.type === 'game.started')).toHaveLength(0);
      });

      test('broadcasts game.ended on gameFinished transition', () => {
        client.gameStarted = true;
        client.gameState = 'Gameplay';
        client.gameFinished = false;
        client.playerCount = 3;
        client.playerNames = ['X', 'Y', 'Z'];

        client.handleEntityUpdate({
          key: 'room',
          val: { state: 'Gameplay', lobbyState: '', gameCanStart: true, gameIsStarting: false, gameFinished: true },
          version: 20, from: 1,
        });

        const endEvents = events.filter(e => e.type === 'game.ended');
        expect(endEvents).toHaveLength(1);
        expect(endEvents[0].data.playerCount).toBe(3);
      });

      test('broadcasts lobby.player-joined from textDescriptions', () => {
        client.playerNames = ['Alice'];
        client.playerCount = 1;

        client.handleEntityUpdate({
          key: 'textDescriptions',
          val: {
            latestDescriptions: [
              { category: 'TEXT_DESCRIPTION_PLAYER_JOINED', text: 'Bob joined.' }
            ]
          },
          version: 3, from: 1,
        });

        expect(events).toHaveLength(1);
        expect(events[0].type).toBe('lobby.player-joined');
        expect(events[0].data.playerName).toBe('Bob');
        expect(events[0].data.playerCount).toBe(2);
        expect(events[0].data.players).toEqual(['Alice', 'Bob']);
      });

      test('does not broadcast duplicate player join', () => {
        client.playerNames = ['Alice', 'Bob'];
        client.playerCount = 2;

        client.handleEntityUpdate({
          key: 'textDescriptions',
          val: {
            latestDescriptions: [
              { category: 'TEXT_DESCRIPTION_PLAYER_JOINED', text: 'Bob joined.' }
            ]
          },
          version: 4, from: 1,
        });

        expect(events).toHaveLength(0);
      });
    });

    describe('handleClientConnected', () => {
      test('broadcasts lobby.player-joined for new player connection', () => {
        client.playerNames = ['Alice'];
        client.playerCount = 1;

        client.handleClientConnected({
          id: 3,
          roles: { player: { name: 'Charlie' } },
        });

        expect(events).toHaveLength(1);
        expect(events[0].type).toBe('lobby.player-joined');
        expect(events[0].data.playerName).toBe('Charlie');
        expect(events[0].data.playerCount).toBe(2);
      });

      test('ignores non-player connections', () => {
        client.handleClientConnected({
          id: 5,
          roles: { shard: {} },
        });

        expect(events).toHaveLength(0);
      });

      test('ignores duplicate player connection', () => {
        client.playerNames = ['Alice'];
        client.playerCount = 1;

        client.handleClientConnected({
          id: 2,
          roles: { player: { name: 'Alice' } },
        });

        expect(events).toHaveLength(0);
      });
    });
  });

  describe('buildReconnectUrl', () => {
    test('uses stored secret and id', () => {
      const client = new EcastShardClient({
        sessionId: 1,
        gameId: 1,
        roomCode: 'TEST',
        maxPlayers: 8,
        onEvent: () => {},
      });
      client.secret = 'abc-123';
      client.shardId = 5;
      client.host = 'ecast-prod-use2.jackboxgames.com';

      const url = client.buildReconnectUrl();
      expect(url).toContain('secret=abc-123');
      expect(url).toContain('id=5');
      expect(url).toContain('role=shard');
      expect(url).toContain('ecast-prod-use2.jackboxgames.com');
    });
  });

  describe('getSnapshot', () => {
    test('returns correct shape with current state', () => {
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'LSBN', maxPlayers: 8, onEvent: () => {}
      });
      client.appTag = 'drawful2international';
      client.playerCount = 3;
      client.playerNames = ['Alice', 'Bob', 'Charlie'];
      client.lobbyState = 'CanStart';
      client.gameState = 'Lobby';
      client.gameStarted = false;
      client.gameFinished = false;

      const snapshot = client.getSnapshot();

      expect(snapshot).toEqual({
        sessionId: 1,
        gameId: 5,
        roomCode: 'LSBN',
        appTag: 'drawful2international',
        maxPlayers: 8,
        playerCount: 3,
        players: ['Alice', 'Bob', 'Charlie'],
        lobbyState: 'CanStart',
        gameState: 'Lobby',
        gameStarted: false,
        gameFinished: false,
        monitoring: true,
      });
    });

    test('returns a defensive copy of playerNames', () => {
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8, onEvent: () => {}
      });
      client.playerNames = ['Alice'];

      const snapshot = client.getSnapshot();
      snapshot.players.push('Mutated');

      expect(client.playerNames).toEqual(['Alice']);
    });
  });

  describe('startStatusBroadcast / stopStatusBroadcast', () => {
    beforeEach(() => jest.useFakeTimers());
    afterEach(() => jest.useRealTimers());

    function stubRefresh(client) {
      client._refreshPlayerCount = () => Promise.resolve();
    }

    test('broadcasts game.status every 20 seconds', async () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      stubRefresh(client);
      client.playerCount = 2;
      client.playerNames = ['A', 'B'];
      client.gameState = 'Lobby';

      client.startStatusBroadcast();

      jest.advanceTimersByTime(20000);
      await Promise.resolve();
      expect(events).toHaveLength(1);
      expect(events[0].type).toBe('game.status');
      expect(events[0].data.monitoring).toBe(true);

      jest.advanceTimersByTime(20000);
      await Promise.resolve();
      expect(events).toHaveLength(2);

      client.stopStatusBroadcast();

      jest.advanceTimersByTime(40000);
      await Promise.resolve();
      expect(events).toHaveLength(2);
    });

    test('disconnect stops the status broadcast', async () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      stubRefresh(client);

      client.startStatusBroadcast();

      jest.advanceTimersByTime(20000);
      await Promise.resolve();
      expect(events).toHaveLength(1);

      client.disconnect();

      jest.advanceTimersByTime(40000);
      await Promise.resolve();
      expect(events).toHaveLength(1);
    });

    test('handleWelcome starts status broadcast', async () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      stubRefresh(client);

      client.handleWelcome({
        id: 7,
        secret: 'abc',
        reconnect: false,
        entities: {},
        here: {},
      });

      jest.advanceTimersByTime(20000);
      await Promise.resolve();
      const statusEvents = events.filter(e => e.type === 'game.status');
      expect(statusEvents).toHaveLength(1);
    });
  });

  describe('module exports', () => {
    const { startMonitor, stopMonitor, cleanupAllShards, getMonitorSnapshot } = require('../../backend/utils/ecast-shard-client');

    test('startMonitor is exported', () => {
      expect(typeof startMonitor).toBe('function');
    });

    test('stopMonitor is exported', () => {
      expect(typeof stopMonitor).toBe('function');
    });

    test('cleanupAllShards is exported', () => {
      expect(typeof cleanupAllShards).toBe('function');
    });

    test('getMonitorSnapshot is exported', () => {
      expect(typeof getMonitorSnapshot).toBe('function');
    });

    test('getMonitorSnapshot returns null when no shard active', () => {
      const snapshot = getMonitorSnapshot(999, 999);
      expect(snapshot).toBeNull();
    });
  });

  describe('handleError with code 2027', () => {
    test('marks game as finished and emits events on room-closed error', () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1,
        gameId: 5,
        roomCode: 'TEST',
        maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      client.playerCount = 4;
      client.playerNames = ['A', 'B', 'C', 'D'];

      client.handleError({ code: 2027, msg: 'the room has already been closed' });

      expect(client.gameFinished).toBe(true);
      expect(events.some(e => e.type === 'game.ended')).toBe(true);
      expect(events.some(e => e.type === 'room.disconnected' && e.data.reason === 'room_closed')).toBe(true);
    });
  });

  describe('handleRoomLock', () => {
    test('emits game.started when game has not yet started', () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1,
        gameId: 5,
        roomCode: 'TEST',
        maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      client.gameStarted = false;
      client.playerCount = 4;
      client.playerNames = ['A', 'B', 'C', 'D'];

      client.handleRoomLock();

      expect(client.gameStarted).toBe(true);
      const startEvents = events.filter(e => e.type === 'game.started');
      expect(startEvents).toHaveLength(1);
      expect(startEvents[0].data.playerCount).toBe(4);
    });

    test('does not emit game.started if game already started', () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1,
        gameId: 5,
        roomCode: 'TEST',
        maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      client.gameStarted = true;

      client.handleRoomLock();

      expect(events.filter(e => e.type === 'game.started')).toHaveLength(0);
    });
  });

  describe('handleRoomExit', () => {
    test('emits game.ended and room.disconnected on room exit', () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1,
        gameId: 5,
        roomCode: 'TEST',
        maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      client.playerCount = 3;
      client.playerNames = ['X', 'Y', 'Z'];

      client.handleRoomExit();

      expect(client.gameFinished).toBe(true);
      expect(events.some(e => e.type === 'game.ended')).toBe(true);
      expect(events.some(e => e.type === 'room.disconnected' && e.data.reason === 'room_closed')).toBe(true);
    });

    test('does not emit events if game already finished', () => {
      const events = [];
      const client = new EcastShardClient({
        sessionId: 1,
        gameId: 5,
        roomCode: 'TEST',
        maxPlayers: 8,
        onEvent: (type, data) => events.push({ type, data }),
      });
      client.gameFinished = true;

      client.handleRoomExit();

      expect(events).toHaveLength(0);
    });
  });
});
