/**
 * @fileoverview Unit tests for LanesService
 * Tests cover CRUD operations, findOrCreate, createReverse, validation, and error handling
 */

import { Repository, SelectQueryBuilder, DeleteResult } from 'typeorm';
import { Lane } from '../entities';

// Mock data
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
const mockLaneId = '550e8400-e29b-41d4-a716-446655440010';

const createMockLane = (overrides?: Partial<Lane>): Partial<Lane> => ({
  id: mockLaneId,
  tenantId: mockTenantId,
  codigo: 'MEX-GDL',
  nombre: 'Mexico - Guadalajara',
  origenCiudad: 'Mexico',
  origenEstado: 'CDMX',
  origenCodigoPostal: '06600',
  destinoCiudad: 'Guadalajara',
  destinoEstado: 'Jalisco',
  destinoCodigoPostal: '44100',
  distanciaKm: 540,
  tiempoEstimadoHoras: 6,
  activo: true,
  createdAt: new Date('2026-01-01'),
  createdById: mockUserId,
  ...overrides,
});

describe('LanesService', () => {
  let mockLaneRepository: Partial<Repository<Lane>>;
  let mockQueryBuilder: Partial<SelectQueryBuilder<Lane>>;
  let lanesService: any;

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

    // Setup mock query builder
    mockQueryBuilder = {
      select: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      andWhere: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      getRawMany: jest.fn().mockResolvedValue([
        { ciudad: 'Mexico' },
        { ciudad: 'Guadalajara' },
      ]),
    };

    // Setup mock lane repository
    mockLaneRepository = {
      create: jest.fn().mockImplementation((data) => ({ ...createMockLane(), ...data })),
      save: jest.fn().mockImplementation((lane) => Promise.resolve(lane)),
      findOne: jest.fn().mockResolvedValue(createMockLane()),
      find: jest.fn().mockResolvedValue([createMockLane()]),
      findAndCount: jest.fn().mockResolvedValue([[createMockLane()], 1]),
      delete: jest.fn().mockResolvedValue({ affected: 1 } as DeleteResult),
      createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
    };

    // Import the service class and instantiate
    const { LanesService } = await import('../services/lanes.service');
    lanesService = new LanesService(mockLaneRepository as Repository<Lane>);
  });

  describe('findAll', () => {
    it('should find all lanes with default params', async () => {
      const result = await lanesService.findAll({ tenantId: mockTenantId });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalled();
      expect(result.data).toBeDefined();
      expect(result.total).toBe(1);
    });

    it('should filter by origenCiudad', async () => {
      await lanesService.findAll({ tenantId: mockTenantId, origenCiudad: 'Mexico' });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalled();
    });

    it('should filter by origenEstado', async () => {
      await lanesService.findAll({ tenantId: mockTenantId, origenEstado: 'CDMX' });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalled();
    });

    it('should filter by destinoCiudad', async () => {
      await lanesService.findAll({ tenantId: mockTenantId, destinoCiudad: 'Guadalajara' });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalled();
    });

    it('should filter by destinoEstado', async () => {
      await lanesService.findAll({ tenantId: mockTenantId, destinoEstado: 'Jalisco' });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalled();
    });

    it('should filter by activo', async () => {
      await lanesService.findAll({ tenantId: mockTenantId, activo: true });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalled();
    });

    it('should apply search filter across multiple fields', async () => {
      await lanesService.findAll({ tenantId: mockTenantId, search: 'MEX' });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalled();
    });

    it('should apply pagination', async () => {
      await lanesService.findAll({ tenantId: mockTenantId, limit: 10, offset: 20 });

      expect(mockLaneRepository.findAndCount).toHaveBeenCalledWith(
        expect.objectContaining({
          take: 10,
          skip: 20,
        })
      );
    });
  });

  describe('findOne', () => {
    it('should find a lane by id', async () => {
      const result = await lanesService.findOne(mockLaneId, mockTenantId);

      expect(mockLaneRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockLaneId, tenantId: mockTenantId },
      });
      expect(result).toBeDefined();
      expect(result.codigo).toBe('MEX-GDL');
    });

    it('should return null when lane not found', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await lanesService.findOne('non-existent-id', mockTenantId);

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

  describe('findByCodigo', () => {
    it('should find a lane by codigo', async () => {
      const result = await lanesService.findByCodigo('MEX-GDL', mockTenantId);

      expect(mockLaneRepository.findOne).toHaveBeenCalledWith({
        where: { codigo: 'MEX-GDL', tenantId: mockTenantId },
      });
      expect(result).toBeDefined();
    });
  });

  describe('findByOrigenDestino', () => {
    it('should find a lane by origen and destino', async () => {
      const result = await lanesService.findByOrigenDestino('Mexico', 'Guadalajara', mockTenantId);

      expect(mockLaneRepository.findOne).toHaveBeenCalled();
      expect(result).toBeDefined();
    });
  });

  describe('create', () => {
    const createDto = {
      codigo: 'GDL-MTY',
      nombre: 'Guadalajara - Monterrey',
      origenCiudad: 'Guadalajara',
      origenEstado: 'Jalisco',
      destinoCiudad: 'Monterrey',
      destinoEstado: 'Nuevo Leon',
      distanciaKm: 750,
      tiempoEstimadoHoras: 8,
    };

    it('should create a new lane', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await lanesService.create(mockTenantId, createDto, mockUserId);

      expect(mockLaneRepository.create).toHaveBeenCalled();
      expect(mockLaneRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should throw error when codigo already exists', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(createMockLane({ codigo: 'GDL-MTY' }));

      await expect(
        lanesService.create(mockTenantId, createDto, mockUserId)
      ).rejects.toThrow('Ya existe una lane con el codigo "GDL-MTY"');
    });
  });

  describe('update', () => {
    const updateDto = {
      nombre: 'Mexico - Guadalajara (Actualizada)',
      distanciaKm: 545,
    };

    it('should update an existing lane', async () => {
      const result = await lanesService.update(mockLaneId, mockTenantId, updateDto, mockUserId);

      expect(mockLaneRepository.findOne).toHaveBeenCalled();
      expect(mockLaneRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should return null when lane not found', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await lanesService.update('non-existent-id', mockTenantId, updateDto, mockUserId);

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

    it('should check for codigo uniqueness when changing codigo', async () => {
      const mockExisting = createMockLane();
      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(mockExisting) // First call: find by id
        .mockResolvedValueOnce(createMockLane({ id: 'other-id', codigo: 'LANE-EXISTING' })); // Second call: find by codigo

      await expect(
        lanesService.update(mockLaneId, mockTenantId, { codigo: 'LANE-EXISTING' }, mockUserId)
      ).rejects.toThrow('Ya existe una lane con el codigo "LANE-EXISTING"');
    });
  });

  describe('activar / desactivar', () => {
    it('should activate a lane', async () => {
      const result = await lanesService.activar(mockLaneId, mockTenantId);

      expect(mockLaneRepository.findOne).toHaveBeenCalled();
      expect(mockLaneRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should deactivate a lane', async () => {
      const result = await lanesService.desactivar(mockLaneId, mockTenantId);

      expect(mockLaneRepository.findOne).toHaveBeenCalled();
      expect(mockLaneRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should return null when lane not found', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await lanesService.activar('non-existent-id', mockTenantId);

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

  describe('delete', () => {
    it('should delete a lane', async () => {
      const result = await lanesService.delete(mockLaneId, mockTenantId);

      expect(mockLaneRepository.findOne).toHaveBeenCalled();
      expect(mockLaneRepository.delete).toHaveBeenCalledWith(mockLaneId);
      expect(result).toBe(true);
    });

    it('should return false when lane not found', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await lanesService.delete('non-existent-id', mockTenantId);

      expect(result).toBe(false);
    });
  });

  describe('getLanesByOrigenEstado', () => {
    it('should get all active lanes by origin state', async () => {
      const result = await lanesService.getLanesByOrigenEstado('CDMX', mockTenantId);

      expect(mockLaneRepository.find).toHaveBeenCalled();
      expect(result).toBeDefined();
    });
  });

  describe('getLanesByDestinoEstado', () => {
    it('should get all active lanes by destination state', async () => {
      const result = await lanesService.getLanesByDestinoEstado('Jalisco', mockTenantId);

      expect(mockLaneRepository.find).toHaveBeenCalled();
      expect(result).toBeDefined();
    });
  });

  describe('getOrigenCiudades', () => {
    it('should get all unique origin cities', async () => {
      const result = await lanesService.getOrigenCiudades(mockTenantId);

      expect(mockLaneRepository.createQueryBuilder).toHaveBeenCalled();
      expect(mockQueryBuilder.select).toHaveBeenCalledWith('DISTINCT l.origen_ciudad', 'ciudad');
      expect(result).toEqual(['Mexico', 'Guadalajara']);
    });
  });

  describe('getDestinoCiudades', () => {
    it('should get all unique destination cities', async () => {
      const result = await lanesService.getDestinoCiudades(mockTenantId);

      expect(mockLaneRepository.createQueryBuilder).toHaveBeenCalled();
      expect(mockQueryBuilder.select).toHaveBeenCalledWith('DISTINCT l.destino_ciudad', 'ciudad');
      expect(result).toBeDefined();
    });
  });

  describe('getOrigenEstados', () => {
    it('should get all unique origin states', async () => {
      mockQueryBuilder.getRawMany = jest.fn().mockResolvedValue([
        { estado: 'CDMX' },
        { estado: 'Jalisco' },
      ]);

      const result = await lanesService.getOrigenEstados(mockTenantId);

      expect(mockLaneRepository.createQueryBuilder).toHaveBeenCalled();
      expect(mockQueryBuilder.select).toHaveBeenCalledWith('DISTINCT l.origen_estado', 'estado');
      expect(result).toEqual(['CDMX', 'Jalisco']);
    });
  });

  describe('getDestinoEstados', () => {
    it('should get all unique destination states', async () => {
      mockQueryBuilder.getRawMany = jest.fn().mockResolvedValue([
        { estado: 'Jalisco' },
        { estado: 'Nuevo Leon' },
      ]);

      const result = await lanesService.getDestinoEstados(mockTenantId);

      expect(mockLaneRepository.createQueryBuilder).toHaveBeenCalled();
      expect(mockQueryBuilder.select).toHaveBeenCalledWith('DISTINCT l.destino_estado', 'estado');
      expect(result).toEqual(['Jalisco', 'Nuevo Leon']);
    });
  });

  describe('findOrCreate', () => {
    const createDto = {
      codigo: 'MTY-GDL',
      nombre: 'Monterrey - Guadalajara',
      origenCiudad: 'Monterrey',
      origenEstado: 'Nuevo Leon',
      destinoCiudad: 'Guadalajara',
      destinoEstado: 'Jalisco',
    };

    it('should return existing lane when found by origen-destino', async () => {
      const existingLane = createMockLane({
        origenCiudad: 'Monterrey',
        destinoCiudad: 'Guadalajara',
      });
      mockLaneRepository.findOne = jest.fn().mockResolvedValueOnce(existingLane);

      const result = await lanesService.findOrCreate(mockTenantId, createDto, mockUserId);

      expect(result.created).toBe(false);
      expect(result.lane).toBeDefined();
    });

    it('should return existing lane when found by codigo', async () => {
      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(null) // origen-destino not found
        .mockResolvedValueOnce(createMockLane({ codigo: 'MTY-GDL' })); // codigo found

      const result = await lanesService.findOrCreate(mockTenantId, createDto, mockUserId);

      expect(result.created).toBe(false);
      expect(result.lane).toBeDefined();
    });

    it('should create new lane when not found', async () => {
      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(null) // origen-destino not found
        .mockResolvedValueOnce(null) // codigo not found
        .mockResolvedValueOnce(null); // create check

      const result = await lanesService.findOrCreate(mockTenantId, createDto, mockUserId);

      expect(mockLaneRepository.create).toHaveBeenCalled();
      expect(mockLaneRepository.save).toHaveBeenCalled();
      expect(result.created).toBe(true);
    });
  });

  describe('getLaneInversa', () => {
    it('should find the reverse lane', async () => {
      const originalLane = createMockLane();
      const reverseLane = createMockLane({
        id: '550e8400-e29b-41d4-a716-446655440020',
        codigo: 'GDL-MEX',
        origenCiudad: 'Guadalajara',
        destinoCiudad: 'Mexico',
      });

      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(originalLane) // find original
        .mockResolvedValueOnce(reverseLane); // find reverse

      const result = await lanesService.getLaneInversa(mockLaneId, mockTenantId);

      expect(result).toBeDefined();
      expect(result.origenCiudad).toBe('Guadalajara');
      expect(result.destinoCiudad).toBe('Mexico');
    });

    it('should return null when original lane not found', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await lanesService.getLaneInversa('non-existent-id', mockTenantId);

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

    it('should return null when reverse lane does not exist', async () => {
      const originalLane = createMockLane();
      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(originalLane) // find original
        .mockResolvedValueOnce(null); // reverse not found

      const result = await lanesService.getLaneInversa(mockLaneId, mockTenantId);

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

  describe('crearLaneInversa', () => {
    it('should create reverse lane', async () => {
      const originalLane = createMockLane();
      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(originalLane) // find original
        .mockResolvedValueOnce(null) // reverse does not exist
        .mockResolvedValueOnce(null); // codigo check for create

      const result = await lanesService.crearLaneInversa(mockLaneId, mockTenantId, mockUserId);

      expect(mockLaneRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          codigo: 'MEX-GDL-REV',
          origenCiudad: 'Guadalajara',
          origenEstado: 'Jalisco',
          destinoCiudad: 'Mexico',
          destinoEstado: 'CDMX',
        })
      );
      expect(mockLaneRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should return existing reverse lane if already exists', async () => {
      const originalLane = createMockLane();
      const existingReverse = createMockLane({
        id: '550e8400-e29b-41d4-a716-446655440020',
        codigo: 'GDL-MEX',
        origenCiudad: 'Guadalajara',
        destinoCiudad: 'Mexico',
      });

      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(originalLane) // find original
        .mockResolvedValueOnce(existingReverse); // reverse already exists

      const result = await lanesService.crearLaneInversa(mockLaneId, mockTenantId, mockUserId);

      expect(mockLaneRepository.create).not.toHaveBeenCalled();
      expect(result).toEqual(existingReverse);
    });

    it('should return null when original lane not found', async () => {
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await lanesService.crearLaneInversa('non-existent-id', mockTenantId, mockUserId);

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

    it('should preserve distance and time when creating reverse', async () => {
      const originalLane = createMockLane({
        distanciaKm: 540,
        tiempoEstimadoHoras: 6,
      });
      mockLaneRepository.findOne = jest.fn()
        .mockResolvedValueOnce(originalLane) // find original
        .mockResolvedValueOnce(null) // reverse does not exist
        .mockResolvedValueOnce(null); // codigo check

      await lanesService.crearLaneInversa(mockLaneId, mockTenantId, mockUserId);

      expect(mockLaneRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          distanciaKm: 540,
          tiempoEstimadoHoras: 6,
        })
      );
    });
  });
});
