/**
 * @ai-summary Unit tests for community stations service
 * @ai-context Tests business logic without database dependencies
 */

import { CommunityStationsService } from '../../domain/community-stations.service';
import { CommunityStationsRepository } from '../../data/community-stations.repository';
import { SubmitCommunityStationBody, CommunityStation } from '../../domain/community-stations.types';
import { redis } from '../../../../core/config/redis';

// Mock repository and redis
jest.mock('../../data/community-stations.repository');
jest.mock('../../../../core/config/redis');

describe('CommunityStationsService', () => {
  let service: CommunityStationsService;
  let mockRepository: jest.Mocked<CommunityStationsRepository>;
  let mockRedis: jest.Mocked<typeof redis>;

  const testUserId = 'test-user-123';
  const testAdminId = 'admin-123';
  const testStationId = 'station-123';

  const mockStationData: SubmitCommunityStationBody = {
    name: 'Test Gas Station',
    address: '123 Main St',
    city: 'Springfield',
    state: 'IL',
    zipCode: '62701',
    latitude: 39.7817,
    longitude: -89.6501,
    brand: 'Shell',
    has93Octane: true,
    has93OctaneEthanolFree: false,
    price93: 3.50,
    notes: 'Great service'
  };

  const mockStation: CommunityStation = {
    id: testStationId,
    submittedBy: testUserId,
    ...mockStationData,
    status: 'pending',
    createdAt: new Date(),
    updatedAt: new Date()
  };

  beforeEach(() => {
    jest.clearAllMocks();
    mockRepository = CommunityStationsRepository as jest.Mocked<typeof CommunityStationsRepository>;
    mockRedis = redis as jest.Mocked<typeof redis>;

    // Setup default mock implementations
    (mockRedis.get as jest.Mock).mockResolvedValue(null);
    (mockRedis.setex as jest.Mock).mockResolvedValue(true);
    (mockRedis.del as jest.Mock).mockResolvedValue(1);
    (mockRedis.keys as jest.Mock).mockResolvedValue([]);

    service = new CommunityStationsService(mockRepository as any);
  });

  describe('submitStation', () => {
    it('should successfully submit a new station', async () => {
      (mockRepository.prototype.submitStation as jest.Mock).mockResolvedValue(mockStation);

      const result = await service.submitStation(testUserId, mockStationData);

      expect(result).toEqual(mockStation);
      expect(mockRepository.prototype.submitStation).toHaveBeenCalledWith(testUserId, mockStationData);
      expect(mockRedis.keys).toHaveBeenCalledWith('mvp:community-stations:pending:*');
    });

    it('should invalidate pending cache after submission', async () => {
      (mockRepository.prototype.submitStation as jest.Mock).mockResolvedValue(mockStation);
      (mockRedis.keys as jest.Mock).mockResolvedValue(['pending-key-1']);

      await service.submitStation(testUserId, mockStationData);

      expect(mockRedis.del).toHaveBeenCalledWith('pending-key-1');
    });
  });

  describe('getMySubmissions', () => {
    it('should retrieve user submissions', async () => {
      const mockResult = { total: 1, stations: [mockStation] };
      (mockRepository.prototype.getUserSubmissions as jest.Mock).mockResolvedValue(mockResult);

      const result = await service.getMySubmissions(testUserId, 100, 0);

      expect(result).toEqual(mockResult);
      expect(mockRepository.prototype.getUserSubmissions).toHaveBeenCalledWith(testUserId, 100, 0);
    });

    it('should use default pagination values', async () => {
      const mockResult = { total: 1, stations: [mockStation] };
      (mockRepository.prototype.getUserSubmissions as jest.Mock).mockResolvedValue(mockResult);

      await service.getMySubmissions(testUserId);

      expect(mockRepository.prototype.getUserSubmissions).toHaveBeenCalledWith(testUserId, 100, 0);
    });
  });

  describe('withdrawSubmission', () => {
    it('should allow user to withdraw own pending submission', async () => {
      (mockRepository.prototype.getStationById as jest.Mock).mockResolvedValue(mockStation);
      (mockRepository.prototype.deleteStation as jest.Mock).mockResolvedValue(true);

      await service.withdrawSubmission(testUserId, testStationId);

      expect(mockRepository.prototype.deleteStation).toHaveBeenCalledWith(testStationId);
      expect(mockRedis.keys).toHaveBeenCalledWith('mvp:community-stations:pending:*');
    });

    it('should prevent withdrawal of non-existent station', async () => {
      (mockRepository.prototype.getStationById as jest.Mock).mockResolvedValue(null);

      await expect(service.withdrawSubmission(testUserId, testStationId))
        .rejects.toThrow('Station not found');
    });

    it('should prevent withdrawal by non-owner', async () => {
      (mockRepository.prototype.getStationById as jest.Mock).mockResolvedValue(mockStation);

      await expect(service.withdrawSubmission('other-user', testStationId))
        .rejects.toThrow('Unauthorized: You can only withdraw your own submissions');
    });

    it('should prevent withdrawal of non-pending submission', async () => {
      const approvedStation = { ...mockStation, status: 'approved' as const };
      (mockRepository.prototype.getStationById as jest.Mock).mockResolvedValue(approvedStation);

      await expect(service.withdrawSubmission(testUserId, testStationId))
        .rejects.toThrow('Can only withdraw pending submissions');
    });
  });

  describe('getApprovedStations', () => {
    it('should retrieve approved stations from cache if available', async () => {
      const cachedResult = { total: 1, stations: [{ ...mockStation, status: 'approved' as const }] };
      (mockRedis.get as jest.Mock).mockResolvedValue(JSON.stringify(cachedResult));

      const result = await service.getApprovedStations(100, 0);

      expect(result).toEqual(cachedResult);
      expect(mockRepository.prototype.getApprovedStations).not.toHaveBeenCalled();
    });

    it('should fetch from repository and cache if not in cache', async () => {
      const mockResult = { total: 1, stations: [{ ...mockStation, status: 'approved' as const }] };
      (mockRedis.get as jest.Mock).mockResolvedValue(null);
      (mockRepository.prototype.getApprovedStations as jest.Mock).mockResolvedValue(mockResult);

      const result = await service.getApprovedStations(100, 0);

      expect(result).toEqual(mockResult);
      expect(mockRepository.prototype.getApprovedStations).toHaveBeenCalledWith(100, 0);
      expect(mockRedis.setex).toHaveBeenCalled();
    });
  });

  describe('getApprovedNearby', () => {
    it('should find nearby approved stations', async () => {
      const nearbyStations = [{ ...mockStation, status: 'approved' as const }];
      (mockRedis.get as jest.Mock).mockResolvedValue(null);
      (mockRepository.prototype.getNearbyApprovedStations as jest.Mock).mockResolvedValue(nearbyStations);

      const result = await service.getApprovedNearby({
        latitude: 39.7817,
        longitude: -89.6501,
        radiusKm: 50
      });

      expect(result).toEqual(nearbyStations);
      expect(mockRepository.prototype.getNearbyApprovedStations).toHaveBeenCalledWith(39.7817, -89.6501, 50);
    });

    it('should use default radius if not provided', async () => {
      (mockRedis.get as jest.Mock).mockResolvedValue(null);
      (mockRepository.prototype.getNearbyApprovedStations as jest.Mock).mockResolvedValue([]);

      await service.getApprovedNearby({
        latitude: 39.7817,
        longitude: -89.6501
      });

      expect(mockRepository.prototype.getNearbyApprovedStations).toHaveBeenCalledWith(39.7817, -89.6501, 50);
    });
  });

  describe('getPendingReview', () => {
    it('should retrieve pending submissions for review', async () => {
      const mockResult = { total: 1, stations: [mockStation] };
      (mockRepository.prototype.getPendingStations as jest.Mock).mockResolvedValue(mockResult);

      const result = await service.getPendingReview(100, 0);

      expect(result).toEqual(mockResult);
      expect(mockRepository.prototype.getPendingStations).toHaveBeenCalledWith(100, 0);
    });
  });

  describe('reviewStation', () => {
    it('should approve a pending station', async () => {
      const approvedStation = { ...mockStation, status: 'approved' as const, reviewedBy: testAdminId, reviewedAt: new Date() };
      (mockRepository.prototype.reviewStation as jest.Mock).mockResolvedValue(approvedStation);
      (mockRedis.keys as jest.Mock).mockResolvedValue([]);

      const result = await service.reviewStation(testAdminId, testStationId, 'approved');

      expect(result).toEqual(approvedStation);
      expect(mockRepository.prototype.reviewStation).toHaveBeenCalledWith(
        testStationId,
        testAdminId,
        'approved',
        undefined
      );
    });

    it('should reject a station with reason', async () => {
      const rejectionReason = 'Invalid location';
      const rejectedStation = { ...mockStation, status: 'rejected' as const, rejectionReason };
      (mockRepository.prototype.reviewStation as jest.Mock).mockResolvedValue(rejectedStation);
      (mockRedis.keys as jest.Mock).mockResolvedValue([]);

      const result = await service.reviewStation(testAdminId, testStationId, 'rejected', rejectionReason);

      expect(result).toEqual(rejectedStation);
      expect(mockRepository.prototype.reviewStation).toHaveBeenCalledWith(
        testStationId,
        testAdminId,
        'rejected',
        rejectionReason
      );
    });

    it('should require rejection reason when rejecting', async () => {
      await expect(service.reviewStation(testAdminId, testStationId, 'rejected'))
        .rejects.toThrow('Rejection reason required when rejecting a station');
    });

    it('should invalidate appropriate caches on approval', async () => {
      const approvedStation = { ...mockStation, status: 'approved' as const };
      (mockRepository.prototype.reviewStation as jest.Mock).mockResolvedValue(approvedStation);
      (mockRedis.keys as jest.Mock).mockResolvedValue(['key1', 'key2']);

      await service.reviewStation(testAdminId, testStationId, 'approved');

      expect(mockRedis.del).toHaveBeenCalledWith('key1', 'key2');
    });
  });

  describe('cache invalidation', () => {
    it('should handle cache invalidation errors gracefully', async () => {
      (mockRepository.prototype.submitStation as jest.Mock).mockResolvedValue(mockStation);
      (mockRedis.keys as jest.Mock).mockRejectedValue(new Error('Redis error'));

      // Should not throw
      const result = await service.submitStation(testUserId, mockStationData);
      expect(result).toEqual(mockStation);
    });
  });
});
