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/auth', () => ({
  login: vi.fn(),
  logout: vi.fn(),
  getCurrentUser: vi.fn(),
  masquerade: vi.fn(),
  stopMasquerade: vi.fn(),
}));

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

vi.mock('../../utils/domain', () => ({
  getBaseDomain: vi.fn(() => 'lvh.me'),
  buildSubdomainUrl: vi.fn((subdomain, path) => `http://${subdomain}.lvh.me:5173${path || '/'}`),
}));

import {
  useAuth,
  useCurrentUser,
  useLogin,
  useLogout,
  useIsAuthenticated,
  useMasquerade,
  useStopMasquerade,
} from '../useAuth';
import * as authApi from '../../api/auth';
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('useAuth hooks', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
  });

  describe('useAuth', () => {
    it('provides setTokens function', () => {
      const { result } = renderHook(() => useAuth(), {
        wrapper: createWrapper(),
      });

      expect(result.current.setTokens).toBeDefined();
      expect(typeof result.current.setTokens).toBe('function');
    });

    it('setTokens calls setCookie for both tokens', () => {
      const { result } = renderHook(() => useAuth(), {
        wrapper: createWrapper(),
      });

      result.current.setTokens('access-123', 'refresh-456');

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

  describe('useCurrentUser', () => {
    it('returns null when no token exists', async () => {
      vi.mocked(cookies.getCookie).mockReturnValue(null);

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

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

      expect(result.current.data).toBeNull();
      expect(authApi.getCurrentUser).not.toHaveBeenCalled();
    });

    it('fetches user when token exists', async () => {
      const mockUser = {
        id: 1,
        email: 'test@example.com',
        role: 'owner',
        first_name: 'Test',
        last_name: 'User',
      };

      vi.mocked(cookies.getCookie).mockReturnValue('valid-token');
      vi.mocked(authApi.getCurrentUser).mockResolvedValue(mockUser as authApi.User);

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

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

      expect(result.current.data).toEqual(mockUser);
      expect(authApi.getCurrentUser).toHaveBeenCalled();
    });

    it('returns null when getCurrentUser fails', async () => {
      vi.mocked(cookies.getCookie).mockReturnValue('invalid-token');
      vi.mocked(authApi.getCurrentUser).mockRejectedValue(new Error('Unauthorized'));

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

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

      expect(result.current.data).toBeNull();
    });
  });

  describe('useLogin', () => {
    it('stores tokens in cookies on success', async () => {
      const mockResponse = {
        access: 'access-token',
        refresh: 'refresh-token',
        user: {
          id: 1,
          email: 'test@example.com',
          role: 'owner',
          first_name: 'Test',
          last_name: 'User',
        },
      };

      vi.mocked(authApi.login).mockResolvedValue(mockResponse as authApi.LoginResponse);

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

      await act(async () => {
        await result.current.mutateAsync({
          email: 'test@example.com',
          password: 'password123',
        });
      });

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

    it('clears masquerade stack on login', async () => {
      window.localStorage.setItem('masquerade_stack', JSON.stringify([{ user_pk: 1 }]));

      const mockResponse = {
        access: 'access-token',
        refresh: 'refresh-token',
        user: {
          id: 1,
          email: 'test@example.com',
          role: 'owner',
        },
      };

      vi.mocked(authApi.login).mockResolvedValue(mockResponse as authApi.LoginResponse);

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

      await act(async () => {
        await result.current.mutateAsync({
          email: 'test@example.com',
          password: 'password123',
        });
      });

      // After login, masquerade_stack should be removed
      expect(window.localStorage.getItem('masquerade_stack')).toBeFalsy();
    });
  });

  describe('useLogout', () => {
    it('clears tokens and masquerade stack', async () => {
      window.localStorage.setItem('masquerade_stack', JSON.stringify([{ user_pk: 1 }]));
      vi.mocked(authApi.logout).mockResolvedValue(undefined);

      // Mock window.location
      const originalLocation = window.location;
      Object.defineProperty(window, 'location', {
        value: { ...originalLocation, href: '', protocol: 'http:', port: '5173' },
        writable: true,
      });

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

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

      expect(cookies.deleteCookie).toHaveBeenCalledWith('access_token');
      expect(cookies.deleteCookie).toHaveBeenCalledWith('refresh_token');
      // After logout, masquerade_stack should be removed
      expect(window.localStorage.getItem('masquerade_stack')).toBeFalsy();

      // Restore window.location
      Object.defineProperty(window, 'location', {
        value: originalLocation,
        writable: true,
      });
    });
  });

  describe('useIsAuthenticated', () => {
    it('returns false when no user', async () => {
      vi.mocked(cookies.getCookie).mockReturnValue(null);

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

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

    it('returns true when user exists', async () => {
      const mockUser = {
        id: 1,
        email: 'test@example.com',
        role: 'owner',
      };

      vi.mocked(cookies.getCookie).mockReturnValue('valid-token');
      vi.mocked(authApi.getCurrentUser).mockResolvedValue(mockUser as authApi.User);

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

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

  describe('useMasquerade', () => {
    let originalLocation: Location;
    let originalFetch: typeof fetch;

    beforeEach(() => {
      originalLocation = window.location;
      originalFetch = global.fetch;
      // Mock window.location
      Object.defineProperty(window, 'location', {
        value: {
          ...originalLocation,
          href: 'http://platform.lvh.me:5173/',
          hostname: 'platform.lvh.me',
          protocol: 'http:',
          port: '5173',
          reload: vi.fn(),
        },
        writable: true,
      });
      // Mock fetch for logout API call
      global.fetch = vi.fn().mockResolvedValue({ ok: true });
    });

    afterEach(() => {
      Object.defineProperty(window, 'location', {
        value: originalLocation,
        writable: true,
      });
      global.fetch = originalFetch;
    });

    it('calls masquerade API with user_pk and current stack', async () => {
      const mockResponse = {
        access: 'new-access-token',
        refresh: 'new-refresh-token',
        user: {
          id: 2,
          email: 'staff@example.com',
          role: 'staff',
          business_subdomain: 'demo',
        },
        masquerade_stack: [{ user_pk: 1, access: 'original-access', refresh: 'original-refresh' }],
      };

      vi.mocked(authApi.masquerade).mockResolvedValue(mockResponse as any);

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

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

      expect(authApi.masquerade).toHaveBeenCalledWith(2, []);
    });

    it('passes existing masquerade stack to API', async () => {
      const existingStack = [{ user_pk: 1, access: 'old-access', refresh: 'old-refresh' }];
      localStorage.setItem('masquerade_stack', JSON.stringify(existingStack));

      const mockResponse = {
        access: 'new-access-token',
        refresh: 'new-refresh-token',
        user: {
          id: 3,
          email: 'staff@example.com',
          role: 'staff',
          business_subdomain: 'demo',
        },
        masquerade_stack: [...existingStack, { user_pk: 2, access: 'mid-access', refresh: 'mid-refresh' }],
      };

      vi.mocked(authApi.masquerade).mockResolvedValue(mockResponse as any);

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

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

      expect(authApi.masquerade).toHaveBeenCalledWith(3, existingStack);
    });

    it('stores masquerade stack in localStorage on success', async () => {
      const mockStack = [{ user_pk: 1, access: 'original-access', refresh: 'original-refresh' }];
      const mockResponse = {
        access: 'new-access-token',
        refresh: 'new-refresh-token',
        user: {
          id: 2,
          email: 'staff@example.com',
          role: 'staff',
          business_subdomain: 'demo',
        },
        masquerade_stack: mockStack,
      };

      vi.mocked(authApi.masquerade).mockResolvedValue(mockResponse as any);

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

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

      expect(localStorage.getItem('masquerade_stack')).toEqual(JSON.stringify(mockStack));
    });

    it('redirects to platform subdomain for platform users', async () => {
      // Set current hostname to something else to trigger redirect
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          hostname: 'demo.lvh.me', // Different from platform
          href: 'http://demo.lvh.me:5173/',
        },
        writable: true,
      });

      const mockResponse = {
        access: 'new-access-token',
        refresh: 'new-refresh-token',
        user: {
          id: 1,
          email: 'admin@example.com',
          role: 'superuser',
        },
        masquerade_stack: [],
      };

      vi.mocked(authApi.masquerade).mockResolvedValue(mockResponse as any);

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

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

      // Should have called fetch to clear session
      expect(global.fetch).toHaveBeenCalled();
    });

    it('sets cookies when no redirect is needed', async () => {
      // Set current hostname to match the target
      Object.defineProperty(window, 'location', {
        value: {
          hostname: 'demo.lvh.me',
          href: 'http://demo.lvh.me:5173/',
          protocol: 'http:',
          port: '5173',
          reload: vi.fn(),
        },
        writable: true,
      });

      const mockResponse = {
        access: 'new-access-token',
        refresh: 'new-refresh-token',
        user: {
          id: 2,
          email: 'staff@example.com',
          role: 'staff',
          business_subdomain: 'demo',
        },
        masquerade_stack: [],
      };

      vi.mocked(authApi.masquerade).mockResolvedValue(mockResponse as any);

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

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

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

  describe('useStopMasquerade', () => {
    let originalLocation: Location;
    let originalFetch: typeof fetch;

    beforeEach(() => {
      originalLocation = window.location;
      originalFetch = global.fetch;
      Object.defineProperty(window, 'location', {
        value: {
          ...originalLocation,
          href: 'http://demo.lvh.me:5173/',
          hostname: 'demo.lvh.me',
          protocol: 'http:',
          port: '5173',
          reload: vi.fn(),
        },
        writable: true,
      });
      global.fetch = vi.fn().mockResolvedValue({ ok: true });
    });

    afterEach(() => {
      Object.defineProperty(window, 'location', {
        value: originalLocation,
        writable: true,
      });
      global.fetch = originalFetch;
    });

    it('throws error when no masquerade stack exists', async () => {
      localStorage.removeItem('masquerade_stack');

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

      let error: Error | undefined;
      await act(async () => {
        try {
          await result.current.mutateAsync();
        } catch (e) {
          error = e as Error;
        }
      });

      expect(error?.message).toBe('No masquerading session to stop');
    });

    it('calls stopMasquerade API with current stack', async () => {
      const existingStack = [{ user_pk: 1, access: 'original-access', refresh: 'original-refresh' }];
      localStorage.setItem('masquerade_stack', JSON.stringify(existingStack));

      const mockResponse = {
        access: 'restored-access-token',
        refresh: 'restored-refresh-token',
        user: {
          id: 1,
          email: 'admin@example.com',
          role: 'superuser',
        },
        masquerade_stack: [],
      };

      vi.mocked(authApi.stopMasquerade).mockResolvedValue(mockResponse as any);

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

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

      expect(authApi.stopMasquerade).toHaveBeenCalledWith(existingStack);
    });

    it('clears masquerade stack when returning to original user', async () => {
      const existingStack = [{ user_pk: 1, access: 'original-access', refresh: 'original-refresh' }];
      localStorage.setItem('masquerade_stack', JSON.stringify(existingStack));

      const mockResponse = {
        access: 'restored-access-token',
        refresh: 'restored-refresh-token',
        user: {
          id: 1,
          email: 'admin@example.com',
          role: 'superuser',
        },
        masquerade_stack: [], // Empty stack means back to original
      };

      vi.mocked(authApi.stopMasquerade).mockResolvedValue(mockResponse as any);

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

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

      expect(localStorage.getItem('masquerade_stack')).toBeNull();
    });

    it('keeps stack when still masquerading after stop', async () => {
      const deepStack = [
        { user_pk: 1, access: 'level1-access', refresh: 'level1-refresh' },
        { user_pk: 2, access: 'level2-access', refresh: 'level2-refresh' },
      ];
      localStorage.setItem('masquerade_stack', JSON.stringify(deepStack));

      const remainingStack = [{ user_pk: 1, access: 'level1-access', refresh: 'level1-refresh' }];
      const mockResponse = {
        access: 'level2-access-token',
        refresh: 'level2-refresh-token',
        user: {
          id: 2,
          email: 'manager@example.com',
          role: 'manager',
          business_subdomain: 'demo',
        },
        masquerade_stack: remainingStack,
      };

      vi.mocked(authApi.stopMasquerade).mockResolvedValue(mockResponse as any);

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

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

      expect(localStorage.getItem('masquerade_stack')).toEqual(JSON.stringify(remainingStack));
    });

    it('sets cookies when no redirect is needed', async () => {
      const existingStack = [{ user_pk: 1, access: 'original-access', refresh: 'original-refresh' }];
      localStorage.setItem('masquerade_stack', JSON.stringify(existingStack));

      // Set hostname to match target subdomain
      Object.defineProperty(window, 'location', {
        value: {
          hostname: 'demo.lvh.me',
          href: 'http://demo.lvh.me:5173/',
          protocol: 'http:',
          port: '5173',
          reload: vi.fn(),
        },
        writable: true,
      });

      const mockResponse = {
        access: 'restored-access-token',
        refresh: 'restored-refresh-token',
        user: {
          id: 2,
          email: 'owner@example.com',
          role: 'owner',
          business_subdomain: 'demo',
        },
        masquerade_stack: [],
      };

      vi.mocked(authApi.stopMasquerade).mockResolvedValue(mockResponse as any);

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

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

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