import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import {
  useCreatePlaylist,
  usePlaylist,
  useUpdatePlaylist,
  useDeletePlaylist,
  usePlaylists,
  useAddCollaborator,
  useRemoveCollaborator,
  useUpdateCollaboratorPermission,
  useCollaborators,
} from './usePlaylist';
import * as playlistService from '../services/playlistService';
import type { Playlist, PlaylistListResponse } from '../types';

// Mock playlistService
vi.mock('../services/playlistService', () => ({
  createPlaylist: vi.fn(),
  getPlaylist: vi.fn(),
  updatePlaylist: vi.fn(),
  deletePlaylist: vi.fn(),
  listPlaylists: vi.fn(),
  addCollaborator: vi.fn(),
  removeCollaborator: vi.fn(),
  updateCollaboratorPermission: vi.fn(),
  getCollaborators: vi.fn(),
}));

// Helper pour créer un QueryClient pour chaque test
function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
      mutations: {
        retry: false,
      },
    },
  });

  return ({ children }: { children: ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

describe('usePlaylist hooks', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('useCreatePlaylist', () => {
    it('should create a playlist successfully', async () => {
      const mockPlaylist: Playlist = {
        id: 1,
        user_id: 1,
        title: 'New Playlist',
        is_public: true,
        track_count: 0,
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-01T00:00:00Z',
      };

      vi.mocked(playlistService.createPlaylist).mockResolvedValue(mockPlaylist);

      const { result } = renderHook(() => useCreatePlaylist(), {
        wrapper: createWrapper(),
      });

      result.current.mutate({
        title: 'New Playlist',
        is_public: true,
      });

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.createPlaylist).toHaveBeenCalledWith({
        title: 'New Playlist',
        is_public: true,
      });
      expect(result.current.data).toEqual(mockPlaylist);
    });

    it('should handle error on create', async () => {
      const error = new Error('Failed to create playlist');
      vi.mocked(playlistService.createPlaylist).mockRejectedValue(error);

      const { result } = renderHook(() => useCreatePlaylist(), {
        wrapper: createWrapper(),
      });

      result.current.mutate({
        title: 'New Playlist',
      });

      await waitFor(() => expect(result.current.isError).toBe(true));

      expect(result.current.error).toBe(error);
    });
  });

  describe('usePlaylist', () => {
    it('should fetch a playlist successfully', async () => {
      const mockPlaylist: Playlist = {
        id: 1,
        user_id: 1,
        title: 'My Playlist',
        is_public: true,
        track_count: 5,
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-01T00:00:00Z',
      };

      vi.mocked(playlistService.getPlaylist).mockResolvedValue(mockPlaylist);

      const { result } = renderHook(() => usePlaylist(1), {
        wrapper: createWrapper(),
      });

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.getPlaylist).toHaveBeenCalledWith(1);
      expect(result.current.data).toEqual(mockPlaylist);
    });

    it('should not fetch when id is 0', () => {
      const { result } = renderHook(() => usePlaylist(0), {
        wrapper: createWrapper(),
      });

      expect(result.current.isFetching).toBe(false);
      expect(playlistService.getPlaylist).not.toHaveBeenCalled();
    });

    it('should handle error on fetch', async () => {
      const error = new Error('Playlist not found');
      vi.mocked(playlistService.getPlaylist).mockRejectedValue(error);

      const { result } = renderHook(() => usePlaylist(999), {
        wrapper: createWrapper(),
      });

      await waitFor(() => expect(result.current.isError).toBe(true));

      expect(result.current.error).toBe(error);
    });
  });

  describe('useUpdatePlaylist', () => {
    it('should update a playlist successfully', async () => {
      const mockPlaylist: Playlist = {
        id: 1,
        user_id: 1,
        title: 'Updated Playlist',
        is_public: false,
        track_count: 5,
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-02T00:00:00Z',
      };

      vi.mocked(playlistService.updatePlaylist).mockResolvedValue(mockPlaylist);

      const { result } = renderHook(() => useUpdatePlaylist(), {
        wrapper: createWrapper(),
      });

      result.current.mutate({
        id: 1,
        data: { title: 'Updated Playlist', is_public: false },
      });

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.updatePlaylist).toHaveBeenCalledWith(1, {
        title: 'Updated Playlist',
        is_public: false,
      });
      expect(result.current.data).toEqual(mockPlaylist);
    });

    it('should handle error on update', async () => {
      const error = new Error('Failed to update playlist');
      vi.mocked(playlistService.updatePlaylist).mockRejectedValue(error);

      const { result } = renderHook(() => useUpdatePlaylist(), {
        wrapper: createWrapper(),
      });

      result.current.mutate({
        id: 1,
        data: { title: 'Updated Playlist' },
      });

      await waitFor(() => expect(result.current.isError).toBe(true));

      expect(result.current.error).toBe(error);
    });
  });

  describe('useDeletePlaylist', () => {
    it('should delete a playlist successfully', async () => {
      vi.mocked(playlistService.deletePlaylist).mockResolvedValue(undefined);

      const { result } = renderHook(() => useDeletePlaylist(), {
        wrapper: createWrapper(),
      });

      result.current.mutate(1);

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.deletePlaylist).toHaveBeenCalledWith(1);
    });

    it('should handle error on delete', async () => {
      const error = new Error('Failed to delete playlist');
      vi.mocked(playlistService.deletePlaylist).mockRejectedValue(error);

      const { result } = renderHook(() => useDeletePlaylist(), {
        wrapper: createWrapper(),
      });

      result.current.mutate(1);

      await waitFor(() => expect(result.current.isError).toBe(true));

      expect(result.current.error).toBe(error);
    });
  });

  describe('usePlaylists', () => {
    it('should fetch playlists successfully with default params', async () => {
      const mockResponse: PlaylistListResponse = {
        playlists: [
          {
            id: 1,
            user_id: 1,
            title: 'Playlist 1',
            is_public: true,
            track_count: 5,
            created_at: '2024-01-01T00:00:00Z',
            updated_at: '2024-01-01T00:00:00Z',
          },
        ],
        total: 1,
        page: 1,
        limit: 20,
      };

      vi.mocked(playlistService.listPlaylists).mockResolvedValue(mockResponse);

      const { result } = renderHook(() => usePlaylists(), {
        wrapper: createWrapper(),
      });

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.listPlaylists).toHaveBeenCalledWith(20, 0);
      expect(result.current.data).toEqual(mockResponse);
    });

    it('should fetch playlists with custom limit and offset', async () => {
      const mockResponse: PlaylistListResponse = {
        playlists: [],
        total: 0,
        page: 1,
        limit: 10,
      };

      vi.mocked(playlistService.listPlaylists).mockResolvedValue(mockResponse);

      const { result } = renderHook(() => usePlaylists(10, 20), {
        wrapper: createWrapper(),
      });

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.listPlaylists).toHaveBeenCalledWith(10, 20);
      expect(result.current.data).toEqual(mockResponse);
    });

    it('should handle error on fetch', async () => {
      const error = new Error('Failed to fetch playlists');
      vi.mocked(playlistService.listPlaylists).mockRejectedValue(error);

      const { result } = renderHook(() => usePlaylists(), {
        wrapper: createWrapper(),
      });

      await waitFor(() => expect(result.current.isError).toBe(true));

      expect(result.current.error).toBe(error);
    });
  });

  describe('useAddCollaborator', () => {
    it('should add a collaborator successfully', async () => {
      const mockCollaborator = {
        id: 1,
        playlist_id: 1,
        user_id: 2,
        permission: 'read' as const,
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-01T00:00:00Z',
        user: {
          id: 2,
          username: 'collaborator',
          email: 'collaborator@example.com',
        },
      };

      vi.mocked(playlistService.addCollaborator).mockResolvedValue(
        mockCollaborator,
      );

      const { result } = renderHook(() => useAddCollaborator(), {
        wrapper: createWrapper(),
      });

      act(() => {
        result.current.mutate({
          playlistId: 1,
          data: { user_id: 2, permission: 'read' },
        });
      });

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

      expect(playlistService.addCollaborator).toHaveBeenCalledWith(1, {
        user_id: 2,
        permission: 'read',
      });
      expect(result.current.data).toEqual(mockCollaborator);
    });

    it('should invalidate queries on success', async () => {
      const mockCollaborator = {
        id: 1,
        playlist_id: 1,
        user_id: 2,
        permission: 'read' as const,
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-01T00:00:00Z',
      };

      vi.mocked(playlistService.addCollaborator).mockResolvedValue(
        mockCollaborator,
      );

      const queryClient = new QueryClient();
      const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');

      const { result } = renderHook(() => useAddCollaborator(), {
        wrapper: ({ children }) => (
          <QueryClientProvider client={queryClient}>
            {children}
          </QueryClientProvider>
        ),
      });

      await act(async () => {
        await result.current.mutateAsync({
          playlistId: 1,
          data: { user_id: 2, permission: 'read' },
        });
      });

      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlist', 1],
      });
      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlist', 1, 'collaborators'],
      });
      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlists'],
      });
    });
  });

  describe('useRemoveCollaborator', () => {
    it('should remove a collaborator successfully', async () => {
      vi.mocked(playlistService.removeCollaborator).mockResolvedValue();

      const { result } = renderHook(() => useRemoveCollaborator(), {
        wrapper: createWrapper(),
      });

      act(() => {
        result.current.mutate({
          playlistId: 1,
          userId: 2,
        });
      });

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.removeCollaborator).toHaveBeenCalledWith(1, 2);
    });

    it('should invalidate queries on success', async () => {
      vi.mocked(playlistService.removeCollaborator).mockResolvedValue();

      const queryClient = new QueryClient();
      const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');

      const { result } = renderHook(() => useRemoveCollaborator(), {
        wrapper: ({ children }) => (
          <QueryClientProvider client={queryClient}>
            {children}
          </QueryClientProvider>
        ),
      });

      await act(async () => {
        await result.current.mutateAsync({
          playlistId: 1,
          userId: 2,
        });
      });

      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlist', 1],
      });
      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlist', 1, 'collaborators'],
      });
      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlists'],
      });
    });
  });

  describe('useUpdateCollaboratorPermission', () => {
    it('should update collaborator permission successfully', async () => {
      vi.mocked(
        playlistService.updateCollaboratorPermission,
      ).mockResolvedValue();

      const { result } = renderHook(() => useUpdateCollaboratorPermission(), {
        wrapper: createWrapper(),
      });

      act(() => {
        result.current.mutate({
          playlistId: 1,
          userId: 2,
          data: { permission: 'write' },
        });
      });

      await waitFor(() => expect(result.current.isSuccess).toBe(true));

      expect(playlistService.updateCollaboratorPermission).toHaveBeenCalledWith(
        1,
        2,
        {
          permission: 'write',
        },
      );
    });

    it('should invalidate queries on success', async () => {
      vi.mocked(
        playlistService.updateCollaboratorPermission,
      ).mockResolvedValue();

      const queryClient = new QueryClient();
      const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');

      const { result } = renderHook(() => useUpdateCollaboratorPermission(), {
        wrapper: ({ children }) => (
          <QueryClientProvider client={queryClient}>
            {children}
          </QueryClientProvider>
        ),
      });

      await act(async () => {
        await result.current.mutateAsync({
          playlistId: 1,
          userId: 2,
          data: { permission: 'write' },
        });
      });

      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlist', 1],
      });
      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlist', 1, 'collaborators'],
      });
      expect(invalidateQueriesSpy).toHaveBeenCalledWith({
        queryKey: ['playlists'],
      });
    });
  });

  describe('useCollaborators', () => {
    it('should fetch collaborators successfully', async () => {
      const mockCollaborators = [
        {
          id: 1,
          playlist_id: 1,
          user_id: 2,
          permission: 'read' as const,
          created_at: '2024-01-01T00:00:00Z',
          updated_at: '2024-01-01T00:00:00Z',
          user: {
            id: 2,
            username: 'collaborator1',
            email: 'collaborator1@example.com',
          },
        },
        {
          id: 2,
          playlist_id: 1,
          user_id: 3,
          permission: 'write' as const,
          created_at: '2024-01-01T00:00:00Z',
          updated_at: '2024-01-01T00:00:00Z',
          user: {
            id: 3,
            username: 'collaborator2',
            email: 'collaborator2@example.com',
          },
        },
      ];

      vi.mocked(playlistService.getCollaborators).mockResolvedValue(
        mockCollaborators,
      );

      const { result } = renderHook(() => useCollaborators(1), {
        wrapper: createWrapper(),
      });

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

      expect(playlistService.getCollaborators).toHaveBeenCalledWith(1);
      expect(result.current.data).toEqual(mockCollaborators);
    });

    it('should not fetch when playlistId is 0', () => {
      const { result } = renderHook(() => useCollaborators(0), {
        wrapper: createWrapper(),
      });

      expect(result.current.isLoading).toBe(false);
      expect(playlistService.getCollaborators).not.toHaveBeenCalled();
    });

    it('should handle error state', async () => {
      const mockError = new Error('Failed to fetch collaborators');
      vi.mocked(playlistService.getCollaborators).mockRejectedValue(mockError);

      const { result } = renderHook(() => useCollaborators(1), {
        wrapper: createWrapper(),
      });

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

      expect(result.current.error).toEqual(mockError);
    });
  });
});
