import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
  recordPlaybackEvent,
  getPlaybackDashboard,
  getPlaybackSummary,
  getPlaybackHeatmap,
  retryPendingAnalytics,
  PENDING_ANALYTICS_STORAGE_KEY,
} from './playbackAnalyticsService';
import { apiClient } from '@/services/api/client';

// Mock apiClient
vi.mock('@/services/api/client', () => ({
  apiClient: {
    post: vi.fn(),
    get: vi.fn(),
  },
}));

// Mock localStorage
const localStorageMock = (() => {
  let store: Record<string, string> = {};
  return {
    getItem: (key: string) => store[key] || null,
    setItem: (key: string, value: string) => {
      store[key] = value;
    },
    removeItem: (key: string) => {
      delete store[key];
    },
    clear: () => {
      store = {};
    },
  };
})();

Object.defineProperty(window, 'localStorage', {
  value: localStorageMock,
});

describe('playbackAnalyticsService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorageMock.clear();
    vi.useFakeTimers();
  });

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

  describe('recordPlaybackEvent', () => {
    it('should record event successfully', async () => {
      const mockResponse = { status: 'recorded', id: 123 };
      vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });

      const event = {
        play_time: 180,
        pause_count: 2,
        seek_count: 1,
        started_at: '2024-01-15T10:30:00Z',
      };

      const result = await recordPlaybackEvent(42, event);

      expect(result).toEqual(mockResponse);
      expect(apiClient.post).toHaveBeenCalledWith(
        '/tracks/42/playback/analytics',
        event,
      );
    });

    it('should retry on network error', async () => {
      const mockResponse = { status: 'recorded', id: 123 };

      // Échouer 2 fois, puis réussir
      vi.mocked(apiClient.post)
        .mockRejectedValueOnce({ request: {} }) // Network error
        .mockRejectedValueOnce({ request: {} }) // Network error
        .mockResolvedValueOnce({ data: mockResponse });

      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      const promise = recordPlaybackEvent(42, event);

      // Avancer les timers pour les retries
      await vi.advanceTimersByTimeAsync(1000); // Premier retry
      await vi.advanceTimersByTimeAsync(2000); // Deuxième retry

      const result = await promise;

      expect(result).toEqual(mockResponse);
      expect(apiClient.post).toHaveBeenCalledTimes(3);
    });

    it('should save to localStorage on final failure', async () => {
      // Toutes les tentatives échouent
      vi.mocked(apiClient.post).mockRejectedValue({
        request: {},
        message: 'Network error',
      });

      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      await expect(recordPlaybackEvent(42, event)).rejects.toThrow();

      // Vérifier que l'événement a été sauvegardé dans localStorage
      const pending = JSON.parse(
        localStorageMock.getItem(PENDING_ANALYTICS_STORAGE_KEY) || '[]',
      );
      expect(pending).toHaveLength(1);
      expect(pending[0].trackId).toBe(42);
      expect(pending[0].event.play_time).toBe(180);
    });

    it('should not retry on 400 error', async () => {
      const error = {
        response: { status: 400, data: { error: 'Invalid request' } },
        message: 'Bad Request',
      };
      vi.mocked(apiClient.post).mockRejectedValue(error);

      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      await expect(recordPlaybackEvent(42, event)).rejects.toThrow(
        'Invalid request',
      );
      expect(apiClient.post).toHaveBeenCalledTimes(1); // Pas de retry
    });

    it('should not retry on 401 error', async () => {
      const error = {
        response: { status: 401, data: { error: 'Unauthorized' } },
        message: 'Unauthorized',
      };
      vi.mocked(apiClient.post).mockRejectedValue(error);

      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      await expect(recordPlaybackEvent(42, event)).rejects.toThrow(
        'Unauthorized',
      );
      expect(apiClient.post).toHaveBeenCalledTimes(1); // Pas de retry
    });

    it('should call onError callback on error', async () => {
      const error = {
        response: { status: 500, data: { error: 'Server error' } },
        message: 'Server error',
      };
      vi.mocked(apiClient.post).mockRejectedValue(error);

      const onError = vi.fn();
      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      await expect(
        recordPlaybackEvent(42, event, { onError }),
      ).rejects.toThrow();

      expect(onError).toHaveBeenCalled();
    });

    it('should call onRetry callback on retry', async () => {
      const mockResponse = { status: 'recorded', id: 123 };

      vi.mocked(apiClient.post)
        .mockRejectedValueOnce({ request: {} })
        .mockResolvedValueOnce({ data: mockResponse });

      const onRetry = vi.fn();
      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      const promise = recordPlaybackEvent(42, event, { onRetry });
      await vi.advanceTimersByTimeAsync(1000);
      await promise;

      expect(onRetry).toHaveBeenCalledWith(1, expect.any(Error));
    });

    it('should skip retry if skipRetry option is true', async () => {
      const error = {
        response: { status: 500, data: { error: 'Server error' } },
        message: 'Server error',
      };
      vi.mocked(apiClient.post).mockRejectedValue(error);

      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      await expect(
        recordPlaybackEvent(42, event, { skipRetry: true }),
      ).rejects.toThrow();

      expect(apiClient.post).toHaveBeenCalledTimes(1); // Pas de retry
    });

    it('should skip fallback if skipFallback option is true', async () => {
      const error = {
        request: {},
        message: 'Network error',
      };
      vi.mocked(apiClient.post).mockRejectedValue(error);

      const event = {
        play_time: 180,
        started_at: '2024-01-15T10:30:00Z',
      };

      await expect(
        recordPlaybackEvent(42, event, { skipFallback: true }),
      ).rejects.toThrow();

      // Vérifier que rien n'a été sauvegardé dans localStorage
      const pending = localStorageMock.getItem(PENDING_ANALYTICS_STORAGE_KEY);
      expect(pending).toBeNull();
    });
  });

  describe('getPlaybackDashboard', () => {
    it('should get dashboard successfully', async () => {
      const mockDashboard = {
        stats: { total_sessions: 100 },
        trends: { sessions_trend: 10 },
        time_series: [],
      };
      vi.mocked(apiClient.get).mockResolvedValue({
        data: { dashboard: mockDashboard },
      });

      const result = await getPlaybackDashboard(42);

      expect(result).toEqual(mockDashboard);
      expect(apiClient.get).toHaveBeenCalledWith(
        '/tracks/42/playback/dashboard',
      );
    });

    it('should retry on network error', async () => {
      const mockDashboard = {
        stats: { total_sessions: 100 },
        trends: { sessions_trend: 10 },
        time_series: [],
      };

      vi.mocked(apiClient.get)
        .mockRejectedValueOnce({ request: {} })
        .mockResolvedValueOnce({ data: { dashboard: mockDashboard } });

      const promise = getPlaybackDashboard(42);
      await vi.advanceTimersByTimeAsync(1000);
      const result = await promise;

      expect(result).toEqual(mockDashboard);
      expect(apiClient.get).toHaveBeenCalledTimes(2);
    });
  });

  describe('getPlaybackSummary', () => {
    it('should get summary successfully', async () => {
      const mockSummary = {
        total_plays: 100,
        completion_rate: 75.0,
        average_play_time: 180.0,
      };
      vi.mocked(apiClient.get).mockResolvedValue({
        data: { summary: mockSummary },
      });

      const result = await getPlaybackSummary(42);

      expect(result).toEqual(mockSummary);
      expect(apiClient.get).toHaveBeenCalledWith('/tracks/42/playback/summary');
    });
  });

  describe('getPlaybackHeatmap', () => {
    it('should get heatmap successfully', async () => {
      const mockHeatmap = {
        track_id: 42,
        track_duration: 180,
        segment_size: 5,
        total_sessions: 100,
        segments: [],
        max_intensity: 1.0,
        generated_at: '2024-01-15T10:30:00Z',
      };
      vi.mocked(apiClient.get).mockResolvedValue({
        data: { heatmap: mockHeatmap },
      });

      const result = await getPlaybackHeatmap(42, 10);

      expect(result).toEqual(mockHeatmap);
      expect(apiClient.get).toHaveBeenCalledWith(
        '/tracks/42/playback/heatmap?segment_size=10',
      );
    });
  });

  describe('retryPendingAnalytics', () => {
    it('should retry pending analytics successfully', async () => {
      const mockResponse = { status: 'recorded', id: 123 };
      vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });

      // Ajouter un événement en attente
      const pending = [
        {
          trackId: 42,
          event: {
            play_time: 180,
            started_at: '2024-01-15T10:30:00Z',
          },
          timestamp: Date.now(),
          retryCount: 0,
        },
      ];
      localStorageMock.setItem(
        PENDING_ANALYTICS_STORAGE_KEY,
        JSON.stringify(pending),
      );

      const count = await retryPendingAnalytics();

      expect(count).toBe(1);
      expect(apiClient.post).toHaveBeenCalledWith(
        '/tracks/42/playback/analytics',
        pending[0].event,
      );

      // Vérifier que l'événement a été supprimé
      const remaining = JSON.parse(
        localStorageMock.getItem(PENDING_ANALYTICS_STORAGE_KEY) || '[]',
      );
      expect(remaining).toHaveLength(0);
    });

    it('should not retry events older than 7 days', async () => {
      // Ajouter un événement très ancien
      const pending = [
        {
          trackId: 42,
          event: {
            play_time: 180,
            started_at: '2024-01-15T10:30:00Z',
          },
          timestamp: Date.now() - 8 * 24 * 60 * 60 * 1000, // 8 jours
          retryCount: 0,
        },
      ];
      localStorageMock.setItem(
        PENDING_ANALYTICS_STORAGE_KEY,
        JSON.stringify(pending),
      );

      const count = await retryPendingAnalytics();

      expect(count).toBe(0);
      expect(apiClient.post).not.toHaveBeenCalled();

      // Vérifier que l'événement a été supprimé
      const remaining = JSON.parse(
        localStorageMock.getItem(PENDING_ANALYTICS_STORAGE_KEY) || '[]',
      );
      expect(remaining).toHaveLength(0);
    });

    it('should remove events after max retries', async () => {
      vi.mocked(apiClient.post).mockRejectedValue({ request: {} });

      // Ajouter un événement avec retryCount déjà élevé (4, sera incrémenté à 5)
      const pending = [
        {
          trackId: 42,
          event: {
            play_time: 180,
            started_at: '2024-01-15T10:30:00Z',
          },
          timestamp: Date.now(),
          retryCount: 4, // Déjà à 4, sera incrémenté à 5 et supprimé
        },
      ];
      localStorageMock.setItem(
        PENDING_ANALYTICS_STORAGE_KEY,
        JSON.stringify(pending),
      );

      const count = await retryPendingAnalytics();

      expect(count).toBe(0);

      // Vérifier que l'événement a été supprimé (retryCount >= 5)
      const remaining = JSON.parse(
        localStorageMock.getItem(PENDING_ANALYTICS_STORAGE_KEY) || '[]',
      );
      // L'événement devrait être supprimé car retryCount atteint 5
      // Note: Le retryCount est incrémenté à 5, donc l'événement est ajouté à toRemove
      expect(remaining).toHaveLength(0);

      // Vérifier que l'API a été appelée une fois (pour le retry)
      expect(apiClient.post).toHaveBeenCalledTimes(1);
    });

    it('should return 0 if no pending analytics', async () => {
      const count = await retryPendingAnalytics();
      expect(count).toBe(0);
      expect(apiClient.post).not.toHaveBeenCalled();
    });
  });
});
