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 {
  useAppointments,
  useAppointment,
  useCreateAppointment,
  useUpdateAppointment,
  useDeleteAppointment,
  useRescheduleAppointment,
} from '../useAppointments';
import apiClient from '../../api/client';
import { AppointmentStatus } 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('useAppointments hooks', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('useAppointments', () => {
    it('fetches appointments and transforms data correctly', async () => {
      const mockAppointments = [
        {
          id: 1,
          resource_id: 5,
          customer_id: 10,
          customer_name: 'John Doe',
          service_id: 15,
          start_time: '2024-01-15T10:00:00Z',
          end_time: '2024-01-15T11:00:00Z',
          duration_minutes: 60,
          status: 'SCHEDULED',
          notes: 'First appointment',
        },
        {
          id: 2,
          resource_id: null, // unassigned
          customer: 20, // alternative field name
          customer_name: 'Jane Smith',
          service: 25, // alternative field name
          start_time: '2024-01-15T14:00:00Z',
          end_time: '2024-01-15T14:30:00Z',
          status: 'COMPLETED',
          notes: '',
        },
      ];
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockAppointments });

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

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

      expect(apiClient.get).toHaveBeenCalledWith('/appointments/?');
      expect(result.current.data).toHaveLength(2);

      // Verify first appointment transformation
      expect(result.current.data?.[0]).toEqual({
        id: '1',
        resourceId: '5',
        customerId: '10',
        customerName: 'John Doe',
        serviceId: '15',
        startTime: new Date('2024-01-15T10:00:00Z'),
        durationMinutes: 60,
        status: 'SCHEDULED',
        notes: 'First appointment',
        depositAmount: null,
        depositTransactionId: '',
        finalChargeTransactionId: '',
        finalPrice: null,
        isVariablePricing: false,
        overpaidAmount: null,
        remainingBalance: null,
      });

      // Verify second appointment transformation (with alternative field names and null resource)
      expect(result.current.data?.[1]).toEqual({
        id: '2',
        resourceId: null,
        customerId: '20',
        customerName: 'Jane Smith',
        serviceId: '25',
        startTime: new Date('2024-01-15T14:00:00Z'),
        durationMinutes: 30,
        status: 'COMPLETED',
        notes: '',
        depositAmount: null,
        depositTransactionId: '',
        finalChargeTransactionId: '',
        finalPrice: null,
        isVariablePricing: false,
        overpaidAmount: null,
        remainingBalance: null,
      });
    });

    it('calculates duration from start_time and end_time when duration_minutes missing', async () => {
      const mockAppointments = [
        {
          id: 1,
          resource_id: 5,
          customer_id: 10,
          customer_name: 'John Doe',
          service_id: 15,
          start_time: '2024-01-15T10:00:00Z',
          end_time: '2024-01-15T10:45:00Z', // 45 minute duration
          status: 'SCHEDULED',
        },
      ];
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockAppointments });

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

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

      expect(result.current.data?.[0].durationMinutes).toBe(45);
    });

    it('applies resource filter to API call', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [] });

      renderHook(() => useAppointments({ resource: '5' }), {
        wrapper: createWrapper(),
      });

      await waitFor(() => {
        expect(apiClient.get).toHaveBeenCalledWith('/appointments/?resource=5');
      });
    });

    it('applies status filter to API call', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [] });

      renderHook(() => useAppointments({ status: 'COMPLETED' as AppointmentStatus }), {
        wrapper: createWrapper(),
      });

      await waitFor(() => {
        expect(apiClient.get).toHaveBeenCalledWith('/appointments/?status=COMPLETED');
      });
    });

    it('applies startDate filter as ISO string with start of day', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [] });

      const startDate = new Date('2024-01-15T14:30:00Z');
      renderHook(() => useAppointments({ startDate }), {
        wrapper: createWrapper(),
      });

      await waitFor(() => {
        const call = vi.mocked(apiClient.get).mock.calls[0][0];
        expect(call).toContain('/appointments/?');
        expect(call).toContain('start_date=');

        // Extract the start_date param
        const url = new URL(call, 'http://localhost');
        const startDateParam = url.searchParams.get('start_date');
        expect(startDateParam).toBeTruthy();

        // Should be start of day in local timezone, converted to ISO
        // The implementation does: new Date(filters.startDate), then setHours(0,0,0,0), then toISOString()
        // So we just verify it's an ISO string
        const parsedDate = new Date(startDateParam!);
        expect(parsedDate).toBeInstanceOf(Date);
        expect(parsedDate.getTime()).toBeGreaterThan(0);
      });
    });

    it('applies endDate filter as ISO string with start of day', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [] });

      const endDate = new Date('2024-01-20T18:45:00Z');
      renderHook(() => useAppointments({ endDate }), {
        wrapper: createWrapper(),
      });

      await waitFor(() => {
        const call = vi.mocked(apiClient.get).mock.calls[0][0];
        expect(call).toContain('/appointments/?');
        expect(call).toContain('end_date=');

        // Extract the end_date param
        const url = new URL(call, 'http://localhost');
        const endDateParam = url.searchParams.get('end_date');
        expect(endDateParam).toBeTruthy();

        // Should be start of day in local timezone, converted to ISO
        // The implementation does: new Date(filters.endDate), then setHours(0,0,0,0), then toISOString()
        // So we just verify it's an ISO string
        const parsedDate = new Date(endDateParam!);
        expect(parsedDate).toBeInstanceOf(Date);
        expect(parsedDate.getTime()).toBeGreaterThan(0);
      });
    });

    it('applies multiple filters together', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [] });

      const startDate = new Date('2024-01-15');
      const endDate = new Date('2024-01-20');

      renderHook(() => useAppointments({
        resource: '5',
        status: 'SCHEDULED' as AppointmentStatus,
        startDate,
        endDate,
      }), {
        wrapper: createWrapper(),
      });

      await waitFor(() => {
        const call = vi.mocked(apiClient.get).mock.calls[0][0];
        expect(call).toContain('resource=5');
        expect(call).toContain('status=SCHEDULED');
        expect(call).toContain('start_date=');
        expect(call).toContain('end_date=');
      });
    });
  });

  describe('useAppointment', () => {
    it('fetches single appointment by id and transforms data', async () => {
      const mockAppointment = {
        id: 1,
        resource_id: 5,
        customer_id: 10,
        customer_name: 'John Doe',
        service_id: 15,
        start_time: '2024-01-15T10:00:00Z',
        end_time: '2024-01-15T11:00:00Z',
        duration_minutes: 60,
        status: 'SCHEDULED',
        notes: 'Test note',
      };
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockAppointment });

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

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

      expect(apiClient.get).toHaveBeenCalledWith('/appointments/1/');
      expect(result.current.data).toEqual({
        id: '1',
        resourceId: '5',
        customerId: '10',
        customerName: 'John Doe',
        serviceId: '15',
        startTime: new Date('2024-01-15T10:00:00Z'),
        durationMinutes: 60,
        status: 'SCHEDULED',
        notes: 'Test note',
        depositAmount: null,
        depositTransactionId: '',
        finalChargeTransactionId: '',
        finalPrice: null,
        isVariablePricing: false,
        overpaidAmount: null,
        remainingBalance: null,
      });
    });

    it('does not fetch when id is empty (enabled condition)', async () => {
      const { result } = renderHook(() => useAppointment(''), {
        wrapper: createWrapper(),
      });

      await waitFor(() => {
        expect(result.current.isLoading).toBe(false);
      });

      expect(apiClient.get).not.toHaveBeenCalled();
    });

    it('does not fetch when id is null or undefined', async () => {
      const { result: result1 } = renderHook(() => useAppointment(null as any), {
        wrapper: createWrapper(),
      });
      const { result: result2 } = renderHook(() => useAppointment(undefined as any), {
        wrapper: createWrapper(),
      });

      await waitFor(() => {
        expect(result1.current.isLoading).toBe(false);
        expect(result2.current.isLoading).toBe(false);
      });

      expect(apiClient.get).not.toHaveBeenCalled();
    });
  });

  describe('useCreateAppointment', () => {
    it('creates appointment with calculated end_time from startTime + durationMinutes', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });

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

      const startTime = new Date('2024-01-15T10:00:00Z');
      const durationMinutes = 60;

      await act(async () => {
        await result.current.mutateAsync({
          resourceId: '5',
          customerId: '10',
          customerName: 'John Doe',
          serviceId: '15',
          startTime,
          durationMinutes,
          status: 'SCHEDULED',
          notes: 'Test appointment',
        });
      });

      const expectedEndTime = new Date('2024-01-15T11:00:00Z');

      expect(apiClient.post).toHaveBeenCalledWith('/appointments/', {
        service: 15,
        resource: 5,
        customer: 10,
        start_time: startTime.toISOString(),
        end_time: expectedEndTime.toISOString(),
        notes: 'Test appointment',
      });
    });

    it('calculates end_time correctly for 30 minute appointment', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });

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

      const startTime = new Date('2024-01-15T14:00:00Z');
      const durationMinutes = 30;

      await act(async () => {
        await result.current.mutateAsync({
          resourceId: '5',
          customerName: 'Jane Smith',
          serviceId: '15',
          startTime,
          durationMinutes,
          status: 'SCHEDULED',
        });
      });

      const expectedEndTime = new Date('2024-01-15T14:30:00Z');

      expect(apiClient.post).toHaveBeenCalledWith('/appointments/', expect.objectContaining({
        start_time: startTime.toISOString(),
        end_time: expectedEndTime.toISOString(),
      }));
    });

    it('sets resource to null when resourceId is null', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });

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

      await act(async () => {
        await result.current.mutateAsync({
          resourceId: null,
          customerName: 'John Doe',
          serviceId: '15',
          startTime: new Date('2024-01-15T10:00:00Z'),
          durationMinutes: 60,
          status: 'SCHEDULED',
        });
      });

      expect(apiClient.post).toHaveBeenCalledWith('/appointments/', expect.objectContaining({
        resource: null,
      }));
    });

    it('includes customer when customerId is provided', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });

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

      await act(async () => {
        await result.current.mutateAsync({
          resourceId: '5',
          customerId: '42',
          customerName: 'John Doe',
          serviceId: '15',
          startTime: new Date('2024-01-15T10:00:00Z'),
          durationMinutes: 60,
          status: 'SCHEDULED',
        });
      });

      expect(apiClient.post).toHaveBeenCalledWith('/appointments/', expect.objectContaining({
        customer: 42,
      }));
    });

    it('omits customer when customerId is not provided (walk-in)', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });

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

      await act(async () => {
        await result.current.mutateAsync({
          resourceId: '5',
          customerName: 'Walk-in Customer',
          serviceId: '15',
          startTime: new Date('2024-01-15T10:00:00Z'),
          durationMinutes: 60,
          status: 'SCHEDULED',
        });
      });

      const callArgs = vi.mocked(apiClient.post).mock.calls[0][1] as any;
      expect(callArgs.customer).toBeUndefined();
    });

    it('handles empty notes string', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });

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

      await act(async () => {
        await result.current.mutateAsync({
          resourceId: '5',
          customerName: 'John Doe',
          serviceId: '15',
          startTime: new Date('2024-01-15T10:00:00Z'),
          durationMinutes: 60,
          status: 'SCHEDULED',
          notes: '',
        });
      });

      expect(apiClient.post).toHaveBeenCalledWith('/appointments/', expect.objectContaining({
        notes: '',
      }));
    });
  });

  describe('useUpdateAppointment', () => {
    it('updates appointment with backend field mapping', async () => {
      vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } });

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

      await act(async () => {
        await result.current.mutateAsync({
          id: '1',
          updates: {
            serviceId: '20',
            status: 'COMPLETED',
            notes: 'Updated notes',
          },
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/appointments/1/', {
        service: 20,
        status: 'COMPLETED',
        notes: 'Updated notes',
      });
    });

    it('updates resourceId as resource_ids array when provided', async () => {
      vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } });

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

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

      expect(apiClient.patch).toHaveBeenCalledWith('/appointments/1/', {
        resource_ids: [5],
      });
    });

    it('sets resource_ids to empty array when resourceId is null', async () => {
      vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } });

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

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

      expect(apiClient.patch).toHaveBeenCalledWith('/appointments/1/', {
        resource_ids: [],
      });
    });

    it('calculates end_time when both startTime and durationMinutes are provided', async () => {
      vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } });

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

      const startTime = new Date('2024-01-15T10:00:00Z');
      const durationMinutes = 45;

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

      const expectedEndTime = new Date('2024-01-15T10:45:00Z');

      expect(apiClient.patch).toHaveBeenCalledWith('/appointments/1/', {
        start_time: startTime.toISOString(),
        end_time: expectedEndTime.toISOString(),
      });
    });

    it('sends only startTime when durationMinutes not provided', async () => {
      vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } });

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

      const startTime = new Date('2024-01-15T10:00:00Z');

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

      const callArgs = vi.mocked(apiClient.patch).mock.calls[0][1] as any;
      expect(callArgs.start_time).toBe(startTime.toISOString());
      expect(callArgs.end_time).toBeUndefined();
    });

    describe('optimistic updates', () => {
      it('updates appointment in cache immediately (onMutate)', async () => {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },
            mutations: { retry: false },
          },
        });

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

        // Set initial cache data
        queryClient.setQueryData(['appointments'], [
          {
            id: '1',
            resourceId: '5',
            customerId: '10',
            customerName: 'John Doe',
            serviceId: '15',
            startTime: new Date('2024-01-15T10:00:00Z'),
            durationMinutes: 60,
            status: 'SCHEDULED',
            notes: 'Original notes',
          },
        ]);

        // Make patch hang so we can check optimistic update
        let resolveUpdate: any;
        const updatePromise = new Promise((resolve) => {
          resolveUpdate = resolve;
        });
        vi.mocked(apiClient.patch).mockReturnValue(updatePromise as any);

        const { result } = renderHook(() => useUpdateAppointment(), { wrapper });

        // Trigger update
        act(() => {
          result.current.mutate({
            id: '1',
            updates: { notes: 'Updated notes', status: 'COMPLETED' },
          });
        });

        // Check that cache was updated immediately (optimistically)
        await waitFor(() => {
          const cached = queryClient.getQueryData(['appointments']) as any[];
          expect(cached[0].notes).toBe('Updated notes');
          expect(cached[0].status).toBe('COMPLETED');
        });

        // Resolve the update
        resolveUpdate({ data: { id: 1 } });
      });

      it('rolls back on error (onError)', async () => {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },
            mutations: { retry: false },
          },
        });

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

        // Set initial cache data
        const originalData = [
          {
            id: '1',
            resourceId: '5',
            customerId: '10',
            customerName: 'John Doe',
            serviceId: '15',
            startTime: new Date('2024-01-15T10:00:00Z'),
            durationMinutes: 60,
            status: 'SCHEDULED' as const,
            notes: 'Original notes',
          },
        ];
        queryClient.setQueryData(['appointments'], originalData);

        // Make patch fail
        vi.mocked(apiClient.patch).mockRejectedValue(new Error('API Error'));

        const { result } = renderHook(() => useUpdateAppointment(), { wrapper });

        // Trigger update
        await act(async () => {
          try {
            await result.current.mutateAsync({
              id: '1',
              updates: { notes: 'Updated notes' },
            });
          } catch {
            // Expected error
          }
        });

        // Check that cache was rolled back
        await waitFor(() => {
          const cached = queryClient.getQueryData(['appointments']) as any[];
          expect(cached[0].notes).toBe('Original notes');
        });
      });

      it('invalidates queries after success (onSettled)', async () => {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },
            mutations: { retry: false },
          },
        });

        const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');

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

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

        const { result } = renderHook(() => useUpdateAppointment(), { wrapper });

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

        await waitFor(() => {
          expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['appointments'] });
        });
      });

      it('handles multiple queries in cache', async () => {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },
            mutations: { retry: false },
          },
        });

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

        const appointment = {
          id: '1',
          resourceId: '5',
          customerId: '10',
          customerName: 'John Doe',
          serviceId: '15',
          startTime: new Date('2024-01-15T10:00:00Z'),
          durationMinutes: 60,
          status: 'SCHEDULED' as const,
          notes: 'Original notes',
        };

        // Set multiple cache entries
        queryClient.setQueryData(['appointments'], [appointment]);
        queryClient.setQueryData(['appointments', { status: 'SCHEDULED' }], [appointment]);

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

        const { result } = renderHook(() => useUpdateAppointment(), { wrapper });

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

        // Both cache entries should be updated
        await waitFor(() => {
          const cache1 = queryClient.getQueryData(['appointments']) as any[];
          const cache2 = queryClient.getQueryData(['appointments', { status: 'SCHEDULED' }]) as any[];

          expect(cache1[0].notes).toBe('Updated notes');
          expect(cache2[0].notes).toBe('Updated notes');
        });
      });
    });
  });

  describe('useDeleteAppointment', () => {
    it('deletes appointment by id', async () => {
      vi.mocked(apiClient.delete).mockResolvedValue({} as any);

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

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

      expect(apiClient.delete).toHaveBeenCalledWith('/appointments/1/');
    });

    describe('optimistic updates', () => {
      it('removes appointment from cache immediately (onMutate)', async () => {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },
            mutations: { retry: false },
          },
        });

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

        // Set initial cache data with 2 appointments
        queryClient.setQueryData(['appointments'], [
          {
            id: '1',
            resourceId: '5',
            customerId: '10',
            customerName: 'John Doe',
            serviceId: '15',
            startTime: new Date('2024-01-15T10:00:00Z'),
            durationMinutes: 60,
            status: 'SCHEDULED',
          },
          {
            id: '2',
            resourceId: '6',
            customerId: '11',
            customerName: 'Jane Smith',
            serviceId: '16',
            startTime: new Date('2024-01-15T14:00:00Z'),
            durationMinutes: 30,
            status: 'SCHEDULED',
          },
        ]);

        // Make delete hang so we can check optimistic update
        let resolveDelete: any;
        const deletePromise = new Promise((resolve) => {
          resolveDelete = resolve;
        });
        vi.mocked(apiClient.delete).mockReturnValue(deletePromise as any);

        const { result } = renderHook(() => useDeleteAppointment(), { wrapper });

        // Trigger delete
        act(() => {
          result.current.mutate('1');
        });

        // Check that appointment was removed from cache immediately
        await waitFor(() => {
          const cached = queryClient.getQueryData(['appointments']) as any[];
          expect(cached).toHaveLength(1);
          expect(cached[0].id).toBe('2');
        });

        // Resolve the delete
        resolveDelete({});
      });

      it('rolls back on error (onError)', async () => {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },
            mutations: { retry: false },
          },
        });

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

        // Set initial cache data
        const originalData = [
          {
            id: '1',
            resourceId: '5',
            customerId: '10',
            customerName: 'John Doe',
            serviceId: '15',
            startTime: new Date('2024-01-15T10:00:00Z'),
            durationMinutes: 60,
            status: 'SCHEDULED' as const,
          },
        ];
        queryClient.setQueryData(['appointments'], originalData);

        // Make delete fail
        vi.mocked(apiClient.delete).mockRejectedValue(new Error('API Error'));

        const { result } = renderHook(() => useDeleteAppointment(), { wrapper });

        // Trigger delete
        await act(async () => {
          try {
            await result.current.mutateAsync('1');
          } catch {
            // Expected error
          }
        });

        // Check that cache was rolled back
        await waitFor(() => {
          const cached = queryClient.getQueryData(['appointments']) as any[];
          expect(cached).toHaveLength(1);
          expect(cached[0].id).toBe('1');
        });
      });

      it('invalidates queries after success (onSettled)', async () => {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },
            mutations: { retry: false },
          },
        });

        const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');

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

        vi.mocked(apiClient.delete).mockResolvedValue({} as any);

        const { result } = renderHook(() => useDeleteAppointment(), { wrapper });

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

        await waitFor(() => {
          expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['appointments'] });
        });
      });
    });
  });

  describe('useRescheduleAppointment', () => {
    it('fetches appointment, then updates with new start time and resource', async () => {
      // Mock the GET to fetch current appointment
      vi.mocked(apiClient.get).mockResolvedValue({
        data: {
          id: 1,
          duration_minutes: 60,
        },
      });

      // Mock the PATCH to update
      vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } });

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

      const newStartTime = new Date('2024-01-16T14:00:00Z');

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

      // Verify GET was called to fetch appointment
      expect(apiClient.get).toHaveBeenCalledWith('/appointments/1/');

      // Verify PATCH was called with new start time, duration, and resource_ids
      expect(apiClient.patch).toHaveBeenCalledWith('/appointments/1/', {
        start_time: newStartTime.toISOString(),
        end_time: new Date('2024-01-16T15:00:00Z').toISOString(),
        resource_ids: [7],
      });
    });

    it('preserves duration from original appointment', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({
        data: {
          id: 1,
          duration_minutes: 45, // Different duration
        },
      });

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

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

      const newStartTime = new Date('2024-01-16T14:00:00Z');

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

      // Should use the fetched duration (45 minutes)
      expect(apiClient.patch).toHaveBeenCalledWith('/appointments/1/', expect.objectContaining({
        start_time: newStartTime.toISOString(),
        end_time: new Date('2024-01-16T14:45:00Z').toISOString(),
      }));
    });

    it('does not update resource when newResourceId is not provided', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({
        data: {
          id: 1,
          duration_minutes: 60,
        },
      });

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

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

      const newStartTime = new Date('2024-01-16T14:00:00Z');

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

      const callArgs = vi.mocked(apiClient.patch).mock.calls[0][1] as any;
      expect(callArgs.resource_ids).toBeUndefined();
    });

    it('sets resource when newResourceId is explicitly null', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({
        data: {
          id: 1,
          duration_minutes: 60,
        },
      });

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

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

      const newStartTime = new Date('2024-01-16T14:00:00Z');

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

      expect(apiClient.patch).toHaveBeenCalledWith('/appointments/1/', expect.objectContaining({
        resource_ids: [],
      }));
    });
  });

  describe('calculateDuration helper (internal)', () => {
    it('calculates correct duration for 60 minute appointment', async () => {
      const mockAppointment = {
        id: 1,
        resource_id: 5,
        customer_id: 10,
        customer_name: 'John Doe',
        service_id: 15,
        start_time: '2024-01-15T10:00:00Z',
        end_time: '2024-01-15T11:00:00Z', // 60 minutes later
        status: 'SCHEDULED',
      };
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockAppointment });

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

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

      expect(result.current.data?.durationMinutes).toBe(60);
    });

    it('calculates correct duration for 30 minute appointment', async () => {
      const mockAppointment = {
        id: 1,
        resource_id: 5,
        customer_id: 10,
        customer_name: 'John Doe',
        service_id: 15,
        start_time: '2024-01-15T10:00:00Z',
        end_time: '2024-01-15T10:30:00Z', // 30 minutes later
        status: 'SCHEDULED',
      };
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockAppointment });

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

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

      expect(result.current.data?.durationMinutes).toBe(30);
    });

    it('rounds to nearest minute for fractional durations', async () => {
      const mockAppointment = {
        id: 1,
        resource_id: 5,
        customer_id: 10,
        customer_name: 'John Doe',
        service_id: 15,
        start_time: '2024-01-15T10:00:00Z',
        end_time: '2024-01-15T10:45:30Z', // 45.5 minutes later
        status: 'SCHEDULED',
      };
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockAppointment });

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

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

      // Should round to 46 minutes
      expect(result.current.data?.durationMinutes).toBe(46);
    });
  });
});
