import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { usePlaybackRealtime } from './usePlaybackRealtime';

// Mock WebSocket
class MockWebSocket {
  static CONNECTING = 0;
  static OPEN = 1;
  static CLOSING = 2;
  static CLOSED = 3;

  readyState = MockWebSocket.CONNECTING;
  url: string;
  onopen: ((event: Event) => void) | null = null;
  onclose: ((event: CloseEvent) => void) | null = null;
  onerror: ((event: Event) => void) | null = null;
  onmessage: ((event: MessageEvent) => void) | null = null;

  constructor(url: string) {
    this.url = url;
    // Simuler l'ouverture après un court délai
    setTimeout(() => {
      this.readyState = MockWebSocket.OPEN;
      if (this.onopen) {
        this.onopen(new Event('open'));
      }
    }, 10);
  }

  send(data: string) {
    // Mock send
  }

  close(code?: number, reason?: string) {
    this.readyState = MockWebSocket.CLOSED;
    if (this.onclose) {
      this.onclose(new CloseEvent('close', { code: code || 1000 }));
    }
  }
}

// Mock global WebSocket using vi.stubGlobal
vi.stubGlobal('WebSocket', MockWebSocket);

// Mock apiClient
vi.mock('@/services/api/client', () => ({
  apiClient: {
    defaults: {
      headers: {
        common: {
          Authorization: 'Bearer test-token',
        },
      },
    },
  },
}));

describe('usePlaybackRealtime', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('should initialize with default state', () => {
    const { result } = renderHook(() =>
      usePlaybackRealtime(null, { autoConnect: false }),
    );

    expect(result.current.isConnected).toBe(false);
    expect(result.current.isConnecting).toBe(false);
    expect(result.current.error).toBe(null);
    expect(result.current.latestAnalytics).toBe(null);
    expect(result.current.latestStats).toBe(null);
  });

  it('should connect to WebSocket when trackId is provided and autoConnect is true', async () => {
    const { result } = renderHook(() =>
      usePlaybackRealtime(123, { autoConnect: true }),
    );

    // Avancer le temps pour déclencher l'ouverture du WebSocket
    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });
  });

  it('should not connect when autoConnect is false', () => {
    const { result } = renderHook(() =>
      usePlaybackRealtime(123, { autoConnect: false }),
    );

    vi.advanceTimersByTime(20);

    expect(result.current.isConnected).toBe(false);
    expect(result.current.isConnecting).toBe(false);
  });

  it('should subscribe to track after connection', async () => {
    const sendSpy = vi.spyOn(MockWebSocket.prototype, 'send');
    const { result } = renderHook(() =>
      usePlaybackRealtime(123, { autoConnect: true }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Vérifier qu'un message de subscription a été envoyé
    await waitFor(() => {
      expect(sendSpy).toHaveBeenCalled();
      const calls = sendSpy.mock.calls;
      const subscribeCall = calls.find((call) => {
        const message = JSON.parse(call[0] as string);
        return message.type === 'subscribe' && message.track_id === 123;
      });
      expect(subscribeCall).toBeDefined();
    });
  });

  it('should handle analytics_update messages', async () => {
    const mockAnalytics = {
      id: 1,
      track_id: 123,
      user_id: 1,
      play_time: 120,
      pause_count: 2,
      seek_count: 3,
      completion_rate: 75.0,
      started_at: '2023-01-01T00:00:00Z',
      created_at: '2023-01-01T00:00:00Z',
    };

    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const { result } = renderHook(() =>
      usePlaybackRealtime(123, { autoConnect: true }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Simuler la réception d'un message analytics_update
    if (wsInstance && wsInstance.onmessage) {
      const message = {
        track_id: 123,
        type: 'analytics_update',
        data: mockAnalytics,
        timestamp: new Date().toISOString(),
      };
      wsInstance.onmessage(
        new MessageEvent('message', { data: JSON.stringify(message) }),
      );
    }

    await waitFor(() => {
      expect(result.current.latestAnalytics).toEqual(mockAnalytics);
    });
  });

  it('should handle stats_update messages', async () => {
    const mockStats = {
      total_sessions: 10,
      total_play_time: 1200,
      average_play_time: 120.0,
      total_pauses: 5,
      average_pauses: 0.5,
      total_seeks: 8,
      average_seeks: 0.8,
      average_completion: 75.0,
      completion_rate: 60.0,
    };

    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const { result } = renderHook(() =>
      usePlaybackRealtime(123, { autoConnect: true }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Simuler la réception d'un message stats_update
    if (wsInstance && wsInstance.onmessage) {
      const message = {
        track_id: 123,
        type: 'stats_update',
        data: mockStats,
        timestamp: new Date().toISOString(),
      };
      wsInstance.onmessage(
        new MessageEvent('message', { data: JSON.stringify(message) }),
      );
    }

    await waitFor(() => {
      expect(result.current.latestStats).toEqual(mockStats);
    });
  });

  it('should call onAnalyticsUpdate callback when analytics update is received', async () => {
    const onAnalyticsUpdate = vi.fn();
    const mockAnalytics = {
      id: 1,
      track_id: 123,
      user_id: 1,
      play_time: 120,
      pause_count: 2,
      seek_count: 3,
      completion_rate: 75.0,
      started_at: '2023-01-01T00:00:00Z',
      created_at: '2023-01-01T00:00:00Z',
    };

    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const { result } = renderHook(() =>
      usePlaybackRealtime(123, {
        autoConnect: true,
        onAnalyticsUpdate,
      }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Simuler la réception d'un message analytics_update
    if (wsInstance && wsInstance.onmessage) {
      const message = {
        track_id: 123,
        type: 'analytics_update',
        data: mockAnalytics,
        timestamp: new Date().toISOString(),
      };
      wsInstance.onmessage(
        new MessageEvent('message', { data: JSON.stringify(message) }),
      );
    }

    await waitFor(() => {
      expect(onAnalyticsUpdate).toHaveBeenCalledWith(mockAnalytics);
    });
  });

  it('should call onStatsUpdate callback when stats update is received', async () => {
    const onStatsUpdate = vi.fn();
    const mockStats = {
      total_sessions: 10,
      total_play_time: 1200,
      average_play_time: 120.0,
      total_pauses: 5,
      average_pauses: 0.5,
      total_seeks: 8,
      average_seeks: 0.8,
      average_completion: 75.0,
      completion_rate: 60.0,
    };

    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const { result } = renderHook(() =>
      usePlaybackRealtime(123, {
        autoConnect: true,
        onStatsUpdate,
      }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Simuler la réception d'un message stats_update
    if (wsInstance && wsInstance.onmessage) {
      const message = {
        track_id: 123,
        type: 'stats_update',
        data: mockStats,
        timestamp: new Date().toISOString(),
      };
      wsInstance.onmessage(
        new MessageEvent('message', { data: JSON.stringify(message) }),
      );
    }

    await waitFor(() => {
      expect(onStatsUpdate).toHaveBeenCalledWith(mockStats);
    });
  });

  it('should disconnect when disconnect is called', async () => {
    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const closeSpy = vi.spyOn(MockWebSocket.prototype, 'close');
    const { result } = renderHook(() =>
      usePlaybackRealtime(123, { autoConnect: true }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    result.current.disconnect();
    vi.advanceTimersByTime(10);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(false);
      expect(closeSpy).toHaveBeenCalled();
    });
  });

  it('should reconnect on connection loss', async () => {
    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const { result } = renderHook(() =>
      usePlaybackRealtime(123, {
        autoConnect: true,
        maxReconnectAttempts: 3,
        reconnectDelay: 100,
      }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Simuler une fermeture inattendue
    if (wsInstance && wsInstance.onclose) {
      wsInstance.onclose(new CloseEvent('close', { code: 1006 }));
    }

    vi.advanceTimersByTime(150);

    // Vérifier qu'une reconnexion a été tentée
    expect(constructorSpy).toHaveBeenCalledTimes(2);
  });

  it('should cleanup on unmount', async () => {
    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const closeSpy = vi.spyOn(MockWebSocket.prototype, 'close');
    const { result, unmount } = renderHook(() =>
      usePlaybackRealtime(123, { autoConnect: true }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    unmount();
    vi.advanceTimersByTime(10);

    await waitFor(() => {
      expect(closeSpy).toHaveBeenCalled();
    });
  });

  it('should handle invalid JSON messages gracefully', async () => {
    const onError = vi.fn();
    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const { result } = renderHook(() =>
      usePlaybackRealtime(123, {
        autoConnect: true,
        onError,
      }),
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Simuler la réception d'un message invalide
    if (wsInstance && wsInstance.onmessage) {
      wsInstance.onmessage(
        new MessageEvent('message', { data: 'invalid json' }),
      );
    }

    await waitFor(() => {
      expect(result.current.error).not.toBe(null);
      expect(onError).toHaveBeenCalled();
    });
  });

  it('should update connection state when trackId changes', async () => {
    let wsInstance: MockWebSocket | null = null;
    const originalConstructor = MockWebSocket;
    const constructorSpy = vi.fn().mockImplementation((url: string) => {
      wsInstance = new originalConstructor(url) as MockWebSocket;
      return wsInstance;
    });
    vi.stubGlobal('WebSocket', constructorSpy);

    const { result, rerender } = renderHook(
      ({ trackId }) => usePlaybackRealtime(trackId, { autoConnect: true }),
      {
        initialProps: { trackId: 123 },
      },
    );

    vi.advanceTimersByTime(20);

    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });

    // Changer le trackId
    rerender({ trackId: 456 });
    vi.advanceTimersByTime(20);

    // La connexion devrait se fermer et se rouvrir pour le nouveau track
    await waitFor(() => {
      expect(result.current.isConnected).toBe(true);
    });
  });
});
