import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';

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

import {
  useResourceTypes,
  useCreateResourceType,
  useUpdateResourceType,
  useDeleteResourceType,
} from '../useResourceTypes';
import apiClient from '../../api/client';
import { ResourceTypeDefinition } from '../../types';

// Create wrapper
const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });

  return function Wrapper({ children }: { children: React.ReactNode }) {
    return React.createElement(QueryClientProvider, { client: queryClient }, children);
  };
};

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

  describe('useResourceTypes', () => {
    it('fetches resource types successfully', async () => {
      const mockResourceTypes: ResourceTypeDefinition[] = [
        {
          id: '1',
          name: 'Stylist',
          category: 'STAFF',
          isDefault: false,
          description: 'Hair stylist',
          iconName: 'scissors',
        },
        {
          id: '2',
          name: 'Treatment Room',
          category: 'OTHER',
          isDefault: false,
          description: 'Private treatment room',
        },
      ];

      vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourceTypes });

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

      // Initially shows placeholder data
      expect(result.current.data).toHaveLength(3);

      await waitFor(() => {
        expect(result.current.isSuccess).toBe(true);
        // Wait for the actual data to be set
        expect(result.current.data).toEqual(mockResourceTypes);
      });

      expect(apiClient.get).toHaveBeenCalledWith('/resource-types/');
      // After success, placeholderData is replaced with actual data
      expect(result.current.data?.[0]).toEqual({
        id: '1',
        name: 'Stylist',
        category: 'STAFF',
        isDefault: false,
        description: 'Hair stylist',
        iconName: 'scissors',
      });
      expect(result.current.data?.[1]).toEqual({
        id: '2',
        name: 'Treatment Room',
        category: 'OTHER',
        isDefault: false,
        description: 'Private treatment room',
      });
    });

    it('returns placeholder data while loading', async () => {
      vi.mocked(apiClient.get).mockImplementation(
        () => new Promise(() => {}) // Never resolves
      );

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

      // Should show placeholder data immediately
      expect(result.current.data).toHaveLength(3);
      expect(result.current.data?.[0]).toMatchObject({
        id: 'default-staff',
        name: 'Staff',
        category: 'STAFF',
        isDefault: true,
      });
      expect(result.current.data?.[1]).toMatchObject({
        id: 'default-room',
        name: 'Room',
        category: 'OTHER',
        isDefault: true,
      });
      expect(result.current.data?.[2]).toMatchObject({
        id: 'default-equipment',
        name: 'Equipment',
        category: 'OTHER',
        isDefault: true,
      });
    });

    it('replaces placeholder data when API returns data', async () => {
      const mockResourceTypes: ResourceTypeDefinition[] = [
        {
          id: '1',
          name: 'Custom Type',
          category: 'STAFF',
          isDefault: false,
        },
      ];

      vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourceTypes });

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

      // Initially shows placeholder data
      expect(result.current.data).toHaveLength(3);
      expect(result.current.isPlaceholderData).toBe(true);

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

      // After success, should use actual data
      expect(result.current.data).toEqual(mockResourceTypes);
      expect(result.current.data).toHaveLength(1);
    });

    it('handles API errors gracefully', async () => {
      const mockError = new Error('Network error');
      vi.mocked(apiClient.get).mockRejectedValue(mockError);

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

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

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

    it('caches data with correct query key', async () => {
      const mockResourceTypes: ResourceTypeDefinition[] = [
        {
          id: '1',
          name: 'Staff',
          category: 'STAFF',
          isDefault: true,
        },
      ];

      vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourceTypes });

      const queryClient = new QueryClient({
        defaultOptions: {
          queries: { retry: false, staleTime: Infinity },
          mutations: { retry: false },
        },
      });

      const wrapper = ({ children }: { children: React.ReactNode }) =>
        React.createElement(QueryClientProvider, { client: queryClient }, children);

      const { result: result1 } = renderHook(() => useResourceTypes(), { wrapper });

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

      const callCountAfterFirst = vi.mocked(apiClient.get).mock.calls.length;

      // Second call should use cached data without making another API call
      const { result: result2 } = renderHook(() => useResourceTypes(), { wrapper });

      // Wait for the hook to settle - it should use cached data immediately
      await waitFor(() => {
        expect(result2.current.data).toEqual(mockResourceTypes);
      });

      // Should not have made any additional API calls due to caching
      expect(vi.mocked(apiClient.get).mock.calls.length).toBe(callCountAfterFirst);
    });
  });

  describe('useCreateResourceType', () => {
    it('creates a new resource type successfully', async () => {
      const newResourceType = {
        name: 'Massage Therapist',
        category: 'STAFF' as const,
        description: 'Licensed massage therapist',
        iconName: 'hands',
      };

      const createdResourceType: ResourceTypeDefinition = {
        id: '3',
        ...newResourceType,
        isDefault: false,
      };

      vi.mocked(apiClient.post).mockResolvedValue({ data: createdResourceType });

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

      let mutationResult: ResourceTypeDefinition | undefined;

      await act(async () => {
        mutationResult = await result.current.mutateAsync(newResourceType);
      });

      expect(apiClient.post).toHaveBeenCalledWith('/resource-types/', newResourceType);
      expect(mutationResult).toEqual(createdResourceType);
    });

    it('creates resource type with minimal fields', async () => {
      const newResourceType = {
        name: 'Equipment',
        category: 'OTHER' as const,
      };

      const createdResourceType: ResourceTypeDefinition = {
        id: '4',
        ...newResourceType,
        isDefault: false,
      };

      vi.mocked(apiClient.post).mockResolvedValue({ data: createdResourceType });

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

      await act(async () => {
        await result.current.mutateAsync(newResourceType);
      });

      expect(apiClient.post).toHaveBeenCalledWith('/resource-types/', newResourceType);
    });

    it('invalidates resource types cache on success', async () => {
      const mockResourceTypes: ResourceTypeDefinition[] = [
        { id: '1', name: 'Staff', category: 'STAFF', isDefault: true },
      ];
      const newResourceType = { name: 'New Type', category: 'OTHER' as const };
      const createdResourceType: ResourceTypeDefinition = {
        id: '2',
        ...newResourceType,
        isDefault: false,
      };

      vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourceTypes });
      vi.mocked(apiClient.post).mockResolvedValue({ data: createdResourceType });

      const wrapper = createWrapper();

      // First fetch resource types
      const { result: queryResult } = renderHook(() => useResourceTypes(), { wrapper });

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

      expect(apiClient.get).toHaveBeenCalledTimes(1);

      // Create a new resource type
      const { result: mutationResult } = renderHook(() => useCreateResourceType(), { wrapper });

      await act(async () => {
        await mutationResult.current.mutateAsync(newResourceType);
      });

      // Cache should be invalidated, triggering a refetch
      await waitFor(() => {
        expect(apiClient.get).toHaveBeenCalledTimes(2);
      });
    });

    it('handles creation errors', async () => {
      const mockError = new Error('Validation failed');
      vi.mocked(apiClient.post).mockRejectedValue(mockError);

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

      let caughtError: Error | null = null;

      await act(async () => {
        try {
          await result.current.mutateAsync({
            name: 'Invalid',
            category: 'STAFF',
          });
        } catch (error) {
          caughtError = error as Error;
        }
      });

      expect(caughtError).toBe(mockError);
      // Wait for the mutation state to update
      await waitFor(() => {
        expect(result.current.isError).toBe(true);
      });
    });
  });

  describe('useUpdateResourceType', () => {
    it('updates a resource type successfully', async () => {
      const updates = {
        name: 'Senior Stylist',
        description: 'Senior level hair stylist',
      };

      const updatedResourceType: ResourceTypeDefinition = {
        id: '1',
        ...updates,
        category: 'STAFF',
        isDefault: false,
      };

      vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedResourceType });

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

      let mutationResult: ResourceTypeDefinition | undefined;

      await act(async () => {
        mutationResult = await result.current.mutateAsync({
          id: '1',
          updates,
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/resource-types/1/', updates);
      expect(mutationResult).toEqual(updatedResourceType);
    });

    it('updates only specified fields', async () => {
      const updates = { iconName: 'star' };

      vi.mocked(apiClient.patch).mockResolvedValue({
        data: {
          id: '1',
          name: 'Staff',
          category: 'STAFF',
          isDefault: true,
          iconName: 'star',
        },
      });

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

      await act(async () => {
        await result.current.mutateAsync({
          id: '1',
          updates,
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/resource-types/1/', updates);
    });

    it('invalidates resource types cache on success', async () => {
      const mockResourceTypes: ResourceTypeDefinition[] = [
        { id: '1', name: 'Staff', category: 'STAFF', isDefault: true },
      ];

      vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourceTypes });
      vi.mocked(apiClient.patch).mockResolvedValue({
        data: { id: '1', name: 'Updated Staff', category: 'STAFF', isDefault: true },
      });

      const wrapper = createWrapper();

      // First fetch resource types
      const { result: queryResult } = renderHook(() => useResourceTypes(), { wrapper });

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

      expect(apiClient.get).toHaveBeenCalledTimes(1);

      // Update a resource type
      const { result: mutationResult } = renderHook(() => useUpdateResourceType(), { wrapper });

      await act(async () => {
        await mutationResult.current.mutateAsync({
          id: '1',
          updates: { name: 'Updated Staff' },
        });
      });

      // Cache should be invalidated, triggering a refetch
      await waitFor(() => {
        expect(apiClient.get).toHaveBeenCalledTimes(2);
      });
    });

    it('handles update errors', async () => {
      const mockError = new Error('Not found');
      vi.mocked(apiClient.patch).mockRejectedValue(mockError);

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

      let caughtError: Error | null = null;

      await act(async () => {
        try {
          await result.current.mutateAsync({
            id: '999',
            updates: { name: 'Does not exist' },
          });
        } catch (error) {
          caughtError = error as Error;
        }
      });

      expect(caughtError).toBe(mockError);
      // Wait for the mutation state to update
      await waitFor(() => {
        expect(result.current.isError).toBe(true);
      });
    });

    it('can update category', async () => {
      const updates = { category: 'OTHER' as const };

      vi.mocked(apiClient.patch).mockResolvedValue({
        data: {
          id: '1',
          name: 'Staff',
          category: 'OTHER',
          isDefault: false,
        },
      });

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

      await act(async () => {
        await result.current.mutateAsync({
          id: '1',
          updates,
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/resource-types/1/', updates);
    });
  });

  describe('useDeleteResourceType', () => {
    it('deletes a resource type successfully', async () => {
      vi.mocked(apiClient.delete).mockResolvedValue({});

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

      await act(async () => {
        await result.current.mutateAsync('5');
      });

      expect(apiClient.delete).toHaveBeenCalledWith('/resource-types/5/');
    });

    it('invalidates resource types cache on success', async () => {
      const mockResourceTypes: ResourceTypeDefinition[] = [
        { id: '1', name: 'Staff', category: 'STAFF', isDefault: true },
        { id: '2', name: 'Room', category: 'OTHER', isDefault: false },
      ];

      vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourceTypes });
      vi.mocked(apiClient.delete).mockResolvedValue({});

      const wrapper = createWrapper();

      // First fetch resource types
      const { result: queryResult } = renderHook(() => useResourceTypes(), { wrapper });

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

      expect(apiClient.get).toHaveBeenCalledTimes(1);

      // Delete a resource type
      const { result: mutationResult } = renderHook(() => useDeleteResourceType(), { wrapper });

      await act(async () => {
        await mutationResult.current.mutateAsync('2');
      });

      // Cache should be invalidated, triggering a refetch
      await waitFor(() => {
        expect(apiClient.get).toHaveBeenCalledTimes(2);
      });
    });

    it('handles deletion errors', async () => {
      const mockError = new Error('Cannot delete default resource type');
      vi.mocked(apiClient.delete).mockRejectedValue(mockError);

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

      let caughtError: Error | null = null;

      await act(async () => {
        try {
          await result.current.mutateAsync('default-staff');
        } catch (error) {
          caughtError = error as Error;
        }
      });

      expect(caughtError).toBe(mockError);
      // Wait for the mutation state to update
      await waitFor(() => {
        expect(result.current.isError).toBe(true);
      });
    });

    it('deletes by string id', async () => {
      vi.mocked(apiClient.delete).mockResolvedValue({});

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

      await act(async () => {
        await result.current.mutateAsync('abc-123-def-456');
      });

      expect(apiClient.delete).toHaveBeenCalledWith('/resource-types/abc-123-def-456/');
    });
  });

  describe('Integration tests', () => {
    it('supports full CRUD workflow', async () => {
      const wrapper = createWrapper();

      // 1. Fetch initial resource types
      vi.mocked(apiClient.get).mockResolvedValue({
        data: [
          { id: '1', name: 'Staff', category: 'STAFF', isDefault: true },
        ],
      });

      const { result: queryResult } = renderHook(() => useResourceTypes(), { wrapper });

      await waitFor(() => {
        expect(queryResult.current.isSuccess).toBe(true);
        expect(queryResult.current.data).toHaveLength(1);
      });

      // 2. Create new resource type
      const newType = { name: 'Therapist', category: 'STAFF' as const };
      vi.mocked(apiClient.post).mockResolvedValue({
        data: { id: '2', ...newType, isDefault: false },
      });

      const { result: createResult } = renderHook(() => useCreateResourceType(), { wrapper });

      await act(async () => {
        await createResult.current.mutateAsync(newType);
      });

      // 3. Update the created resource type
      vi.mocked(apiClient.patch).mockResolvedValue({
        data: { id: '2', name: 'Senior Therapist', category: 'STAFF', isDefault: false },
      });

      const { result: updateResult } = renderHook(() => useUpdateResourceType(), { wrapper });

      await act(async () => {
        await updateResult.current.mutateAsync({
          id: '2',
          updates: { name: 'Senior Therapist' },
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/resource-types/2/', {
        name: 'Senior Therapist',
      });

      // 4. Delete the resource type
      vi.mocked(apiClient.delete).mockResolvedValue({});

      const { result: deleteResult } = renderHook(() => useDeleteResourceType(), { wrapper });

      await act(async () => {
        await deleteResult.current.mutateAsync('2');
      });

      expect(apiClient.delete).toHaveBeenCalledWith('/resource-types/2/');
    });

    it('handles concurrent mutations correctly', async () => {
      const wrapper = createWrapper();

      vi.mocked(apiClient.post).mockResolvedValue({
        data: { id: '1', name: 'Type 1', category: 'STAFF', isDefault: false },
      });

      const { result: createResult1 } = renderHook(() => useCreateResourceType(), { wrapper });
      const { result: createResult2 } = renderHook(() => useCreateResourceType(), { wrapper });

      await act(async () => {
        await Promise.all([
          createResult1.current.mutateAsync({ name: 'Type 1', category: 'STAFF' }),
          createResult2.current.mutateAsync({ name: 'Type 2', category: 'OTHER' }),
        ]);
      });

      expect(apiClient.post).toHaveBeenCalledTimes(2);
    });
  });
});
