/**
 * @fileoverview Unit tests for TarifasService
 * Tests cover CRUD operations, cotizar calculations, validation, and error handling
 */

import { Repository, SelectQueryBuilder, DeleteResult } from 'typeorm';
import { Tarifa, TipoTarifa, 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-446655440003';
const mockClienteId = '550e8400-e29b-41d4-a716-446655440004';
const mockTarifaId = '550e8400-e29b-41d4-a716-446655440010';

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

const createMockTarifa = (overrides?: Partial<Tarifa>): Partial<Tarifa> => ({
  id: mockTarifaId,
  tenantId: mockTenantId,
  codigo: 'TAR-001',
  nombre: 'Tarifa General FTL',
  descripcion: 'Tarifa general para carga completa',
  clienteId: null,
  laneId: mockLaneId,
  modalidadServicio: 'FTL',
  tipoEquipo: '53FT',
  tipoTarifa: TipoTarifa.POR_KM,
  tarifaBase: 5000,
  tarifaKm: 12.5,
  tarifaTonelada: null,
  tarifaM3: null,
  tarifaPallet: null,
  tarifaHora: null,
  minimoFacturar: 3000,
  pesoMinimoKg: null,
  moneda: 'MXN',
  fechaInicio: new Date('2026-01-01'),
  fechaFin: null,
  activa: true,
  createdAt: new Date('2026-01-01'),
  createdById: mockUserId,
  updatedAt: new Date('2026-01-01'),
  lane: mockLane as Lane,
  // Mock the vigente getter
  get vigente() {
    const hoy = new Date();
    const inicio = new Date(this.fechaInicio as Date);
    const fin = this.fechaFin ? new Date(this.fechaFin) : null;
    return this.activa && hoy >= inicio && (!fin || hoy <= fin);
  },
  ...overrides,
});

describe('TarifasService', () => {
  let mockTarifaRepository: Partial<Repository<Tarifa>>;
  let mockLaneRepository: Partial<Repository<Lane>>;
  let mockQueryBuilder: Partial<SelectQueryBuilder<Tarifa>>;
  let tarifasService: any;

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

    // Setup mock query builder
    mockQueryBuilder = {
      where: jest.fn().mockReturnThis(),
      andWhere: jest.fn().mockReturnThis(),
      leftJoinAndSelect: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      addOrderBy: jest.fn().mockReturnThis(),
      take: jest.fn().mockReturnThis(),
      skip: jest.fn().mockReturnThis(),
      getManyAndCount: jest.fn().mockResolvedValue([[createMockTarifa()], 1]),
      getMany: jest.fn().mockResolvedValue([createMockTarifa()]),
      getOne: jest.fn().mockResolvedValue(createMockTarifa()),
      getCount: jest.fn().mockResolvedValue(1),
      clone: jest.fn().mockReturnThis(),
    };

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

    // Setup mock lane repository
    mockLaneRepository = {
      findOne: jest.fn().mockResolvedValue(mockLane),
    };

    // Import the service class and instantiate
    const { TarifasService } = await import('../services/tarifas.service');
    tarifasService = new TarifasService(
      mockTarifaRepository as Repository<Tarifa>,
      mockLaneRepository as Repository<Lane>
    );
  });

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

      expect(mockTarifaRepository.createQueryBuilder).toHaveBeenCalled();
      expect(mockQueryBuilder.where).toHaveBeenCalledWith('t.tenant_id = :tenantId', { tenantId: mockTenantId });
      expect(mockQueryBuilder.getManyAndCount).toHaveBeenCalled();
      expect(result.data).toBeDefined();
      expect(result.total).toBe(1);
    });

    it('should filter by clienteId', async () => {
      await tarifasService.findAll({ tenantId: mockTenantId, clienteId: mockClienteId });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.cliente_id = :clienteId', { clienteId: mockClienteId });
    });

    it('should filter by laneId', async () => {
      await tarifasService.findAll({ tenantId: mockTenantId, laneId: mockLaneId });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.lane_id = :laneId', { laneId: mockLaneId });
    });

    it('should filter by tipoTarifa', async () => {
      await tarifasService.findAll({ tenantId: mockTenantId, tipoTarifa: TipoTarifa.POR_KM });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.tipo_tarifa = :tipoTarifa', { tipoTarifa: TipoTarifa.POR_KM });
    });

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

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.activa = :activa', { activa: true });
    });

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

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.activa = true');
    });

    it('should filter by moneda', async () => {
      await tarifasService.findAll({ tenantId: mockTenantId, moneda: 'USD' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.moneda = :moneda', { moneda: 'USD' });
    });

    it('should filter by search term', async () => {
      await tarifasService.findAll({ tenantId: mockTenantId, search: 'FTL' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        '(t.codigo ILIKE :search OR t.nombre ILIKE :search OR t.descripcion ILIKE :search)',
        { search: '%FTL%' }
      );
    });

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

      expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
    });
  });

  describe('findOne', () => {
    it('should find a tarifa by id', async () => {
      const result = await tarifasService.findOne(mockTarifaId, mockTenantId);

      expect(mockTarifaRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockTarifaId, tenantId: mockTenantId },
        relations: ['lane'],
      });
      expect(result).toBeDefined();
      expect(result.codigo).toBe('TAR-001');
    });

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

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

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

  describe('findByCodigo', () => {
    it('should find a tarifa by codigo', async () => {
      const result = await tarifasService.findByCodigo('TAR-001', mockTenantId);

      expect(mockTarifaRepository.findOne).toHaveBeenCalledWith({
        where: { codigo: 'TAR-001', tenantId: mockTenantId },
        relations: ['lane'],
      });
      expect(result).toBeDefined();
    });
  });

  describe('create', () => {
    const createDto = {
      codigo: 'TAR-002',
      nombre: 'Nueva Tarifa',
      tipoTarifa: TipoTarifa.POR_VIAJE,
      tarifaBase: 10000,
      fechaInicio: new Date('2026-02-01'),
    };

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

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

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

    it('should throw error when codigo already exists', async () => {
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(createMockTarifa({ codigo: 'TAR-002' }));

      await expect(
        tarifasService.create(mockTenantId, createDto, mockUserId)
      ).rejects.toThrow('Ya existe una tarifa con el codigo "TAR-002"');
    });

    it('should validate lane if provided', async () => {
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(null);
      mockLaneRepository.findOne = jest.fn().mockResolvedValue(mockLane);

      const dtoWithLane = { ...createDto, laneId: mockLaneId };
      await tarifasService.create(mockTenantId, dtoWithLane, mockUserId);

      expect(mockLaneRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockLaneId, tenantId: mockTenantId },
      });
    });

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

      const dtoWithLane = { ...createDto, laneId: 'non-existent-lane' };

      await expect(
        tarifasService.create(mockTenantId, dtoWithLane, mockUserId)
      ).rejects.toThrow('Lane no encontrada');
    });
  });

  describe('update', () => {
    const updateDto = {
      nombre: 'Tarifa Actualizada',
      tarifaBase: 6000,
    };

    it('should update an existing tarifa', async () => {
      const result = await tarifasService.update(mockTarifaId, mockTenantId, updateDto, mockUserId);

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

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

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

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

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

      await expect(
        tarifasService.update(mockTarifaId, mockTenantId, { codigo: 'TAR-EXISTING' }, mockUserId)
      ).rejects.toThrow('Ya existe una tarifa con el codigo "TAR-EXISTING"');
    });
  });

  describe('activar / desactivar', () => {
    it('should activate a tarifa', async () => {
      const result = await tarifasService.activar(mockTarifaId, mockTenantId);

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

    it('should deactivate a tarifa', async () => {
      const result = await tarifasService.desactivar(mockTarifaId, mockTenantId);

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

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

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

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

  describe('delete', () => {
    it('should delete a tarifa', async () => {
      const result = await tarifasService.delete(mockTarifaId, mockTenantId);

      expect(mockTarifaRepository.findOne).toHaveBeenCalled();
      expect(mockTarifaRepository.delete).toHaveBeenCalledWith(mockTarifaId);
      expect(result).toBe(true);
    });

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

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

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

  describe('cotizar', () => {
    it('should calculate quote for POR_KM tariff', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.POR_KM,
        tarifaBase: 5000,
        tarifaKm: 10,
        minimoFacturar: null,
      });
      // Add vigente getter
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, { km: 500 });

      expect(result.montoCalculado).toBe(10000); // 5000 + (500 * 10)
      expect(result.desglose.tarifaBase).toBe(5000);
      expect(result.desglose.montoVariable).toBe(5000);
      expect(result.desglose.minimoAplicado).toBe(false);
    });

    it('should calculate quote for POR_TONELADA tariff', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.POR_TONELADA,
        tarifaBase: 2000,
        tarifaTonelada: 150,
        minimoFacturar: null,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, { toneladas: 20 });

      expect(result.montoCalculado).toBe(5000); // 2000 + (20 * 150)
      expect(result.desglose.montoVariable).toBe(3000);
    });

    it('should calculate quote for POR_M3 tariff', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.POR_M3,
        tarifaBase: 1000,
        tarifaM3: 50,
        minimoFacturar: null,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, { m3: 30 });

      expect(result.montoCalculado).toBe(2500); // 1000 + (30 * 50)
    });

    it('should calculate quote for POR_PALLET tariff', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.POR_PALLET,
        tarifaBase: 500,
        tarifaPallet: 200,
        minimoFacturar: null,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, { pallets: 15 });

      expect(result.montoCalculado).toBe(3500); // 500 + (15 * 200)
    });

    it('should calculate quote for POR_HORA tariff', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.POR_HORA,
        tarifaBase: 1000,
        tarifaHora: 300,
        minimoFacturar: null,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, { horas: 8 });

      expect(result.montoCalculado).toBe(3400); // 1000 + (8 * 300)
    });

    it('should calculate quote for MIXTA tariff', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.MIXTA,
        tarifaBase: 3000,
        tarifaKm: 5,
        tarifaTonelada: 100,
        minimoFacturar: null,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, { km: 400, toneladas: 10 });

      expect(result.montoCalculado).toBe(6000); // 3000 + (400 * 5) + (10 * 100)
      expect(result.desglose.montoVariable).toBe(3000);
    });

    it('should calculate quote for POR_VIAJE tariff', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.POR_VIAJE,
        tarifaBase: 15000,
        minimoFacturar: null,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, {});

      expect(result.montoCalculado).toBe(15000);
      expect(result.desglose.montoVariable).toBe(0);
    });

    it('should apply minimum when calculated amount is lower', async () => {
      const mockTarifa = createMockTarifa({
        tipoTarifa: TipoTarifa.POR_KM,
        tarifaBase: 1000,
        tarifaKm: 5,
        minimoFacturar: 5000,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => true });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      const result = await tarifasService.cotizar(mockTarifaId, mockTenantId, { km: 100 });

      expect(result.montoCalculado).toBe(5000); // Minimo applied (1000 + 500 = 1500 < 5000)
      expect(result.desglose.minimoAplicado).toBe(true);
    });

    it('should throw error when tarifa not found', async () => {
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(null);

      await expect(
        tarifasService.cotizar('non-existent-id', mockTenantId, { km: 100 })
      ).rejects.toThrow('Tarifa no encontrada');
    });

    it('should throw error when tarifa is not vigente', async () => {
      const mockTarifa = createMockTarifa({
        activa: false,
      });
      Object.defineProperty(mockTarifa, 'vigente', { get: () => false });
      mockTarifaRepository.findOne = jest.fn().mockResolvedValue(mockTarifa);

      await expect(
        tarifasService.cotizar(mockTarifaId, mockTenantId, { km: 100 })
      ).rejects.toThrow('La tarifa no esta vigente');
    });
  });

  describe('getTarifaAplicable', () => {
    it('should find client-specific tarifa with lane priority', async () => {
      const clientLaneTarifa = createMockTarifa({ clienteId: mockClienteId, laneId: mockLaneId });
      mockQueryBuilder.getOne = jest.fn().mockResolvedValueOnce(clientLaneTarifa);

      const result = await tarifasService.getTarifaAplicable(mockTenantId, {
        clienteId: mockClienteId,
        laneId: mockLaneId,
      });

      expect(result).toBeDefined();
      expect(mockQueryBuilder.clone).toHaveBeenCalled();
    });

    it('should fallback to general tarifa when no specific found', async () => {
      mockQueryBuilder.getOne = jest.fn()
        .mockResolvedValueOnce(null) // client + lane
        .mockResolvedValueOnce(null) // client only
        .mockResolvedValueOnce(null) // lane only
        .mockResolvedValueOnce(createMockTarifa({ clienteId: null, laneId: null })); // general

      const result = await tarifasService.getTarifaAplicable(mockTenantId, {
        clienteId: mockClienteId,
        laneId: mockLaneId,
      });

      expect(result).toBeDefined();
    });
  });

  describe('getTarifasCliente', () => {
    it('should get all active tarifas for a client', async () => {
      const result = await tarifasService.getTarifasCliente(mockClienteId, mockTenantId);

      expect(mockTarifaRepository.find).toHaveBeenCalledWith({
        where: { clienteId: mockClienteId, tenantId: mockTenantId, activa: true },
        relations: ['lane'],
        order: { fechaInicio: 'DESC' },
      });
      expect(result).toBeDefined();
    });
  });

  describe('getTarifasLane', () => {
    it('should get all active tarifas for a lane', async () => {
      const result = await tarifasService.getTarifasLane(mockLaneId, mockTenantId);

      expect(mockTarifaRepository.find).toHaveBeenCalledWith({
        where: { laneId: mockLaneId, tenantId: mockTenantId, activa: true },
        relations: ['lane'],
        order: { fechaInicio: 'DESC' },
      });
      expect(result).toBeDefined();
    });
  });

  describe('getTarifasPorVencer', () => {
    it('should get tarifas expiring within specified days', async () => {
      mockQueryBuilder.getMany = jest.fn().mockResolvedValue([createMockTarifa()]);

      const result = await tarifasService.getTarifasPorVencer(mockTenantId, 30);

      expect(mockTarifaRepository.createQueryBuilder).toHaveBeenCalled();
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.activa = true');
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('t.fecha_fin IS NOT NULL');
      expect(result).toBeDefined();
    });
  });

  describe('clonarTarifa', () => {
    it('should clone a tarifa with new validity period', async () => {
      mockTarifaRepository.findOne = jest.fn()
        .mockResolvedValueOnce(createMockTarifa()) // source tarifa
        .mockResolvedValueOnce(null); // codigo check

      const result = await tarifasService.clonarTarifa(
        mockTarifaId,
        mockTenantId,
        'TAR-002-CLONE',
        new Date('2027-01-01'),
        new Date('2027-12-31'),
        mockUserId
      );

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

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

      const result = await tarifasService.clonarTarifa(
        'non-existent-id',
        mockTenantId,
        'TAR-002-CLONE',
        new Date('2027-01-01')
      );

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

    it('should throw error when new codigo already exists', async () => {
      mockTarifaRepository.findOne = jest.fn()
        .mockResolvedValueOnce(createMockTarifa()) // source tarifa
        .mockResolvedValueOnce(createMockTarifa({ codigo: 'TAR-EXISTING' })); // codigo exists

      await expect(
        tarifasService.clonarTarifa(
          mockTarifaId,
          mockTenantId,
          'TAR-EXISTING',
          new Date('2027-01-01')
        )
      ).rejects.toThrow('Ya existe una tarifa con el codigo "TAR-EXISTING"');
    });
  });
});
