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 the customDomains API
vi.mock('../../api/customDomains', () => ({
  getCustomDomains: vi.fn(),
  addCustomDomain: vi.fn(),
  deleteCustomDomain: vi.fn(),
  verifyCustomDomain: vi.fn(),
  setPrimaryDomain: vi.fn(),
}));

import {
  useCustomDomains,
  useAddCustomDomain,
  useDeleteCustomDomain,
  useVerifyCustomDomain,
  useSetPrimaryDomain,
} from '../useCustomDomains';
import * as customDomainsApi from '../../api/customDomains';

// Create wrapper with fresh QueryClient for each test
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);
  };
};

// Mock data
const mockCustomDomain = {
  id: 1,
  domain: 'example.com',
  is_verified: true,
  ssl_provisioned: true,
  is_primary: true,
  verification_token: 'abc123',
  dns_txt_record: 'smoothschedule-verify=abc123',
  dns_txt_record_name: '_smoothschedule',
  created_at: '2024-01-01T00:00:00Z',
  verified_at: '2024-01-01T12:00:00Z',
};

const mockUnverifiedDomain = {
  id: 2,
  domain: 'test.com',
  is_verified: false,
  ssl_provisioned: false,
  is_primary: false,
  verification_token: 'xyz789',
  dns_txt_record: 'smoothschedule-verify=xyz789',
  dns_txt_record_name: '_smoothschedule',
  created_at: '2024-01-02T00:00:00Z',
};

const mockCustomDomains = [mockCustomDomain, mockUnverifiedDomain];

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

  // ============================================
  // Query Hook - useCustomDomains
  // ============================================

  describe('useCustomDomains', () => {
    it('fetches all custom domains successfully', async () => {
      vi.mocked(customDomainsApi.getCustomDomains).mockResolvedValue(mockCustomDomains);

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

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

      expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(1);
      expect(result.current.data).toEqual(mockCustomDomains);
      expect(result.current.data).toHaveLength(2);
    });

    it('returns empty array when no domains exist', async () => {
      vi.mocked(customDomainsApi.getCustomDomains).mockResolvedValue([]);

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

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

      expect(result.current.data).toEqual([]);
      expect(result.current.data).toHaveLength(0);
    });

    it('handles fetch errors without retrying', async () => {
      vi.mocked(customDomainsApi.getCustomDomains).mockRejectedValue(
        new Error('Failed to fetch domains')
      );

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

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

      expect(result.current.error).toEqual(new Error('Failed to fetch domains'));
      expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(1); // No retry
    });

    it('uses staleTime of 5 minutes', async () => {
      vi.mocked(customDomainsApi.getCustomDomains).mockResolvedValue(mockCustomDomains);

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

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

      expect(result.current.dataUpdatedAt).toBeGreaterThan(0);
    });

    it('handles 404 errors gracefully', async () => {
      const notFoundError = new Error('Not found');
      vi.mocked(customDomainsApi.getCustomDomains).mockRejectedValue(notFoundError);

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

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

      expect(result.current.error).toEqual(notFoundError);
      expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(1);
    });
  });

  // ============================================
  // Mutation Hook - useAddCustomDomain
  // ============================================

  describe('useAddCustomDomain', () => {
    it('adds a new custom domain successfully', async () => {
      const newDomain = { ...mockUnverifiedDomain, domain: 'newdomain.com' };
      vi.mocked(customDomainsApi.addCustomDomain).mockResolvedValue(newDomain);

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

      let addedDomain;
      await act(async () => {
        addedDomain = await result.current.mutateAsync('newdomain.com');
      });

      expect(customDomainsApi.addCustomDomain).toHaveBeenCalledWith(
        'newdomain.com',
        expect.anything()
      );
      expect(addedDomain).toEqual(newDomain);
    });

    it('invalidates customDomains query on success', async () => {
      const newDomain = { ...mockUnverifiedDomain, domain: 'another.com' };
      vi.mocked(customDomainsApi.getCustomDomains)
        .mockResolvedValueOnce(mockCustomDomains)
        .mockResolvedValueOnce([...mockCustomDomains, newDomain]);
      vi.mocked(customDomainsApi.addCustomDomain).mockResolvedValue(newDomain);

      const wrapper = createWrapper();

      // First fetch custom domains
      const { result: domainsResult } = renderHook(() => useCustomDomains(), {
        wrapper,
      });

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

      expect(domainsResult.current.data).toHaveLength(2);

      // Add a new domain
      const { result: addResult } = renderHook(() => useAddCustomDomain(), {
        wrapper,
      });

      await act(async () => {
        await addResult.current.mutateAsync('another.com');
      });

      // Verify customDomains was invalidated and refetched
      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(2);
      });
    });

    it('handles add domain errors', async () => {
      vi.mocked(customDomainsApi.addCustomDomain).mockRejectedValue(
        new Error('Domain already exists')
      );

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync('example.com');
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(new Error('Domain already exists'));
    });

    it('handles domain with uppercase and whitespace', async () => {
      const newDomain = { ...mockUnverifiedDomain, domain: 'test.com' };
      vi.mocked(customDomainsApi.addCustomDomain).mockResolvedValue(newDomain);

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

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

      // API should normalize the domain
      expect(customDomainsApi.addCustomDomain).toHaveBeenCalledWith(
        '  TEST.COM  ',
        expect.anything()
      );
    });
  });

  // ============================================
  // Mutation Hook - useDeleteCustomDomain
  // ============================================

  describe('useDeleteCustomDomain', () => {
    it('deletes a custom domain successfully', async () => {
      vi.mocked(customDomainsApi.deleteCustomDomain).mockResolvedValue(undefined);

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

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

      expect(customDomainsApi.deleteCustomDomain).toHaveBeenCalledWith(2, expect.anything());
    });

    it('invalidates customDomains query on success', async () => {
      vi.mocked(customDomainsApi.getCustomDomains)
        .mockResolvedValueOnce(mockCustomDomains)
        .mockResolvedValueOnce([mockCustomDomain]); // After delete
      vi.mocked(customDomainsApi.deleteCustomDomain).mockResolvedValue(undefined);

      const wrapper = createWrapper();

      // First fetch custom domains
      const { result: domainsResult } = renderHook(() => useCustomDomains(), {
        wrapper,
      });

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

      expect(domainsResult.current.data).toHaveLength(2);

      // Delete a domain
      const { result: deleteResult } = renderHook(() => useDeleteCustomDomain(), {
        wrapper,
      });

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

      // Verify customDomains was invalidated and refetched
      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(2);
      });
    });

    it('handles delete domain errors', async () => {
      vi.mocked(customDomainsApi.deleteCustomDomain).mockRejectedValue(
        new Error('Cannot delete primary domain')
      );

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync(1);
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(new Error('Cannot delete primary domain'));
    });

    it('handles 404 errors for non-existent domains', async () => {
      vi.mocked(customDomainsApi.deleteCustomDomain).mockRejectedValue(
        new Error('Domain not found')
      );

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync(999);
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(new Error('Domain not found'));
    });
  });

  // ============================================
  // Mutation Hook - useVerifyCustomDomain
  // ============================================

  describe('useVerifyCustomDomain', () => {
    it('verifies a custom domain successfully', async () => {
      const verifyResponse = { verified: true, message: 'Domain verified successfully' };
      vi.mocked(customDomainsApi.verifyCustomDomain).mockResolvedValue(verifyResponse);

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

      let verifyResult;
      await act(async () => {
        verifyResult = await result.current.mutateAsync(2);
      });

      expect(customDomainsApi.verifyCustomDomain).toHaveBeenCalledWith(2, expect.anything());
      expect(verifyResult).toEqual(verifyResponse);
    });

    it('returns failure when verification fails', async () => {
      const verifyResponse = {
        verified: false,
        message: 'TXT record not found. Please add the DNS record and try again.',
      };
      vi.mocked(customDomainsApi.verifyCustomDomain).mockResolvedValue(verifyResponse);

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

      let verifyResult;
      await act(async () => {
        verifyResult = await result.current.mutateAsync(2);
      });

      expect(verifyResult).toEqual(verifyResponse);
      expect(verifyResult?.verified).toBe(false);
    });

    it('invalidates customDomains query on success', async () => {
      const verifyResponse = { verified: true, message: 'Domain verified' };
      const verifiedDomain = { ...mockUnverifiedDomain, is_verified: true };

      vi.mocked(customDomainsApi.getCustomDomains)
        .mockResolvedValueOnce(mockCustomDomains)
        .mockResolvedValueOnce([mockCustomDomain, verifiedDomain]);
      vi.mocked(customDomainsApi.verifyCustomDomain).mockResolvedValue(verifyResponse);

      const wrapper = createWrapper();

      // First fetch custom domains
      const { result: domainsResult } = renderHook(() => useCustomDomains(), {
        wrapper,
      });

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

      // Verify a domain
      const { result: verifyResult } = renderHook(() => useVerifyCustomDomain(), {
        wrapper,
      });

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

      // Verify customDomains was invalidated and refetched
      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(2);
      });
    });

    it('handles verification errors', async () => {
      vi.mocked(customDomainsApi.verifyCustomDomain).mockRejectedValue(
        new Error('Verification service unavailable')
      );

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync(2);
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(new Error('Verification service unavailable'));
    });

    it('invalidates even on failed verification (not error)', async () => {
      const verifyResponse = { verified: false, message: 'TXT record not found' };
      vi.mocked(customDomainsApi.getCustomDomains).mockResolvedValue(mockCustomDomains);
      vi.mocked(customDomainsApi.verifyCustomDomain).mockResolvedValue(verifyResponse);

      const wrapper = createWrapper();

      const { result: domainsResult } = renderHook(() => useCustomDomains(), {
        wrapper,
      });

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

      const { result: verifyResult } = renderHook(() => useVerifyCustomDomain(), {
        wrapper,
      });

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

      // Should still invalidate even though verified=false
      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(2);
      });
    });
  });

  // ============================================
  // Mutation Hook - useSetPrimaryDomain
  // ============================================

  describe('useSetPrimaryDomain', () => {
    it('sets a domain as primary successfully', async () => {
      const updatedDomain = { ...mockUnverifiedDomain, is_primary: true };
      vi.mocked(customDomainsApi.setPrimaryDomain).mockResolvedValue(updatedDomain);

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

      let primaryDomain;
      await act(async () => {
        primaryDomain = await result.current.mutateAsync(2);
      });

      expect(customDomainsApi.setPrimaryDomain).toHaveBeenCalledWith(2, expect.anything());
      expect(primaryDomain).toEqual(updatedDomain);
      expect(primaryDomain?.is_primary).toBe(true);
    });

    it('invalidates customDomains query on success', async () => {
      const updatedPrimaryDomain = { ...mockUnverifiedDomain, is_primary: true };
      const oldPrimaryDomain = { ...mockCustomDomain, is_primary: false };

      vi.mocked(customDomainsApi.getCustomDomains)
        .mockResolvedValueOnce(mockCustomDomains)
        .mockResolvedValueOnce([oldPrimaryDomain, updatedPrimaryDomain]);
      vi.mocked(customDomainsApi.setPrimaryDomain).mockResolvedValue(updatedPrimaryDomain);

      const wrapper = createWrapper();

      // First fetch custom domains
      const { result: domainsResult } = renderHook(() => useCustomDomains(), {
        wrapper,
      });

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

      // Set new primary domain
      const { result: setPrimaryResult } = renderHook(() => useSetPrimaryDomain(), {
        wrapper,
      });

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

      // Verify customDomains was invalidated and refetched
      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(2);
      });
    });

    it('handles set primary domain errors', async () => {
      vi.mocked(customDomainsApi.setPrimaryDomain).mockRejectedValue(
        new Error('Domain must be verified before setting as primary')
      );

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync(2);
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(
        new Error('Domain must be verified before setting as primary')
      );
    });

    it('handles non-existent domain errors', async () => {
      vi.mocked(customDomainsApi.setPrimaryDomain).mockRejectedValue(
        new Error('Domain not found')
      );

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync(999);
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(new Error('Domain not found'));
    });
  });

  // ============================================
  // Integration Tests
  // ============================================

  describe('Integration - Query invalidation', () => {
    it('all mutations invalidate the customDomains query', async () => {
      const wrapper = createWrapper();

      vi.mocked(customDomainsApi.getCustomDomains).mockResolvedValue(mockCustomDomains);
      vi.mocked(customDomainsApi.addCustomDomain).mockResolvedValue(mockUnverifiedDomain);
      vi.mocked(customDomainsApi.deleteCustomDomain).mockResolvedValue(undefined);
      vi.mocked(customDomainsApi.verifyCustomDomain).mockResolvedValue({
        verified: true,
        message: 'Success',
      });
      vi.mocked(customDomainsApi.setPrimaryDomain).mockResolvedValue(mockCustomDomain);

      // Initial fetch
      const { result: queryResult } = renderHook(() => useCustomDomains(), {
        wrapper,
      });

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

      expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(1);

      // Test add mutation
      const { result: addResult } = renderHook(() => useAddCustomDomain(), {
        wrapper,
      });

      await act(async () => {
        await addResult.current.mutateAsync('new.com');
      });

      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(2);
      });

      // Test delete mutation
      const { result: deleteResult } = renderHook(() => useDeleteCustomDomain(), {
        wrapper,
      });

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

      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(3);
      });

      // Test verify mutation
      const { result: verifyResult } = renderHook(() => useVerifyCustomDomain(), {
        wrapper,
      });

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

      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(4);
      });

      // Test setPrimary mutation
      const { result: setPrimaryResult } = renderHook(() => useSetPrimaryDomain(), {
        wrapper,
      });

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

      await waitFor(() => {
        expect(customDomainsApi.getCustomDomains).toHaveBeenCalledTimes(5);
      });
    });
  });
});
