// Mock the Google Maps client
const mockPlaceAutocomplete = jest.fn();
const mockPlaceDetails = jest.fn();
const mockGeocode = jest.fn();

jest.mock('@googlemaps/google-maps-services-js', () => ({
  Client: jest.fn().mockImplementation(() => ({
    placeAutocomplete: mockPlaceAutocomplete,
    placeDetails: mockPlaceDetails,
    geocode: mockGeocode
  }))
}));

describe('GoogleMapsService', () => {
  let service;
  let consoleSpy, consoleErrorSpy;

  beforeEach(() => {
    // Clear all mocks
    jest.clearAllMocks();

    // Set up console spies
    consoleSpy = jest.spyOn(console, 'log').mockImplementation();
    consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();

    // Reset environment
    delete process.env.GOOGLE_MAPS_API_KEY;

    // Clear module cache to get fresh instance
    jest.resetModules();
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    consoleErrorSpy.mockRestore();
  });

  describe('Constructor', () => {
    it('should initialize with API key and log success', () => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';

      service = require('../../../services/googleMapsService');

      expect(consoleSpy).toHaveBeenCalledWith('✅ Google Maps service initialized');
      expect(service.isConfigured()).toBe(true);
    });

    it('should log error when API key is not configured', () => {
      delete process.env.GOOGLE_MAPS_API_KEY;

      service = require('../../../services/googleMapsService');

      expect(consoleErrorSpy).toHaveBeenCalledWith('❌ Google Maps API key not configured in environment variables');
      expect(service.isConfigured()).toBe(false);
    });

    it('should initialize Google Maps Client', () => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
      const { Client } = require('@googlemaps/google-maps-services-js');

      service = require('../../../services/googleMapsService');

      expect(Client).toHaveBeenCalledWith({});
    });
  });

  describe('getPlacesAutocomplete', () => {
    beforeEach(() => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
      service = require('../../../services/googleMapsService');
    });

    describe('Input validation', () => {
      it('should throw error when API key is not configured', async () => {
        service.apiKey = null;

        await expect(service.getPlacesAutocomplete('test')).rejects.toThrow('Google Maps API key not configured');
      });

      it('should return empty predictions for empty input', async () => {
        const result = await service.getPlacesAutocomplete('');

        expect(result).toEqual({ predictions: [] });
        expect(mockPlaceAutocomplete).not.toHaveBeenCalled();
      });

      it('should return empty predictions for input less than 2 characters', async () => {
        const result = await service.getPlacesAutocomplete('a');

        expect(result).toEqual({ predictions: [] });
        expect(mockPlaceAutocomplete).not.toHaveBeenCalled();
      });

      it('should trim input and proceed with valid input', async () => {
        mockPlaceAutocomplete.mockResolvedValue({
          data: {
            status: 'OK',
            predictions: []
          }
        });

        await service.getPlacesAutocomplete('  test  ');

        expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
          params: expect.objectContaining({
            input: 'test'
          }),
          timeout: 5000
        });
      });
    });

    describe('Parameters handling', () => {
      beforeEach(() => {
        mockPlaceAutocomplete.mockResolvedValue({
          data: {
            status: 'OK',
            predictions: []
          }
        });
      });

      it('should use default parameters', async () => {
        await service.getPlacesAutocomplete('test input');

        expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
          params: {
            key: 'test-api-key',
            input: 'test input',
            types: 'address',
            language: 'en'
          },
          timeout: 5000
        });
      });

      it('should accept custom options', async () => {
        const options = {
          types: 'establishment',
          language: 'fr'
        };

        await service.getPlacesAutocomplete('test input', options);

        expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
          params: {
            key: 'test-api-key',
            input: 'test input',
            types: 'establishment',
            language: 'fr'
          },
          timeout: 5000
        });
      });

      it('should include session token when provided', async () => {
        const options = {
          sessionToken: 'session-123'
        };

        await service.getPlacesAutocomplete('test input', options);

        expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
          params: expect.objectContaining({
            sessiontoken: 'session-123'
          }),
          timeout: 5000
        });
      });

      it('should handle component restrictions', async () => {
        const options = {
          componentRestrictions: {
            country: 'us',
            administrative_area: 'CA'
          }
        };

        await service.getPlacesAutocomplete('test input', options);

        expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
          params: expect.objectContaining({
            components: 'country:us|administrative_area:CA'
          }),
          timeout: 5000
        });
      });

      it('should merge additional options', async () => {
        const options = {
          radius: 1000,
          location: '40.7128,-74.0060'
        };

        await service.getPlacesAutocomplete('test input', options);

        expect(mockPlaceAutocomplete).toHaveBeenCalledWith({
          params: expect.objectContaining({
            radius: 1000,
            location: '40.7128,-74.0060'
          }),
          timeout: 5000
        });
      });
    });

    describe('Successful responses', () => {
      it('should return formatted predictions on success', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            predictions: [
              {
                place_id: 'ChIJ123',
                description: 'Test Location, City, State',
                types: ['establishment'],
                structured_formatting: {
                  main_text: 'Test Location',
                  secondary_text: 'City, State'
                }
              },
              {
                place_id: 'ChIJ456',
                description: 'Another Place',
                types: ['locality'],
                structured_formatting: {
                  main_text: 'Another Place'
                }
              }
            ]
          }
        };

        mockPlaceAutocomplete.mockResolvedValue(mockResponse);

        const result = await service.getPlacesAutocomplete('test input');

        expect(result).toEqual({
          predictions: [
            {
              placeId: 'ChIJ123',
              description: 'Test Location, City, State',
              types: ['establishment'],
              mainText: 'Test Location',
              secondaryText: 'City, State'
            },
            {
              placeId: 'ChIJ456',
              description: 'Another Place',
              types: ['locality'],
              mainText: 'Another Place',
              secondaryText: ''
            }
          ]
        });
      });

      it('should handle predictions without secondary text', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            predictions: [
              {
                place_id: 'ChIJ123',
                description: 'Test Location',
                types: ['establishment'],
                structured_formatting: {
                  main_text: 'Test Location'
                }
              }
            ]
          }
        };

        mockPlaceAutocomplete.mockResolvedValue(mockResponse);

        const result = await service.getPlacesAutocomplete('test input');

        expect(result.predictions[0].secondaryText).toBe('');
      });
    });

    describe('Error responses', () => {
      it('should handle API error responses', async () => {
        const mockResponse = {
          data: {
            status: 'ZERO_RESULTS',
            error_message: 'No results found'
          }
        };

        mockPlaceAutocomplete.mockResolvedValue(mockResponse);

        const result = await service.getPlacesAutocomplete('test input');

        expect(result).toEqual({
          predictions: [],
          error: 'No results found for this query',
          status: 'ZERO_RESULTS'
        });

        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Places Autocomplete API error:',
          'ZERO_RESULTS',
          'No results found'
        );
      });

      it('should handle unknown error status', async () => {
        const mockResponse = {
          data: {
            status: 'UNKNOWN_STATUS'
          }
        };

        mockPlaceAutocomplete.mockResolvedValue(mockResponse);

        const result = await service.getPlacesAutocomplete('test input');

        expect(result.error).toBe('Google Maps API error: UNKNOWN_STATUS');
      });

      it('should handle network errors', async () => {
        mockPlaceAutocomplete.mockRejectedValue(new Error('Network error'));

        await expect(service.getPlacesAutocomplete('test input')).rejects.toThrow('Failed to fetch place predictions');

        expect(consoleErrorSpy).toHaveBeenCalledWith('Places Autocomplete service error:', 'Network error');
      });
    });
  });

  describe('getPlaceDetails', () => {
    beforeEach(() => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
      service = require('../../../services/googleMapsService');
    });

    describe('Input validation', () => {
      it('should throw error when API key is not configured', async () => {
        service.apiKey = null;

        await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('Google Maps API key not configured');
      });

      it('should throw error when placeId is not provided', async () => {
        await expect(service.getPlaceDetails()).rejects.toThrow('Place ID is required');
        await expect(service.getPlaceDetails('')).rejects.toThrow('Place ID is required');
        await expect(service.getPlaceDetails(null)).rejects.toThrow('Place ID is required');
      });
    });

    describe('Parameters handling', () => {
      beforeEach(() => {
        mockPlaceDetails.mockResolvedValue({
          data: {
            status: 'OK',
            result: {
              place_id: 'ChIJ123',
              formatted_address: 'Test Address',
              address_components: [],
              geometry: {
                location: { lat: 40.7128, lng: -74.0060 }
              }
            }
          }
        });
      });

      it('should use default parameters', async () => {
        await service.getPlaceDetails('ChIJ123');

        expect(mockPlaceDetails).toHaveBeenCalledWith({
          params: {
            key: 'test-api-key',
            place_id: 'ChIJ123',
            fields: [
              'address_components',
              'formatted_address',
              'geometry',
              'place_id'
            ],
            language: 'en'
          },
          timeout: 5000
        });
      });

      it('should accept custom language', async () => {
        await service.getPlaceDetails('ChIJ123', { language: 'fr' });

        expect(mockPlaceDetails).toHaveBeenCalledWith({
          params: expect.objectContaining({
            language: 'fr'
          }),
          timeout: 5000
        });
      });

      it('should include session token when provided', async () => {
        await service.getPlaceDetails('ChIJ123', { sessionToken: 'session-123' });

        expect(mockPlaceDetails).toHaveBeenCalledWith({
          params: expect.objectContaining({
            sessiontoken: 'session-123'
          }),
          timeout: 5000
        });
      });
    });

    describe('Successful responses', () => {
      it('should return formatted place details', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            result: {
              place_id: 'ChIJ123',
              formatted_address: '123 Test St, Test City, TC 12345, USA',
              address_components: [
                {
                  long_name: '123',
                  short_name: '123',
                  types: ['street_number']
                },
                {
                  long_name: 'Test Street',
                  short_name: 'Test St',
                  types: ['route']
                },
                {
                  long_name: 'Test City',
                  short_name: 'Test City',
                  types: ['locality', 'political']
                },
                {
                  long_name: 'Test State',
                  short_name: 'TS',
                  types: ['administrative_area_level_1', 'political']
                },
                {
                  long_name: '12345',
                  short_name: '12345',
                  types: ['postal_code']
                },
                {
                  long_name: 'United States',
                  short_name: 'US',
                  types: ['country', 'political']
                }
              ],
              geometry: {
                location: { lat: 40.7128, lng: -74.0060 }
              }
            }
          }
        };

        mockPlaceDetails.mockResolvedValue(mockResponse);

        const result = await service.getPlaceDetails('ChIJ123');

        expect(result).toEqual({
          placeId: 'ChIJ123',
          formattedAddress: '123 Test St, Test City, TC 12345, USA',
          addressComponents: {
            streetNumber: '123',
            route: 'Test Street',
            locality: 'Test City',
            administrativeAreaLevel1: 'TS',
            administrativeAreaLevel1Long: 'Test State',
            postalCode: '12345',
            country: 'US'
          },
          geometry: {
            latitude: 40.7128,
            longitude: -74.0060
          }
        });
      });

      it('should handle place details without address components', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            result: {
              place_id: 'ChIJ123',
              formatted_address: 'Test Address',
              geometry: {
                location: { lat: 40.7128, lng: -74.0060 }
              }
            }
          }
        };

        mockPlaceDetails.mockResolvedValue(mockResponse);

        const result = await service.getPlaceDetails('ChIJ123');

        expect(result.addressComponents).toEqual({});
      });

      it('should handle place details without geometry', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            result: {
              place_id: 'ChIJ123',
              formatted_address: 'Test Address'
            }
          }
        };

        mockPlaceDetails.mockResolvedValue(mockResponse);

        const result = await service.getPlaceDetails('ChIJ123');

        expect(result.geometry).toEqual({
          latitude: 0,
          longitude: 0
        });
      });

      it('should handle partial geometry data', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            result: {
              place_id: 'ChIJ123',
              formatted_address: 'Test Address',
              geometry: {}
            }
          }
        };

        mockPlaceDetails.mockResolvedValue(mockResponse);

        const result = await service.getPlaceDetails('ChIJ123');

        expect(result.geometry).toEqual({
          latitude: 0,
          longitude: 0
        });
      });
    });

    describe('Error responses', () => {
      it('should handle API error responses', async () => {
        const mockResponse = {
          data: {
            status: 'NOT_FOUND',
            error_message: 'Place not found'
          }
        };

        mockPlaceDetails.mockResolvedValue(mockResponse);

        await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('The specified place was not found');

        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Place Details API error:',
          'NOT_FOUND',
          'Place not found'
        );
      });

      it('should handle response without result', async () => {
        const mockResponse = {
          data: {
            status: 'OK'
          }
        };

        mockPlaceDetails.mockResolvedValue(mockResponse);

        await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('Google Maps API error: OK');
      });

      it('should handle network errors', async () => {
        const originalError = new Error('Network error');
        mockPlaceDetails.mockRejectedValue(originalError);

        await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow(originalError);

        expect(consoleErrorSpy).toHaveBeenCalledWith('Place Details service error:', 'Network error');
      });
    });
  });

  describe('geocodeAddress', () => {
    beforeEach(() => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
      service = require('../../../services/googleMapsService');
    });

    describe('Input validation', () => {
      it('should throw error when API key is not configured', async () => {
        service.apiKey = null;

        await expect(service.geocodeAddress('123 Test St')).rejects.toThrow('Google Maps API key not configured');
      });

      it('should throw error when address is not provided', async () => {
        await expect(service.geocodeAddress()).rejects.toThrow('Address is required for geocoding');
        await expect(service.geocodeAddress('')).rejects.toThrow('Address is required for geocoding');
        await expect(service.geocodeAddress('   ')).rejects.toThrow('Address is required for geocoding');
      });
    });

    describe('Parameters handling', () => {
      beforeEach(() => {
        mockGeocode.mockResolvedValue({
          data: {
            status: 'OK',
            results: [
              {
                formatted_address: 'Test Address',
                place_id: 'ChIJ123',
                geometry: {
                  location: { lat: 40.7128, lng: -74.0060 }
                }
              }
            ]
          }
        });
      });

      it('should use default parameters', async () => {
        await service.geocodeAddress('123 Test St');

        expect(mockGeocode).toHaveBeenCalledWith({
          params: {
            key: 'test-api-key',
            address: '123 Test St',
            language: 'en'
          },
          timeout: 5000
        });
      });

      it('should trim address input', async () => {
        await service.geocodeAddress('  123 Test St  ');

        expect(mockGeocode).toHaveBeenCalledWith({
          params: expect.objectContaining({
            address: '123 Test St'
          }),
          timeout: 5000
        });
      });

      it('should accept custom language', async () => {
        await service.geocodeAddress('123 Test St', { language: 'fr' });

        expect(mockGeocode).toHaveBeenCalledWith({
          params: expect.objectContaining({
            language: 'fr'
          }),
          timeout: 5000
        });
      });

      it('should handle component restrictions', async () => {
        const options = {
          componentRestrictions: {
            country: 'us',
            administrative_area: 'CA'
          }
        };

        await service.geocodeAddress('123 Test St', options);

        expect(mockGeocode).toHaveBeenCalledWith({
          params: expect.objectContaining({
            components: 'country:us|administrative_area:CA'
          }),
          timeout: 5000
        });
      });

      it('should handle bounds parameter', async () => {
        const options = {
          bounds: '40.7,-74.1|40.8,-73.9'
        };

        await service.geocodeAddress('123 Test St', options);

        expect(mockGeocode).toHaveBeenCalledWith({
          params: expect.objectContaining({
            bounds: '40.7,-74.1|40.8,-73.9'
          }),
          timeout: 5000
        });
      });
    });

    describe('Successful responses', () => {
      it('should return geocoded location', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            results: [
              {
                formatted_address: '123 Test St, Test City, TC 12345, USA',
                place_id: 'ChIJ123',
                geometry: {
                  location: { lat: 40.7128, lng: -74.0060 }
                }
              }
            ]
          }
        };

        mockGeocode.mockResolvedValue(mockResponse);

        const result = await service.geocodeAddress('123 Test St');

        expect(result).toEqual({
          latitude: 40.7128,
          longitude: -74.0060,
          formattedAddress: '123 Test St, Test City, TC 12345, USA',
          placeId: 'ChIJ123'
        });
      });

      it('should return first result when multiple results', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            results: [
              {
                formatted_address: 'First Result',
                place_id: 'ChIJ123',
                geometry: { location: { lat: 40.7128, lng: -74.0060 } }
              },
              {
                formatted_address: 'Second Result',
                place_id: 'ChIJ456',
                geometry: { location: { lat: 40.7129, lng: -74.0061 } }
              }
            ]
          }
        };

        mockGeocode.mockResolvedValue(mockResponse);

        const result = await service.geocodeAddress('123 Test St');

        expect(result.formattedAddress).toBe('First Result');
        expect(result.placeId).toBe('ChIJ123');
      });
    });

    describe('Error responses', () => {
      it('should handle API error responses', async () => {
        const mockResponse = {
          data: {
            status: 'ZERO_RESULTS',
            error_message: 'No results found'
          }
        };

        mockGeocode.mockResolvedValue(mockResponse);

        const result = await service.geocodeAddress('123 Test St');

        expect(result).toEqual({
          error: 'No results found for this query',
          status: 'ZERO_RESULTS'
        });

        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Geocoding API error:',
          'ZERO_RESULTS',
          'No results found'
        );
      });

      it('should handle empty results array', async () => {
        const mockResponse = {
          data: {
            status: 'OK',
            results: []
          }
        };

        mockGeocode.mockResolvedValue(mockResponse);

        const result = await service.geocodeAddress('123 Test St');

        expect(result.error).toBe('Google Maps API error: OK');
      });

      it('should handle network errors', async () => {
        mockGeocode.mockRejectedValue(new Error('Network error'));

        await expect(service.geocodeAddress('123 Test St')).rejects.toThrow('Failed to geocode address');

        expect(consoleErrorSpy).toHaveBeenCalledWith('Geocoding service error:', 'Network error');
      });
    });
  });

  describe('getErrorMessage', () => {
    beforeEach(() => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
      service = require('../../../services/googleMapsService');
    });

    it('should return correct error messages for known status codes', () => {
      expect(service.getErrorMessage('ZERO_RESULTS')).toBe('No results found for this query');
      expect(service.getErrorMessage('OVER_QUERY_LIMIT')).toBe('API quota exceeded. Please try again later');
      expect(service.getErrorMessage('REQUEST_DENIED')).toBe('API request denied. Check API key configuration');
      expect(service.getErrorMessage('INVALID_REQUEST')).toBe('Invalid request parameters');
      expect(service.getErrorMessage('UNKNOWN_ERROR')).toBe('Server error. Please try again');
      expect(service.getErrorMessage('NOT_FOUND')).toBe('The specified place was not found');
    });

    it('should return generic error message for unknown status codes', () => {
      expect(service.getErrorMessage('UNKNOWN_STATUS')).toBe('Google Maps API error: UNKNOWN_STATUS');
      expect(service.getErrorMessage('CUSTOM_ERROR')).toBe('Google Maps API error: CUSTOM_ERROR');
    });

    it('should handle null/undefined status', () => {
      expect(service.getErrorMessage(null)).toBe('Google Maps API error: null');
      expect(service.getErrorMessage(undefined)).toBe('Google Maps API error: undefined');
    });
  });

  describe('isConfigured', () => {
    it('should return true when API key is configured', () => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
      service = require('../../../services/googleMapsService');

      expect(service.isConfigured()).toBe(true);
    });

    it('should return false when API key is not configured', () => {
      delete process.env.GOOGLE_MAPS_API_KEY;
      service = require('../../../services/googleMapsService');

      expect(service.isConfigured()).toBe(false);
    });

    it('should return false when API key is empty string', () => {
      process.env.GOOGLE_MAPS_API_KEY = '';
      service = require('../../../services/googleMapsService');

      expect(service.isConfigured()).toBe(false);
    });
  });

  describe('Singleton pattern', () => {
    it('should return the same instance on multiple requires', () => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';

      const service1 = require('../../../services/googleMapsService');
      const service2 = require('../../../services/googleMapsService');

      expect(service1).toBe(service2);
    });
  });

  describe('Integration scenarios', () => {
    beforeEach(() => {
      process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
      service = require('../../../services/googleMapsService');
    });

    it('should handle typical place search workflow', async () => {
      // Mock autocomplete response
      mockPlaceAutocomplete.mockResolvedValue({
        data: {
          status: 'OK',
          predictions: [
            {
              place_id: 'ChIJ123',
              description: 'Test Location',
              types: ['establishment'],
              structured_formatting: {
                main_text: 'Test Location',
                secondary_text: 'City, State'
              }
            }
          ]
        }
      });

      // Mock place details response
      mockPlaceDetails.mockResolvedValue({
        data: {
          status: 'OK',
          result: {
            place_id: 'ChIJ123',
            formatted_address: 'Test Location, City, State',
            address_components: [],
            geometry: {
              location: { lat: 40.7128, lng: -74.0060 }
            }
          }
        }
      });

      // Step 1: Get autocomplete predictions
      const autocompleteResult = await service.getPlacesAutocomplete('test loc');
      expect(autocompleteResult.predictions).toHaveLength(1);

      // Step 2: Get detailed place information
      const placeId = autocompleteResult.predictions[0].placeId;
      const detailsResult = await service.getPlaceDetails(placeId);

      expect(detailsResult.placeId).toBe('ChIJ123');
      expect(detailsResult.geometry.latitude).toBe(40.7128);
      expect(detailsResult.geometry.longitude).toBe(-74.0060);
    });

    it('should handle geocoding workflow', async () => {
      mockGeocode.mockResolvedValue({
        data: {
          status: 'OK',
          results: [
            {
              formatted_address: '123 Test St, Test City, TC 12345, USA',
              place_id: 'ChIJ123',
              geometry: {
                location: { lat: 40.7128, lng: -74.0060 }
              }
            }
          ]
        }
      });

      const result = await service.geocodeAddress('123 Test St, Test City, TC');

      expect(result.latitude).toBe(40.7128);
      expect(result.longitude).toBe(-74.0060);
      expect(result.formattedAddress).toBe('123 Test St, Test City, TC 12345, USA');
    });
  });
});