const request = require('supertest');
const express = require('express');

// Mock logger
const mockLogger = {
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
  debug: jest.fn(),
  http: jest.fn(),
  withRequestId: jest.fn()
};

// Set up withRequestId to return the same mock logger
mockLogger.withRequestId.mockReturnValue(mockLogger);

jest.mock('../../../utils/logger', () => mockLogger);

// Mock dependencies
jest.mock('../../../services/googleMapsService', () => ({
  getPlacesAutocomplete: jest.fn(),
  getPlaceDetails: jest.fn(),
  geocodeAddress: jest.fn(),
  isConfigured: jest.fn()
}));

// Mock auth middleware
jest.mock('../../../middleware/auth', () => ({
  authenticateToken: (req, res, next) => {
    if (req.headers.authorization) {
      req.user = { id: 1 };
      next();
    } else {
      res.status(401).json({ error: 'No token provided' });
    }
  }
}));

// Mock rate limiter middleware
jest.mock('../../../middleware/rateLimiter', () => ({
  burstProtection: (req, res, next) => next(),
  placesAutocomplete: (req, res, next) => next(),
  placeDetails: (req, res, next) => next(),
  geocoding: (req, res, next) => next()
}));

const googleMapsService = require('../../../services/googleMapsService');
const mapsRoutes = require('../../../routes/maps');

// Set up Express app for testing
const app = express();
app.use(express.json());
app.use('/maps', mapsRoutes);

describe('Maps Routes', () => {
  let consoleSpy, consoleErrorSpy, consoleLogSpy;

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

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

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

  describe('Input Validation Middleware', () => {
    it('should trim and validate input length', async () => {
      const longInput = 'a'.repeat(501);

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: longInput });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Input too long' });
    });

    it('should validate place ID format', async () => {
      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({ placeId: 'invalid@place#id!' });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Invalid place ID format' });
    });

    it('should validate address length', async () => {
      const longAddress = 'a'.repeat(501);

      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({ address: longAddress });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Address too long' });
    });

    it('should allow valid place ID format', async () => {
      googleMapsService.getPlaceDetails.mockResolvedValue({
        result: { name: 'Test Place' }
      });

      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({ placeId: 'ChIJ123abc_DEF' });

      expect(response.status).toBe(200);
    });

    it('should trim whitespace from inputs', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue({
        predictions: []
      });

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: '  test input  ' });

      expect(response.status).toBe(200);
      expect(googleMapsService.getPlacesAutocomplete).toHaveBeenCalledWith(
        'test input',
        expect.any(Object)
      );
    });
  });

  describe('Error Handling Middleware', () => {
    it('should handle API key configuration errors', async () => {
      const configError = new Error('API key not configured');
      googleMapsService.getPlacesAutocomplete.mockRejectedValue(configError);

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'test' });

      expect(response.status).toBe(503);
      expect(response.body).toEqual({
        error: 'Maps service temporarily unavailable',
        details: 'Configuration issue'
      });
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Maps service error',
        expect.objectContaining({
          error: 'API key not configured'
        })
      );
    });

    it('should handle quota exceeded errors', async () => {
      const quotaError = new Error('quota exceeded');
      googleMapsService.getPlacesAutocomplete.mockRejectedValue(quotaError);

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'test' });

      expect(response.status).toBe(429);
      expect(response.body).toEqual({
        error: 'Service temporarily unavailable due to high demand',
        details: 'Please try again later'
      });
    });

    it('should handle generic service errors', async () => {
      const serviceError = new Error('Network timeout');
      googleMapsService.getPlacesAutocomplete.mockRejectedValue(serviceError);

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'test' });

      expect(response.status).toBe(500);
      expect(response.body).toEqual({
        error: 'Failed to process request',
        details: 'Network timeout'
      });
    });
  });

  describe('POST /places/autocomplete', () => {
    const mockPredictions = {
      predictions: [
        {
          description: '123 Main St, New York, NY, USA',
          place_id: 'ChIJ123abc',
          types: ['street_address']
        },
        {
          description: '456 Oak Ave, New York, NY, USA',
          place_id: 'ChIJ456def',
          types: ['street_address']
        }
      ]
    };

    it('should return autocomplete predictions successfully', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue(mockPredictions);

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({
          input: '123 Main',
          types: ['address'],
          componentRestrictions: { country: 'us' },
          sessionToken: 'session123'
        });

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockPredictions);

      expect(googleMapsService.getPlacesAutocomplete).toHaveBeenCalledWith(
        '123 Main',
        {
          types: ['address'],
          componentRestrictions: { country: 'us' },
          sessionToken: 'session123'
        }
      );

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Places Autocomplete request',
        expect.objectContaining({
          userId: 1,
          queryLength: 8,
          resultsCount: 2
        })
      );
    });

    it('should use default types when not provided', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue(mockPredictions);

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'test' });

      expect(response.status).toBe(200);
      expect(googleMapsService.getPlacesAutocomplete).toHaveBeenCalledWith(
        'test',
        {
          types: ['address'],
          componentRestrictions: undefined,
          sessionToken: undefined
        }
      );
    });

    it('should return empty predictions for short input', async () => {
      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'a' });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ predictions: [] });
      expect(googleMapsService.getPlacesAutocomplete).not.toHaveBeenCalled();
    });

    it('should return empty predictions for missing input', async () => {
      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({});

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ predictions: [] });
      expect(googleMapsService.getPlacesAutocomplete).not.toHaveBeenCalled();
    });

    it('should require authentication', async () => {
      const response = await request(app)
        .post('/maps/places/autocomplete')
        .send({ input: 'test' });

      expect(response.status).toBe(401);
      expect(response.body).toEqual({ error: 'No token provided' });
    });

    it('should log request with user ID from authenticated user', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue(mockPredictions);

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'test' });

      expect(response.status).toBe(200);
      // Should log with user ID
      expect(mockLogger.info).toHaveBeenCalledWith(
        'Places Autocomplete request',
        expect.objectContaining({
          userId: 1
        })
      );
    });

    it('should handle empty predictions from service', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue({ predictions: [] });

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'nonexistent place' });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ predictions: [] });

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Places Autocomplete request',
        expect.objectContaining({
          userId: 1,
          queryLength: 17,
          resultsCount: 0
        })
      );
    });

    it('should handle service response without predictions array', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue({});

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'test' });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({});

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Places Autocomplete request',
        expect.objectContaining({
          userId: 1,
          queryLength: 4,
          resultsCount: 0
        })
      );
    });
  });

  describe('POST /places/details', () => {
    const mockPlaceDetails = {
      result: {
        place_id: 'ChIJ123abc',
        name: 'Central Park',
        formatted_address: 'New York, NY 10024, USA',
        geometry: {
          location: { lat: 40.785091, lng: -73.968285 }
        },
        types: ['park', 'point_of_interest']
      }
    };

    it('should return place details successfully', async () => {
      googleMapsService.getPlaceDetails.mockResolvedValue(mockPlaceDetails);

      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({
          placeId: 'ChIJ123abc',
          sessionToken: 'session123'
        });

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockPlaceDetails);

      expect(googleMapsService.getPlaceDetails).toHaveBeenCalledWith(
        'ChIJ123abc',
        { sessionToken: 'session123' }
      );

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Place Details request',
        expect.objectContaining({
          userId: 1,
          placeIdPrefix: 'ChIJ123abc...'
        })
      );
    });

    it('should handle place details without session token', async () => {
      googleMapsService.getPlaceDetails.mockResolvedValue(mockPlaceDetails);

      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({ placeId: 'ChIJ123abc' });

      expect(response.status).toBe(200);
      expect(googleMapsService.getPlaceDetails).toHaveBeenCalledWith(
        'ChIJ123abc',
        { sessionToken: undefined }
      );
    });

    it('should return error for missing place ID', async () => {
      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({});

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Place ID is required' });
      expect(googleMapsService.getPlaceDetails).not.toHaveBeenCalled();
    });

    it('should return error for empty place ID', async () => {
      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({ placeId: '' });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Place ID is required' });
    });

    it('should require authentication', async () => {
      const response = await request(app)
        .post('/maps/places/details')
        .send({ placeId: 'ChIJ123abc' });

      expect(response.status).toBe(401);
    });

    it('should handle very long place IDs in logging', async () => {
      const longPlaceId = 'ChIJ' + 'a'.repeat(100);
      googleMapsService.getPlaceDetails.mockResolvedValue(mockPlaceDetails);

      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({ placeId: longPlaceId });

      expect(response.status).toBe(200);
      expect(mockLogger.info).toHaveBeenCalledWith(
        'Place Details request',
        expect.objectContaining({
          userId: 1,
          placeIdPrefix: longPlaceId.substring(0, 10) + '...'
        })
      );
    });

    it('should handle service errors', async () => {
      const serviceError = new Error('Place not found');
      googleMapsService.getPlaceDetails.mockRejectedValue(serviceError);

      const response = await request(app)
        .post('/maps/places/details')
        .set('Authorization', 'Bearer valid_token')
        .send({ placeId: 'ChIJ123abc' });

      expect(response.status).toBe(500);
      expect(response.body).toEqual({
        error: 'Failed to process request',
        details: 'Place not found'
      });
    });
  });

  describe('POST /geocode', () => {
    const mockGeocodeResults = {
      results: [
        {
          formatted_address: '123 Main St, New York, NY 10001, USA',
          geometry: {
            location: { lat: 40.7484405, lng: -73.9856644 }
          },
          place_id: 'ChIJ123abc',
          types: ['street_address']
        }
      ]
    };

    it('should return geocoding results successfully', async () => {
      googleMapsService.geocodeAddress.mockResolvedValue(mockGeocodeResults);

      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({
          address: '123 Main St, New York, NY',
          componentRestrictions: { country: 'US' }
        });

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockGeocodeResults);

      expect(googleMapsService.geocodeAddress).toHaveBeenCalledWith(
        '123 Main St, New York, NY',
        { componentRestrictions: { country: 'US' } }
      );

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Geocoding request',
        expect.objectContaining({
          userId: 1,
          addressLength: 25
        })
      );
    });

    it('should handle geocoding without component restrictions', async () => {
      googleMapsService.geocodeAddress.mockResolvedValue(mockGeocodeResults);

      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({ address: '123 Main St' });

      expect(response.status).toBe(200);
      expect(googleMapsService.geocodeAddress).toHaveBeenCalledWith(
        '123 Main St',
        { componentRestrictions: undefined }
      );
    });

    it('should return error for missing address', async () => {
      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({});

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Address is required' });
      expect(googleMapsService.geocodeAddress).not.toHaveBeenCalled();
    });

    it('should return error for empty address', async () => {
      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({ address: '' });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Address is required' });
    });

    it('should require authentication', async () => {
      const response = await request(app)
        .post('/maps/geocode')
        .send({ address: '123 Main St' });

      expect(response.status).toBe(401);
    });

    it('should handle addresses with special characters', async () => {
      googleMapsService.geocodeAddress.mockResolvedValue(mockGeocodeResults);

      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({ address: '123 Main St, Apt #4B' });

      expect(response.status).toBe(200);
      expect(googleMapsService.geocodeAddress).toHaveBeenCalledWith(
        '123 Main St, Apt #4B',
        { componentRestrictions: undefined }
      );
    });

    it('should handle service errors', async () => {
      const serviceError = new Error('Invalid address');
      googleMapsService.geocodeAddress.mockRejectedValue(serviceError);

      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({ address: 'invalid address' });

      expect(response.status).toBe(500);
      expect(response.body).toEqual({
        error: 'Failed to process request',
        details: 'Invalid address'
      });
    });

    it('should handle empty geocoding results', async () => {
      googleMapsService.geocodeAddress.mockResolvedValue({ results: [] });

      const response = await request(app)
        .post('/maps/geocode')
        .set('Authorization', 'Bearer valid_token')
        .send({ address: 'nonexistent address' });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ results: [] });
    });
  });

  describe('GET /health', () => {
    it('should return healthy status when service is configured', async () => {
      googleMapsService.isConfigured.mockReturnValue(true);

      const response = await request(app)
        .get('/maps/health');

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        status: 'healthy',
        service: 'Google Maps API Proxy',
        timestamp: expect.any(String),
        configuration: {
          apiKeyConfigured: true
        }
      });

      // Verify timestamp is a valid ISO string
      expect(new Date(response.body.timestamp).toISOString()).toBe(response.body.timestamp);
    });

    it('should return unavailable status when service is not configured', async () => {
      googleMapsService.isConfigured.mockReturnValue(false);

      const response = await request(app)
        .get('/maps/health');

      expect(response.status).toBe(503);
      expect(response.body).toEqual({
        status: 'unavailable',
        service: 'Google Maps API Proxy',
        timestamp: expect.any(String),
        configuration: {
          apiKeyConfigured: false
        }
      });
    });

    it('should not require authentication', async () => {
      googleMapsService.isConfigured.mockReturnValue(true);

      const response = await request(app)
        .get('/maps/health');

      expect(response.status).toBe(200);
      // Should work without authorization header
    });

    it('should always return current timestamp', async () => {
      googleMapsService.isConfigured.mockReturnValue(true);

      const beforeTime = new Date().toISOString();
      const response = await request(app)
        .get('/maps/health');
      const afterTime = new Date().toISOString();

      expect(response.status).toBe(200);
      expect(new Date(response.body.timestamp).getTime()).toBeGreaterThanOrEqual(new Date(beforeTime).getTime());
      expect(new Date(response.body.timestamp).getTime()).toBeLessThanOrEqual(new Date(afterTime).getTime());
    });
  });

  describe('Rate Limiting Integration', () => {
    it('should apply burst protection to all endpoints', async () => {
      // This test verifies that rate limiting middleware is applied
      // In a real scenario, we'd test actual rate limiting behavior
      googleMapsService.getPlacesAutocomplete.mockResolvedValue({ predictions: [] });

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: 'test' });

      expect(response.status).toBe(200);
      // The fact that the request succeeded means rate limiting middleware was applied without blocking
    });
  });

  describe('Edge Cases and Security', () => {
    it('should handle null input gracefully', async () => {
      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: null });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ predictions: [] });
    });

    it('should handle undefined values in request body', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue({ predictions: [] });

      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({
          input: 'test',
          types: undefined,
          componentRestrictions: undefined,
          sessionToken: undefined
        });

      expect(response.status).toBe(200);
      expect(googleMapsService.getPlacesAutocomplete).toHaveBeenCalledWith(
        'test',
        {
          types: ['address'], // Should use default
          componentRestrictions: undefined,
          sessionToken: undefined
        }
      );
    });

    it('should handle malformed JSON gracefully', async () => {
      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .set('Content-Type', 'application/json')
        .send('invalid json');

      expect(response.status).toBe(400); // Express will handle malformed JSON
    });

    it('should sanitize input to prevent injection attacks', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue({ predictions: [] });

      const maliciousInput = '<script>alert("xss")</script>';
      const response = await request(app)
        .post('/maps/places/autocomplete')
        .set('Authorization', 'Bearer valid_token')
        .send({ input: maliciousInput });

      expect(response.status).toBe(200);
      // Input should be treated as string and passed through
      expect(googleMapsService.getPlacesAutocomplete).toHaveBeenCalledWith(
        maliciousInput,
        expect.any(Object)
      );
    });

    it('should handle concurrent requests to different endpoints', async () => {
      googleMapsService.getPlacesAutocomplete.mockResolvedValue({ predictions: [] });
      googleMapsService.getPlaceDetails.mockResolvedValue({ result: {} });
      googleMapsService.geocodeAddress.mockResolvedValue({ results: [] });

      const [response1, response2, response3] = await Promise.all([
        request(app)
          .post('/maps/places/autocomplete')
          .set('Authorization', 'Bearer valid_token')
          .send({ input: 'test1' }),
        request(app)
          .post('/maps/places/details')
          .set('Authorization', 'Bearer valid_token')
          .send({ placeId: 'ChIJ123abc' }),
        request(app)
          .post('/maps/geocode')
          .set('Authorization', 'Bearer valid_token')
          .send({ address: 'test address' })
      ]);

      expect(response1.status).toBe(200);
      expect(response2.status).toBe(200);
      expect(response3.status).toBe(200);
    });
  });
});