// src/hooks/useProfileAddress.test.ts
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mock, afterEach } from 'vitest';
import toast from 'react-hot-toast';
import { useProfileAddress } from './useProfileAddress';
import { useApi } from './useApi';
import { logger } from '../services/logger.client';
import { createMockAddress, createMockUserProfile } from '../tests/utils/mockFactories';

// Mock dependencies
vi.mock('react-hot-toast', () => ({
  default: {
    success: vi.fn(),
    error: vi.fn(),
  },
}));
vi.mock('./useApi');
vi.mock('../services/logger.client', () => ({
  logger: {
    debug: vi.fn(),
    warn: vi.fn(),
  },
}));

const mockedUseApi = vi.mocked(useApi);
const mockedToast = vi.mocked(toast);

// Mock data
const mockUserProfile = createMockUserProfile({
  address_id: 1,
  full_name: 'Test User',
});

const mockUserProfileNoAddress = createMockUserProfile({
  ...mockUserProfile,
  address_id: null,
});

const mockAddress = createMockAddress({
  address_id: 1,
  address_line_1: '123 Main St',
  city: 'Anytown',
  province_state: 'CA',
  postal_code: '12345',
  country: 'USA',
  latitude: 34.05,
  longitude: -118.25,
});

describe('useProfileAddress Hook', () => {
  let mockGeocode: Mock;
  let mockFetchAddress: Mock;

  beforeEach(() => {
    vi.clearAllMocks();

    mockGeocode = vi.fn();
    mockFetchAddress = vi.fn();

    // Robust mock implementation based on argument types.
    // This handles the two useApi calls (Geocode: string input, Fetch: number input)
    // without relying on unstable function names or render order.
    mockedUseApi.mockImplementation(() => {
      return {
        execute: vi.fn(async (arg: any) => {
          if (typeof arg === 'string') {
            return mockGeocode(arg);
          } else if (typeof arg === 'number') {
            return mockFetchAddress(arg);
          }
        }),
        loading: false,
        error: null,
        data: null,
        reset: vi.fn(),
        isRefetching: false,
      };
    });
  });

  it('should initialize with empty address and initialAddress', () => {
    const { result } = renderHook(() => useProfileAddress(null, false));
    expect(result.current.address).toEqual({});
    expect(result.current.initialAddress).toEqual({});
    expect(result.current.isGeocoding).toBe(false);
  });

  describe('Address Fetching Effect', () => {
    it('should not fetch address if isOpen is false', () => {
      renderHook(() => useProfileAddress(mockUserProfile, false));
      expect(mockFetchAddress).not.toHaveBeenCalled();
    });

    it('should not fetch address if profile is null', () => {
      renderHook(() => useProfileAddress(null, true));
      expect(mockFetchAddress).not.toHaveBeenCalled();
    });

    it('should fetch address when isOpen and a profile with address_id are provided', async () => {
      mockFetchAddress.mockResolvedValue(mockAddress);
      const { result } = renderHook(() => useProfileAddress(mockUserProfile, true));

      await waitFor(() => {
        expect(mockFetchAddress).toHaveBeenCalledWith(mockUserProfile.address_id);
        expect(result.current.address).toEqual(mockAddress);
        expect(result.current.initialAddress).toEqual(mockAddress);
      });
    });

    it('should reset address if profile has no address_id', () => {
      const { result } = renderHook(() => useProfileAddress(mockUserProfileNoAddress, true));
      expect(mockFetchAddress).not.toHaveBeenCalled();
      expect(result.current.address).toEqual({});
      expect(result.current.initialAddress).toEqual({});
    });

    it('should reset state when modal is closed', async () => {
      mockFetchAddress.mockResolvedValue(mockAddress);
      const { result, rerender } = renderHook(
        ({ userProfile, isOpen }) => useProfileAddress(userProfile, isOpen),
        { initialProps: { userProfile: mockUserProfile, isOpen: true } },
      );

      await waitFor(() => {
        expect(result.current.address).toEqual(mockAddress);
      });

      rerender({ userProfile: mockUserProfile, isOpen: false });

      expect(result.current.address).toEqual({});
      expect(result.current.initialAddress).toEqual({});
    });

    it('should handle fetch failure gracefully', async () => {
      mockFetchAddress.mockResolvedValue(null);
      const { result } = renderHook(() => useProfileAddress(mockUserProfile, true));

      await waitFor(() => {
        expect(mockFetchAddress).toHaveBeenCalledWith(mockUserProfile.address_id);
      });

      expect(result.current.address).toEqual({});
      expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Fetch returned null'));
    });
  });

  describe('handleAddressChange', () => {
    it('should update the address state field by field', () => {
      const { result } = renderHook(() => useProfileAddress(null, false));

      act(() => {
        result.current.handleAddressChange('city', 'New York');
      });
      expect(result.current.address.city).toBe('New York');

      act(() => {
        result.current.handleAddressChange('postal_code', '10001');
      });
      expect(result.current.address.city).toBe('New York');
      expect(result.current.address.postal_code).toBe('10001');
    });
  });

  describe('handleManualGeocode', () => {
    it('should call geocode API with the correct address string', async () => {
      const { result } = renderHook(() => useProfileAddress(null, false));

      act(() => {
        result.current.handleAddressChange('address_line_1', '1 Infinite Loop');
        result.current.handleAddressChange('city', 'Cupertino');
        result.current.handleAddressChange('province_state', 'CA');
      });

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

      expect(mockGeocode).toHaveBeenCalledWith(expect.stringContaining('1 Infinite Loop'));
    });

    it('should update address with new coordinates on successful geocode', async () => {
      const newCoords = { lat: 37.33, lng: -122.03 };
      mockGeocode.mockResolvedValue(newCoords);
      const { result } = renderHook(() => useProfileAddress(null, false));

      act(() => {
        result.current.handleAddressChange('city', 'Cupertino');
      });

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

      expect(result.current.address.latitude).toBe(newCoords.lat);
      expect(result.current.address.longitude).toBe(newCoords.lng);
      expect(mockedToast.success).toHaveBeenCalledWith('Address re-geocoded successfully!');
    });

    it('should show an error toast if address string is empty', async () => {
      const { result } = renderHook(() => useProfileAddress(null, false));

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

      expect(mockGeocode).not.toHaveBeenCalled();
      expect(mockedToast.error).toHaveBeenCalledWith(
        'Please fill in the address fields before geocoding.',
      );
    });
  });

  describe('Automatic Geocoding (Debounce)', () => {
    beforeEach(() => {
      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
    });

    it('should trigger geocode after user stops typing in an address without coordinates', async () => {
      const addressWithoutCoords = { ...mockAddress, latitude: undefined, longitude: undefined };
      mockFetchAddress.mockResolvedValue(addressWithoutCoords);
      const newCoords = { lat: 38.89, lng: -77.03 };
      mockGeocode.mockResolvedValue(newCoords);

      const { result } = renderHook(() => useProfileAddress(mockUserProfile, true));

      // Wait for initial fetch
      await waitFor(() => expect(result.current.address.city).toBe('Anytown'));

      // Change the address
      act(() => {
        result.current.handleAddressChange('city', 'Washington');
      });

      // Geocode should not be called immediately due to debounce
      expect(mockGeocode).not.toHaveBeenCalled();

      // Advance debounce timer
      await act(async () => {
        vi.advanceTimersByTime(1600);
      });

      await waitFor(() => {
        expect(mockGeocode).toHaveBeenCalledWith(expect.stringContaining('Washington'));
        expect(result.current.address.latitude).toBe(newCoords.lat);
        expect(result.current.address.longitude).toBe(newCoords.lng);
        expect(mockedToast.success).toHaveBeenCalledWith('Address geocoded successfully!');
      });
    });

    it('should NOT trigger geocode if address already has coordinates', async () => {
      mockFetchAddress.mockResolvedValue(mockAddress); // Has coords
      const { result } = renderHook(() => useProfileAddress(mockUserProfile, true));

      await waitFor(() => expect(result.current.address.city).toBe('Anytown'));

      act(() => {
        result.current.handleAddressChange('city', 'NewCity');
      });

      await act(async () => {
        vi.advanceTimersByTime(1600);
      });

      expect(mockGeocode).not.toHaveBeenCalled();
    });

    it('should NOT trigger geocode on initial load, even if address has no coords', async () => {
      const addressWithoutCoords = { ...mockAddress, latitude: undefined, longitude: undefined };
      mockFetchAddress.mockResolvedValue(addressWithoutCoords);
      const { result } = renderHook(() => useProfileAddress(mockUserProfile, true));

      await waitFor(() => expect(result.current.address.city).toBe('Anytown'));

      await act(async () => {
        vi.advanceTimersByTime(1600);
      });

      // Should not call because address hasn't changed from initial
      expect(mockGeocode).not.toHaveBeenCalled();
    });
  });
});
