import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { ViajeCostosService } from '../viaje-costos.service';

/**
 * Costo y rentabilidad del viaje (Fase B). El costo es la suma de las
 * rendiciones de viáticos asociadas (una por persona); el margen bruto sale
 * del costo de mercadería de las líneas de factura que lo tienen cargado.
 */
describe('ViajeCostosService', () => {
  const rendicion = (id: string, o: any = {}) => ({
    id: `fila-${id}`,
    rendicion: {
      id,
      numero_rendicion: o.numero ?? `RVI-${id}`,
      concepto: o.concepto ?? 'Viáticos reparto',
      estado: o.estado ?? 'CERRADA',
      fecha_inicio: new Date('2026-01-19T00:00:00Z'),
      fecha_fin: new Date('2026-01-23T00:00:00Z'),
      monto_rendido_total: o.rendido ?? 0,
      monto_adelanto_total: o.adelanto ?? 0,
      cotizacion: o.cotizacion ?? 1,
      moneda: { codigo: o.moneda ?? 'PYG' },
      empleado: { nombres: o.nombres ?? 'Ever', apellidos: o.apellidos ?? 'Benítez' },
      gastos: o.gastos ?? [],
    },
  });
  const gasto = (lineas: [string | null, number][], o: any = {}) => ({
    cotizacion: o.cotizacion ?? 1,
    moneda: { codigo: o.moneda ?? 'PYG' },
    gasto_det: lineas.map(([tipo, total]) => ({ total, tipo_gasto: tipo ? { descripcion: tipo } : null })),
  });

  const crear = ({
    filas = [] as any[],
    lineas = [] as any[],
    viaje = {} as any,
    rendicionBuscada = null as any,
    asociacionExistente = null as any,
    filaBuscada = null as any,
  }) => {
    const prisma: any = {
      viaje_rendiciones: {
        findMany: jest.fn().mockResolvedValue(filas),
        findFirst: jest.fn().mockImplementation((args: any) =>
          Promise.resolve(args?.where?.rendicion_viatico_id ? asociacionExistente : filaBuscada),
        ),
        createMany: jest.fn().mockResolvedValue({ count: 1 }),
        delete: jest.fn().mockResolvedValue({}),
      },
      rendicion_viatico: { findFirst: jest.fn().mockResolvedValue(rendicionBuscada) },
      factura_det: { findMany: jest.fn().mockResolvedValue(lineas) },
    };
    const documentos: any = { idsPorTipo: jest.fn().mockResolvedValue(lineas.length ? ['f-1'] : []) };
    const base = { id: 'v-1', empresa_id: 'emp-1', estado: 'EN_RUTA', ...viaje };
    const viajes: any = {
      findOne: jest.fn().mockResolvedValue(base),
      exigirEditable: jest.fn().mockImplementation(() => {
        if (!['PLANIFICADO', 'EN_RUTA'].includes(base.estado)) throw new BadRequestException('cerrado');
        return Promise.resolve(base);
      }),
    };
    const monedas: any = { getCotizacionVigente: jest.fn().mockResolvedValue({ tasa: 7500 }) };
    const service = Object.create(ViajeCostosService.prototype) as any;
    service.prisma = prisma;
    // Los cálculos piden solo los vigentes; acá el filtro de bajas no es lo que se prueba.
    documentos.idsVigentes ??= documentos.idsPorTipo;
    service.documentos = documentos;
    service.viajes = viajes;
    service.monedas = monedas;
    return { service, prisma };
  };

  describe('costos', () => {
    it('suma varias rendiciones, una por persona, con desglose por tipo de gasto', async () => {
      const { service } = crear({
        filas: [
          rendicion('r-1', { nombres: 'Ever', rendido: 900_000,
            gastos: [gasto([['VIÁTICOS A VENDEDORES', 600_000], ['COMBUSTIBLES Y LUBRICANTES', 300_000]])] }),
          rendicion('r-2', { nombres: 'Victor', apellidos: 'Gómez', rendido: 2_980_000,
            gastos: [gasto([['VIÁTICOS A VENDEDORES', 2_980_000]])] }),
        ],
      });

      const r = await service.costos('v-1', 'emp-1');

      expect(r.costo_base).toBe(3_880_000);
      expect(r.rendiciones).toHaveLength(2);
      expect(r.por_persona).toEqual([
        { empleado: 'Victor Gómez', monto_base: 2_980_000 },
        { empleado: 'Ever Benítez', monto_base: 900_000 },
      ]);
      expect(r.por_tipo).toEqual([
        { tipo: 'VIÁTICOS A VENDEDORES', monto_base: 3_580_000 },
        { tipo: 'COMBUSTIBLES Y LUBRICANTES', monto_base: 300_000 },
      ]);
      expect(r.un_solo_tipo).toBe(false);
    });

    it('lo rendido sin gasto que lo respalde va como "Sin detalle"; un solo tipo se avisa', async () => {
      const { service } = crear({
        filas: [rendicion('r-1', { rendido: 1_000_000, gastos: [gasto([['VIÁTICOS A VENDEDORES', 700_000]])] })],
      });

      const r = await service.costos('v-1', 'emp-1');

      expect(r.rendiciones[0].por_tipo).toEqual([
        { tipo: 'VIÁTICOS A VENDEDORES', monto_base: 700_000 },
        { tipo: 'Sin detalle', monto_base: 300_000 },
      ]);
      // Todo cae en "VIÁTICOS A VENDEDORES": no dice en qué se gastó.
      expect(r.un_solo_tipo).toBe(true);
    });

    it('la rendición anulada se lista pero no suma al costo', async () => {
      const { service } = crear({
        filas: [rendicion('r-1', { rendido: 500_000 }), rendicion('r-2', { rendido: 800_000, estado: 'ANULADA' })],
      });

      const r = await service.costos('v-1', 'emp-1');

      expect(r.costo_base).toBe(500_000);
      expect(r.rendiciones.find((x: any) => x.id === 'r-2').anulada).toBe(true);
      expect(r.rendiciones_activas).toBe(1);
    });

    it('rendición en dólares: convierte con su cotización', async () => {
      const { service } = crear({ filas: [rendicion('r-1', { rendido: 100, moneda: 'USD', cotizacion: 7_300 })] });
      const r = await service.costos('v-1', 'emp-1');
      expect(r.costo_base).toBe(730_000);
    });

    it('margen bruto solo con las líneas que tienen costo, con su cobertura', async () => {
      const { service } = crear({
        lineas: [
          { dtotopeitem: 1_000, costo_total_venta: 600, factura_cab: { dticam: null, estado: 'Aprobado', moneda: { codigo: 'PYG' } } },
          { dtotopeitem: 500, costo_total_venta: 300, factura_cab: { dticam: null, estado: 'Aprobado', moneda: { codigo: 'PYG' } } },
          // Sin costo cargado: no resta (no se asume costo cero).
          { dtotopeitem: 1_500, costo_total_venta: null, factura_cab: { dticam: null, estado: 'Aprobado', moneda: { codigo: 'PYG' } } },
          { dtotopeitem: 999, costo_total_venta: 1, factura_cab: { dticam: null, estado: 'Anulado', moneda: { codigo: 'PYG' } } },
        ],
      });

      const r = await service.costos('v-1', 'emp-1');

      expect(r.margen.importe_base).toBe(3_000);
      expect(r.margen.importe_con_costo_base).toBe(1_500);
      expect(r.margen.costo_mercaderia_base).toBe(900);
      expect(r.margen.margen_bruto_base).toBe(600);
      expect(r.margen.cobertura).toBeCloseTo(0.5);
      expect(r.margen.margen_pct).toBeCloseTo(0.4);
    });

    it('sin líneas con costo: margen nulo, cobertura cero', async () => {
      const { service } = crear({
        lineas: [{ dtotopeitem: 1_000, costo_total_venta: 0, factura_cab: { dticam: null, estado: 'Aprobado', moneda: { codigo: 'PYG' } } }],
      });
      const r = await service.costos('v-1', 'emp-1');
      expect(r.margen.margen_bruto_base).toBeNull();
      expect(r.margen.margen_pct).toBeNull();
      expect(r.margen.cobertura).toBe(0);
    });
  });

  describe('asociar', () => {
    it('asocia una rendición de la empresa', async () => {
      const { service, prisma } = crear({ rendicionBuscada: { id: 'r-1', estado: 'CERRADA', numero_rendicion: 'RVI-1' } });
      await service.asociar('v-1', 'emp-1', ['r-1'], 'u-1');
      expect(prisma.viaje_rendiciones.createMany).toHaveBeenCalledWith({
        data: [{ viaje_id: 'v-1', rendicion_viatico_id: 'r-1', created_by: 'u-1' }],
      });
    });

    it('rechaza una rendición de otra empresa', async () => {
      const { service } = crear({ rendicionBuscada: null });
      await expect(service.asociar('v-1', 'emp-1', ['r-x'], 'u-1')).rejects.toThrow(NotFoundException);
    });

    it('rechaza una rendición anulada', async () => {
      const { service } = crear({ rendicionBuscada: { id: 'r-1', estado: 'ANULADA', numero_rendicion: 'RVI-1' } });
      await expect(service.asociar('v-1', 'emp-1', ['r-1'], 'u-1')).rejects.toThrow(BadRequestException);
    });

    it('rechaza una rendición que ya está en otro viaje, diciendo cuál', async () => {
      const { service } = crear({
        rendicionBuscada: { id: 'r-1', estado: 'CERRADA', numero_rendicion: 'RVI-1' },
        asociacionExistente: { viaje_id: 'v-2', viaje: { numero: 'VJ-000002' } },
      });
      await expect(service.asociar('v-1', 'emp-1', ['r-1'], 'u-1')).rejects.toThrow(/VJ-000002/);
      await expect(service.asociar('v-1', 'emp-1', ['r-1'], 'u-1')).rejects.toThrow(ConflictException);
    });

    it('si ya está en este viaje no la duplica', async () => {
      const { service, prisma } = crear({
        rendicionBuscada: { id: 'r-1', estado: 'CERRADA', numero_rendicion: 'RVI-1' },
        asociacionExistente: { viaje_id: 'v-1', viaje: { numero: 'VJ-000001' } },
      });
      await service.asociar('v-1', 'emp-1', ['r-1'], 'u-1');
      expect(prisma.viaje_rendiciones.createMany).not.toHaveBeenCalled();
    });

    it('un viaje cerrado no acepta rendiciones nuevas', async () => {
      const { service } = crear({ viaje: { estado: 'CERRADO' }, rendicionBuscada: { id: 'r-1', estado: 'CERRADA' } });
      await expect(service.asociar('v-1', 'emp-1', ['r-1'], 'u-1')).rejects.toThrow(BadRequestException);
    });
  });

  describe('quitar', () => {
    it('quita la fila del viaje', async () => {
      const { service, prisma } = crear({ filaBuscada: { id: 'fila-1' } });
      await service.quitar('v-1', 'emp-1', 'fila-1');
      expect(prisma.viaje_rendiciones.delete).toHaveBeenCalledWith({ where: { id: 'fila-1' } });
    });

    it('404 si la fila no es de este viaje', async () => {
      const { service } = crear({ filaBuscada: null });
      await expect(service.quitar('v-1', 'emp-1', 'fila-x')).rejects.toThrow(NotFoundException);
    });
  });
});
