import { VideoPlayer, VideoPlayerOptions } from '../video-player';
import { VideoPlayerPool } from '../video-player-pool';

// Mock the VideoPlayer class
jest.mock('../video-player', () => ({
  VideoPlayer: jest.fn().mockImplementation((options: VideoPlayerOptions) => ({
    id: options.id,
    isPlaying: false,
    onRemove: jest.fn(),
    pause: jest.fn(),
    play: jest.fn(),
  })),
}));

const createMockPlayer = (id: string, overrides: Partial<VideoPlayer> = {}): VideoPlayer =>
  ({
    id,
    isPlaying: false,
    onRemove: jest.fn(),
    pause: jest.fn(),
    play: jest.fn(),
    ...overrides,
  }) as VideoPlayer;

describe('VideoPlayerPool', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('constructor', () => {
    it('should initialize with an empty pool', () => {
      const pool = new VideoPlayerPool();

      expect(pool.pool.size).toBe(0);
      expect(pool.players).toEqual([]);
    });

    it('should initialize state with null activeVideoPlayer', () => {
      const pool = new VideoPlayerPool();
      const state = pool.state.getLatestValue();

      expect(state.activeVideoPlayer).toBeNull();
    });
  });

  describe('players getter', () => {
    it('should return an empty array when pool is empty', () => {
      const pool = new VideoPlayerPool();

      expect(pool.players).toEqual([]);
    });

    it('should return all players as an array', () => {
      const pool = new VideoPlayerPool();

      pool.getOrAddPlayer({ id: 'player-1' });
      pool.getOrAddPlayer({ id: 'player-2' });
      pool.getOrAddPlayer({ id: 'player-3' });

      expect(pool.players).toHaveLength(3);
    });
  });

  describe('getOrAddPlayer', () => {
    it('should create a new player when id does not exist', () => {
      const pool = new VideoPlayerPool();
      const options: VideoPlayerOptions = { id: 'new-player' };

      const player = pool.getOrAddPlayer(options);

      expect(VideoPlayer).toHaveBeenCalledWith(options);
      expect(pool.pool.has('new-player')).toBe(true);
      expect(player.id).toBe('new-player');
    });

    it('should return existing player when id already exists', () => {
      const pool = new VideoPlayerPool();
      const options: VideoPlayerOptions = { id: 'existing-player' };

      const firstPlayer = pool.getOrAddPlayer(options);
      const secondPlayer = pool.getOrAddPlayer(options);

      expect(VideoPlayer).toHaveBeenCalledTimes(1);
      expect(firstPlayer).toBe(secondPlayer);
    });

    it('should set pool reference on new player', () => {
      const pool = new VideoPlayerPool();
      const options: VideoPlayerOptions = { id: 'player-with-pool' };

      const player = pool.getOrAddPlayer(options);

      expect(player.pool).toBe(pool);
    });

    it('should pass autoPlay option to VideoPlayer', () => {
      const pool = new VideoPlayerPool();
      const options: VideoPlayerOptions = { autoPlay: true, id: 'autoplay-player' };

      pool.getOrAddPlayer(options);

      expect(VideoPlayer).toHaveBeenCalledWith(options);
    });
  });

  describe('setActivePlayer', () => {
    it('should set the active video player', () => {
      const pool = new VideoPlayerPool();
      const mockPlayer = createMockPlayer('active-player');

      pool.setActivePlayer(mockPlayer);

      expect(pool.state.getLatestValue().activeVideoPlayer).toBe(mockPlayer);
    });

    it('should allow setting active player to null', () => {
      const pool = new VideoPlayerPool();
      const mockPlayer = createMockPlayer('active-player');

      pool.setActivePlayer(mockPlayer);
      pool.setActivePlayer(null);

      expect(pool.state.getLatestValue().activeVideoPlayer).toBeNull();
    });
  });

  describe('getActivePlayer', () => {
    it('should return null when no active player is set', () => {
      const pool = new VideoPlayerPool();

      expect(pool.getActivePlayer()).toBeNull();
    });

    it('should return the current active player', () => {
      const pool = new VideoPlayerPool();
      const mockPlayer = createMockPlayer('active-player');

      pool.setActivePlayer(mockPlayer);

      expect(pool.getActivePlayer()).toBe(mockPlayer);
    });
  });

  describe('removePlayer', () => {
    it('should do nothing when player does not exist', () => {
      const pool = new VideoPlayerPool();

      // Should not throw
      pool.removePlayer('non-existent-player');

      expect(pool.pool.size).toBe(0);
    });

    it('should remove player from pool', () => {
      const pool = new VideoPlayerPool();
      pool.getOrAddPlayer({ id: 'player-to-remove' });

      expect(pool.pool.has('player-to-remove')).toBe(true);

      pool.removePlayer('player-to-remove');

      expect(pool.pool.has('player-to-remove')).toBe(false);
    });

    it('should call onRemove on the player being removed', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'player-to-remove' });

      pool.removePlayer('player-to-remove');

      expect(player.onRemove).toHaveBeenCalled();
    });

    it('should set active player to null if removed player was active', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'active-player' });

      pool.setActivePlayer(player);
      expect(pool.getActivePlayer()).toBe(player);

      pool.removePlayer('active-player');

      expect(pool.getActivePlayer()).toBeNull();
    });

    it('should not affect active player if removed player was not active', () => {
      const pool = new VideoPlayerPool();
      const activePlayer = pool.getOrAddPlayer({ id: 'active-player' });
      pool.getOrAddPlayer({ id: 'other-player' });

      pool.setActivePlayer(activePlayer);
      pool.removePlayer('other-player');

      expect(pool.getActivePlayer()).toBe(activePlayer);
    });
  });

  describe('deregister', () => {
    it('should do nothing when player does not exist', () => {
      const pool = new VideoPlayerPool();

      // Should not throw
      pool.deregister('non-existent-player');

      expect(pool.pool.size).toBe(0);
    });

    it('should remove player from pool without calling onRemove', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'player-to-deregister' });

      pool.deregister('player-to-deregister');

      expect(pool.pool.has('player-to-deregister')).toBe(false);
      expect(player.onRemove).not.toHaveBeenCalled();
    });
  });

  describe('clear', () => {
    it('should do nothing when pool is empty', () => {
      const pool = new VideoPlayerPool();

      // Should not throw
      pool.clear();

      expect(pool.pool.size).toBe(0);
      expect(pool.getActivePlayer()).toBeNull();
    });

    it('should remove all players from pool', () => {
      const pool = new VideoPlayerPool();
      pool.getOrAddPlayer({ id: 'player-1' });
      pool.getOrAddPlayer({ id: 'player-2' });
      pool.getOrAddPlayer({ id: 'player-3' });

      expect(pool.pool.size).toBe(3);

      pool.clear();

      expect(pool.pool.size).toBe(0);
    });

    it('should call onRemove on all players', () => {
      const pool = new VideoPlayerPool();
      const player1 = pool.getOrAddPlayer({ id: 'player-1' });
      const player2 = pool.getOrAddPlayer({ id: 'player-2' });

      pool.clear();

      expect(player1.onRemove).toHaveBeenCalled();
      expect(player2.onRemove).toHaveBeenCalled();
    });

    it('should set active player to null', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'active-player' });

      pool.setActivePlayer(player);
      expect(pool.getActivePlayer()).toBe(player);

      pool.clear();

      expect(pool.getActivePlayer()).toBeNull();
    });
  });

  describe('requestPlay', () => {
    it('should set active player when player exists in pool', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'player-1' });

      pool.requestPlay('player-1');

      expect(pool.getActivePlayer()).toBe(player);
    });

    it('should not change active player when player does not exist', () => {
      const pool = new VideoPlayerPool();
      const existingPlayer = pool.getOrAddPlayer({ id: 'existing-player' });
      pool.setActivePlayer(existingPlayer);

      pool.requestPlay('non-existent-player');

      expect(pool.getActivePlayer()).toBe(existingPlayer);
    });

    it('should pause current active player if it is playing and different from requested', () => {
      const pool = new VideoPlayerPool();
      const currentPlayer = createMockPlayer('current-player', { isPlaying: true });
      pool.pool.set('current-player', currentPlayer);
      pool.pool.set('new-player', createMockPlayer('new-player'));

      pool.setActivePlayer(currentPlayer);
      pool.requestPlay('new-player');

      expect(currentPlayer.pause).toHaveBeenCalled();
    });

    it('should not pause current player if it is not playing', () => {
      const pool = new VideoPlayerPool();
      const currentPlayer = createMockPlayer('current-player', { isPlaying: false });
      pool.pool.set('current-player', currentPlayer);
      pool.pool.set('new-player', createMockPlayer('new-player'));

      pool.setActivePlayer(currentPlayer);
      pool.requestPlay('new-player');

      expect(currentPlayer.pause).not.toHaveBeenCalled();
    });

    it('should not pause current player if it is the same as requested', () => {
      const pool = new VideoPlayerPool();
      const currentPlayer = createMockPlayer('same-player', { isPlaying: true });
      pool.pool.set('same-player', currentPlayer);

      pool.setActivePlayer(currentPlayer);
      pool.requestPlay('same-player');

      expect(currentPlayer.pause).not.toHaveBeenCalled();
    });

    it('should handle case when there is no current active player', () => {
      const pool = new VideoPlayerPool();
      const newPlayer = pool.getOrAddPlayer({ id: 'new-player' });

      expect(pool.getActivePlayer()).toBeNull();

      pool.requestPlay('new-player');

      expect(pool.getActivePlayer()).toBe(newPlayer);
    });
  });

  describe('notifyPaused', () => {
    it('should set active player to null', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'active-player' });

      pool.setActivePlayer(player);
      expect(pool.getActivePlayer()).toBe(player);

      pool.notifyPaused();

      expect(pool.getActivePlayer()).toBeNull();
    });

    it('should work even when there is no active player', () => {
      const pool = new VideoPlayerPool();

      expect(pool.getActivePlayer()).toBeNull();

      // Should not throw
      pool.notifyPaused();

      expect(pool.getActivePlayer()).toBeNull();
    });
  });

  describe('state reactivity', () => {
    it('should notify subscribers when active player changes', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'player-1' });
      const callback = jest.fn();

      pool.state.subscribeWithSelector(
        (state) => ({
          activeVideoPlayer: state.activeVideoPlayer,
        }),
        ({ activeVideoPlayer }) => callback(activeVideoPlayer),
      );

      pool.setActivePlayer(player);

      expect(callback).toHaveBeenCalledWith(player);
    });

    it('should notify subscribers when active player is cleared', () => {
      const pool = new VideoPlayerPool();
      const player = pool.getOrAddPlayer({ id: 'player-1' });
      pool.setActivePlayer(player);

      const callback = jest.fn();
      pool.state.subscribeWithSelector(
        (state) => ({
          activeVideoPlayer: state.activeVideoPlayer,
        }),
        ({ activeVideoPlayer }) => callback(activeVideoPlayer),
      );

      pool.notifyPaused();

      expect(callback).toHaveBeenCalledWith(null);
    });
  });

  describe('edge cases', () => {
    it('should handle multiple sequential play requests', () => {
      const pool = new VideoPlayerPool();
      const player1 = createMockPlayer('player-1', { isPlaying: true });
      const player2 = createMockPlayer('player-2', { isPlaying: false });
      const player3 = createMockPlayer('player-3', { isPlaying: false });

      pool.pool.set('player-1', player1);
      pool.pool.set('player-2', player2);
      pool.pool.set('player-3', player3);

      pool.setActivePlayer(player1);
      pool.requestPlay('player-2');
      // After switching, player2 is now active. Update mock to reflect playing state
      (player2 as { isPlaying: boolean }).isPlaying = true;
      pool.requestPlay('player-3');

      expect(player1.pause).toHaveBeenCalled();
      expect(player2.pause).toHaveBeenCalled();
      expect(pool.getActivePlayer()).toBe(player3);
    });

    it('should handle removing all players one by one', () => {
      const pool = new VideoPlayerPool();
      const player1 = pool.getOrAddPlayer({ id: 'player-1' });
      pool.getOrAddPlayer({ id: 'player-2' });

      pool.setActivePlayer(player1);

      pool.removePlayer('player-1');
      expect(pool.getActivePlayer()).toBeNull();
      expect(pool.pool.size).toBe(1);

      pool.removePlayer('player-2');
      expect(pool.pool.size).toBe(0);
    });

    it('should handle adding player with same id after removal', () => {
      const pool = new VideoPlayerPool();

      const originalPlayer = pool.getOrAddPlayer({ id: 'reusable-id' });
      pool.removePlayer('reusable-id');

      const newPlayer = pool.getOrAddPlayer({ id: 'reusable-id' });

      expect(newPlayer).not.toBe(originalPlayer);
      expect(pool.pool.has('reusable-id')).toBe(true);
    });
  });
});
