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 dependencies
vi.mock('../../api/oauth', () => ({
  getOAuthProviders: vi.fn(),
  getOAuthConnections: vi.fn(),
  initiateOAuth: vi.fn(),
  handleOAuthCallback: vi.fn(),
  disconnectOAuth: vi.fn(),
}));

vi.mock('../../utils/cookies', () => ({
  setCookie: vi.fn(),
}));

import {
  useOAuthProviders,
  useOAuthConnections,
  useInitiateOAuth,
  useOAuthCallback,
  useDisconnectOAuth,
} from '../useOAuth';
import * as oauthApi from '../../api/oauth';
import * as cookies from '../../utils/cookies';

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

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

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

  describe('useOAuthProviders', () => {
    it('fetches OAuth providers successfully', async () => {
      const mockProviders: oauthApi.OAuthProvider[] = [
        {
          name: 'google',
          display_name: 'Google',
          icon: 'https://example.com/google.png',
        },
        {
          name: 'microsoft',
          display_name: 'Microsoft',
          icon: 'https://example.com/microsoft.png',
        },
      ];

      vi.mocked(oauthApi.getOAuthProviders).mockResolvedValue(mockProviders);

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

      expect(result.current.isLoading).toBe(true);

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

      expect(result.current.data).toEqual(mockProviders);
      expect(oauthApi.getOAuthProviders).toHaveBeenCalledTimes(1);
    });

    it('handles errors when fetching providers fails', async () => {
      const mockError = new Error('Failed to fetch providers');
      vi.mocked(oauthApi.getOAuthProviders).mockRejectedValue(mockError);

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

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

      expect(result.current.isError).toBe(true);
      expect(result.current.error).toEqual(mockError);
    });

    it('uses correct query configuration', () => {
      vi.mocked(oauthApi.getOAuthProviders).mockResolvedValue([]);

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

      // The hook should be configured with staleTime and refetchOnWindowFocus
      // We can verify this by checking that the hook doesn't refetch immediately
      expect(result.current.isLoading).toBe(true);
    });
  });

  describe('useOAuthConnections', () => {
    it('fetches OAuth connections successfully', async () => {
      const mockConnections: oauthApi.OAuthConnection[] = [
        {
          id: '1',
          provider: 'google',
          provider_user_id: 'user123',
          email: 'test@example.com',
          connected_at: '2025-01-01T00:00:00Z',
        },
        {
          id: '2',
          provider: 'microsoft',
          provider_user_id: 'user456',
          email: 'test@microsoft.com',
          connected_at: '2025-01-02T00:00:00Z',
        },
      ];

      vi.mocked(oauthApi.getOAuthConnections).mockResolvedValue(mockConnections);

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

      expect(result.current.isLoading).toBe(true);

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

      expect(result.current.data).toEqual(mockConnections);
      expect(oauthApi.getOAuthConnections).toHaveBeenCalledTimes(1);
    });

    it('handles errors when fetching connections fails', async () => {
      const mockError = new Error('Failed to fetch connections');
      vi.mocked(oauthApi.getOAuthConnections).mockRejectedValue(mockError);

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

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

      expect(result.current.isError).toBe(true);
      expect(result.current.error).toEqual(mockError);
    });

    it('returns empty array when no connections exist', async () => {
      vi.mocked(oauthApi.getOAuthConnections).mockResolvedValue([]);

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

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

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

  describe('useInitiateOAuth', () => {
    it('initiates OAuth flow and redirects to authorization URL', async () => {
      const mockAuthUrl = 'https://accounts.google.com/oauth/authorize?client_id=123';
      vi.mocked(oauthApi.initiateOAuth).mockResolvedValue({
        authorization_url: mockAuthUrl,
      });

      // Mock window.location
      const originalLocation = window.location;
      delete (window as any).location;
      window.location = { ...originalLocation, href: '' } as Location;

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

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

      expect(oauthApi.initiateOAuth).toHaveBeenCalledWith('google');
      expect(window.location.href).toBe(mockAuthUrl);

      // Restore window.location
      window.location = originalLocation;
    });

    it('handles errors when initiating OAuth fails', async () => {
      const mockError = new Error('Failed to initiate OAuth');
      vi.mocked(oauthApi.initiateOAuth).mockRejectedValue(mockError);

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

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

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

    it('supports multiple OAuth providers', async () => {
      const providers = ['google', 'microsoft', 'github'];

      for (const provider of providers) {
        vi.mocked(oauthApi.initiateOAuth).mockResolvedValue({
          authorization_url: `https://${provider}.com/oauth/authorize`,
        });

        const originalLocation = window.location;
        delete (window as any).location;
        window.location = { ...originalLocation, href: '' } as Location;

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

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

        expect(oauthApi.initiateOAuth).toHaveBeenCalledWith(provider);
        expect(window.location.href).toBe(`https://${provider}.com/oauth/authorize`);

        window.location = originalLocation;
        vi.clearAllMocks();
      }
    });
  });

  describe('useOAuthCallback', () => {
    it('handles OAuth callback and stores tokens in cookies', async () => {
      const mockResponse: oauthApi.OAuthTokenResponse = {
        access: 'access-token-123',
        refresh: 'refresh-token-456',
        user: {
          id: 1,
          username: 'testuser',
          email: 'test@example.com',
          name: 'Test User',
          role: 'owner',
          is_staff: false,
          is_superuser: false,
        },
      };

      vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse);

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

      await act(async () => {
        await result.current.mutateAsync({
          provider: 'google',
          code: 'auth-code-123',
          state: 'state-456',
        });
      });

      expect(oauthApi.handleOAuthCallback).toHaveBeenCalledWith(
        'google',
        'auth-code-123',
        'state-456'
      );
      expect(cookies.setCookie).toHaveBeenCalledWith('access_token', 'access-token-123', 7);
      expect(cookies.setCookie).toHaveBeenCalledWith('refresh_token', 'refresh-token-456', 7);
    });

    it('sets user in cache after successful callback', async () => {
      const mockUser = {
        id: 1,
        username: 'testuser',
        email: 'test@example.com',
        name: 'Test User',
        role: 'owner',
        is_staff: false,
        is_superuser: false,
      };

      const mockResponse: oauthApi.OAuthTokenResponse = {
        access: 'access-token',
        refresh: 'refresh-token',
        user: mockUser,
      };

      vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse);

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

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

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

      await act(async () => {
        await result.current.mutateAsync({
          provider: 'google',
          code: 'code',
          state: 'state',
        });
      });

      // Verify user was set in cache
      const cachedUser = queryClient.getQueryData(['currentUser']);
      expect(cachedUser).toEqual(mockUser);
    });

    it('invalidates OAuth connections after successful callback', async () => {
      const mockResponse: oauthApi.OAuthTokenResponse = {
        access: 'access-token',
        refresh: 'refresh-token',
        user: {
          id: 1,
          username: 'testuser',
          email: 'test@example.com',
          name: 'Test User',
          role: 'owner',
          is_staff: false,
          is_superuser: false,
        },
      };

      vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse);

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

      // Set initial connections data
      queryClient.setQueryData(['oauthConnections'], []);

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

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

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

      await act(async () => {
        await result.current.mutateAsync({
          provider: 'google',
          code: 'code',
          state: 'state',
        });
      });

      expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['oauthConnections'] });
    });

    it('handles errors during OAuth callback', async () => {
      const mockError = new Error('Invalid authorization code');
      vi.mocked(oauthApi.handleOAuthCallback).mockRejectedValue(mockError);

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync({
            provider: 'google',
            code: 'invalid-code',
            state: 'state',
          });
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(mockError);
      await waitFor(() => {
        expect(result.current.isError).toBe(true);
      });
      expect(result.current.error).toEqual(mockError);
      expect(cookies.setCookie).not.toHaveBeenCalled();
    });

    it('handles callback with optional user fields', async () => {
      const mockResponse: oauthApi.OAuthTokenResponse = {
        access: 'access-token',
        refresh: 'refresh-token',
        user: {
          id: 1,
          username: 'testuser',
          email: 'test@example.com',
          name: 'Test User',
          role: 'owner',
          avatar_url: 'https://example.com/avatar.png',
          is_staff: true,
          is_superuser: false,
          business: 123,
          business_name: 'Test Business',
          business_subdomain: 'testbiz',
        },
      };

      vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse);

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

      await act(async () => {
        await result.current.mutateAsync({
          provider: 'microsoft',
          code: 'code',
          state: 'state',
        });
      });

      expect(cookies.setCookie).toHaveBeenCalledWith('access_token', 'access-token', 7);
      expect(cookies.setCookie).toHaveBeenCalledWith('refresh_token', 'refresh-token', 7);
    });
  });

  describe('useDisconnectOAuth', () => {
    it('disconnects OAuth provider successfully', async () => {
      vi.mocked(oauthApi.disconnectOAuth).mockResolvedValue(undefined);

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

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

      // React Query passes mutation context as second parameter
      expect(oauthApi.disconnectOAuth).toHaveBeenCalledWith('google', expect.any(Object));

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

    it('invalidates OAuth connections after disconnect', async () => {
      vi.mocked(oauthApi.disconnectOAuth).mockResolvedValue(undefined);

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

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

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

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

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

      expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['oauthConnections'] });
    });

    it('handles errors when disconnect fails', async () => {
      const mockError = new Error('Failed to disconnect');
      vi.mocked(oauthApi.disconnectOAuth).mockRejectedValue(mockError);

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

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

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

    it('can disconnect multiple providers sequentially', async () => {
      vi.mocked(oauthApi.disconnectOAuth).mockResolvedValue(undefined);

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

      // Disconnect first provider
      await act(async () => {
        await result.current.mutateAsync('google');
      });

      // React Query passes mutation context as second parameter
      expect(oauthApi.disconnectOAuth).toHaveBeenNthCalledWith(1, 'google', expect.any(Object));

      // Disconnect second provider
      await act(async () => {
        await result.current.mutateAsync('microsoft');
      });

      expect(oauthApi.disconnectOAuth).toHaveBeenNthCalledWith(2, 'microsoft', expect.any(Object));
      expect(oauthApi.disconnectOAuth).toHaveBeenCalledTimes(2);
    });
  });
});
