import { BadRequestException, NotFoundException, NotImplementedException } from '@nestjs/common';
import { OrdenFacturacionService } from './orden-facturacion.service';

const DET = (over: Record<string, any> = {}) => ({
  id: 'det-1', producto_id: 'prod-1', descripcion: 'Taladro X200', unidad_medida: 'UNI',
  cantidad: 10, cant_pendiente: 10, subtotal: 1_100_000, monto_iva: 100_000, porcentaje_iva: 10,
  proporcion_iva: 100, descuento_monto: 0, descuento_porcentaje: 0, descuento_global_item: 0, ...over,
});

const ORDEN = (over: Record<string, any> = {}) => ({
  id: 'ov-1', empresa_id: 'emp-1', sucursal_id: 'suc-1', cliente_id: 'cli-1',
  tipo_pedido: 'orden_venta', estado_orden: 'aprobada', numero_pedido: 'OV-0000312',
  moneda_id: null, condicion_operacion_id: null, detalle: [DET()], ...over,
});

describe('OrdenFacturacionService', () => {
  const crear = (over: Record<string, any> = {}) => {
    const prisma: any = {
      pedidos: { findFirst: jest.fn().mockResolvedValue(ORDEN()) },
      numeraciones_documento: {
        findFirst: jest.fn().mockResolvedValue({ id: 'num-1' }),
        findMany: jest.fn().mockResolvedValue([{ id: 'num-1', es_default: true }]),
      },
      moneda: { findFirst: jest.fn().mockResolvedValue({ id: 'mon-1' }), findUnique: jest.fn().mockResolvedValue({ id: 'mon-1' }) },
      indicador_presencia: { findFirst: jest.fn().mockResolvedValue({ id: 'ind-1' }) },
      tipo_impuesto: { findFirst: jest.fn().mockResolvedValue({ id: 'ti-1' }) },
      tipo_transaccion: { findFirst: jest.fn().mockResolvedValue({ id: 'tt-1' }) },
      condicion_operacion: { findFirst: jest.fn().mockResolvedValue({ id: 'co-1' }), findUnique: jest.fn().mockResolvedValue({ id: 'co-1' }) },
      medio_pago: { findFirst: jest.fn().mockResolvedValue({ id: 'mp-1' }), findUnique: jest.fn().mockResolvedValue({ id: 'mp-1' }) },
      ...over,
    };
    const facturas = { create: jest.fn().mockResolvedValue({ data: { id: 'fac-1' } }) };
    const pedidos = {
      facturarOrden: jest.fn().mockResolvedValue({}),
      findOneOrden: jest.fn().mockResolvedValue({ id: 'ov-1', estado_orden: 'facturada' }),
    };
    return { service: new OrdenFacturacionService(prisma as any, facturas as any, pedidos as any), prisma, facturas, pedidos };
  };

  describe('validaciones previas', () => {
    /** Todo lo que puede fallar se valida ANTES de emitir: la factura no se deshace. */
    it('una orden en borrador no se factura', async () => {
      const { service, facturas } = crear({
        pedidos: { findFirst: jest.fn().mockResolvedValue(ORDEN({ estado_orden: 'borrador' })) },
      });
      await expect(service.convertirAFactura('ov-1', 'emp-1', 'u-1')).rejects.toThrow(BadRequestException);
      expect(facturas.create).not.toHaveBeenCalled();
    });

    it('una orden de otra empresa no existe', async () => {
      const { service } = crear({ pedidos: { findFirst: jest.fn().mockResolvedValue(null) } });
      await expect(service.convertirAFactura('ov-1', 'emp-1', 'u-1')).rejects.toThrow(NotFoundException);
    });

    it('sin ítems pendientes no hay nada que facturar', async () => {
      const { service, facturas } = crear({
        pedidos: { findFirst: jest.fn().mockResolvedValue(ORDEN({ detalle: [DET({ cant_pendiente: 0 })] })) },
      });
      await expect(service.convertirAFactura('ov-1', 'emp-1', 'u-1')).rejects.toThrow(/pendientes/i);
      expect(facturas.create).not.toHaveBeenCalled();
    });

    it('la facturación parcial por ítem todavía no existe y lo dice', async () => {
      const { service } = crear();
      await expect(
        service.convertirAFactura('ov-1', 'emp-1', 'u-1', { items: [{ pedido_detalle_id: 'd', cantidad: 1 }] }),
      ).rejects.toThrow(NotImplementedException);
    });
  });

  describe('numeración', () => {
    it('con varias y ninguna por defecto, pide elegir en vez de adivinar', async () => {
      const { service, facturas } = crear({
        numeraciones_documento: {
          findMany: jest.fn().mockResolvedValue([{ id: 'n1', es_default: false }, { id: 'n2', es_default: false }]),
          findFirst: jest.fn(),
        },
      });
      await expect(service.convertirAFactura('ov-1', 'emp-1', 'u-1')).rejects.toThrow(/numeracion_id|predeterminada/i);
      expect(facturas.create).not.toHaveBeenCalled();
    });

    it('si hay una sola activa la usa sin pedir nada', async () => {
      const { service, facturas } = crear({
        numeraciones_documento: {
          findMany: jest.fn().mockResolvedValue([{ id: 'n-unica', es_default: false }]),
          findFirst: jest.fn(),
        },
      });
      await service.convertirAFactura('ov-1', 'emp-1', 'u-1');
      expect(facturas.create.mock.calls[0][0].cabecera.numeracion_id).toBe('n-unica');
    });

    it('sin numeración de factura en la sucursal, falla con un mensaje claro', async () => {
      const { service } = crear({
        numeraciones_documento: { findMany: jest.fn().mockResolvedValue([]), findFirst: jest.fn() },
      });
      await expect(service.convertirAFactura('ov-1', 'emp-1', 'u-1')).rejects.toThrow(/numeración de factura/i);
    });
  });

  describe('armado de la factura', () => {
    /**
     * Los importes salen de la orden, no de la lista de precios: si entre medio
     * cambió un precio, la factura tiene que seguir coincidiendo con lo firmado.
     */
    it('toma los importes de la orden tal cual', async () => {
      const { service, facturas } = crear();

      await service.convertirAFactura('ov-1', 'emp-1', 'u-1');

      const dto = facturas.create.mock.calls[0][0];
      expect(dto.items[0].total_operacion).toBe(1_100_000);
      expect(dto.items[0].liquidacion_iva).toBe(100_000);
      expect(dto.items[0].base_gravada).toBe(1_000_000);
      expect(dto.items[0].precio_unitario).toBe(110_000);
    });

    it('los subtotales cuadran con las líneas', async () => {
      const { service, facturas } = crear();

      await service.convertirAFactura('ov-1', 'emp-1', 'u-1');

      const s = facturas.create.mock.calls[0][0].subtotal;
      expect(s.total_general_operacion).toBe(1_100_000);
      expect(s.total_iva).toBe(100_000);
      expect(s.total_base_gravada_10).toBe(1_000_000);
      expect(s.subtotal_10).toBe(1_100_000);
    });

    it('un ítem exento va como afectación 3 y no suma base gravada', async () => {
      const { service, facturas } = crear({
        pedidos: {
          findFirst: jest.fn().mockResolvedValue(
            ORDEN({ detalle: [DET({ porcentaje_iva: 0, monto_iva: 0, subtotal: 500_000 })] }),
          ),
        },
      });

      await service.convertirAFactura('ov-1', 'emp-1', 'u-1');

      const dto = facturas.create.mock.calls[0][0];
      expect(dto.items[0].afectacion_iva).toBe(3);
      expect(dto.items[0].base_gravada_exenta).toBe(500_000);
      expect(dto.subtotal.subtotal_exento).toBe(500_000);
      expect(dto.subtotal.total_base_gravada).toBe(0);
    });

    it('factura sólo lo pendiente de una orden parcialmente facturada', async () => {
      const { service, facturas } = crear({
        pedidos: {
          findFirst: jest.fn().mockResolvedValue(
            ORDEN({
              estado_orden: 'facturada_parcial',
              detalle: [DET({ cant_pendiente: 0 }), DET({ id: 'det-2', cant_pendiente: 4, subtotal: 440_000, monto_iva: 40_000 })],
            }),
          ),
        },
      });

      await service.convertirAFactura('ov-1', 'emp-1', 'u-1');

      const items = facturas.create.mock.calls[0][0].items;
      expect(items).toHaveLength(1);
      expect(items[0].cantidad).toBe(4);
    });
  });

  describe('después de emitir', () => {
    it('marca la orden con el id de la factura', async () => {
      const { service, pedidos } = crear();
      await service.convertirAFactura('ov-1', 'emp-1', 'u-1');
      expect(pedidos.facturarOrden).toHaveBeenCalledWith('ov-1', 'fac-1', 'emp-1', 'u-1');
    });

    /**
     * La factura ya existe y está encolada a SIFEN: no se puede deshacer. Lo
     * único útil es decir su número (no el id interno) para vincularla a mano.
     */
    it('si el marcado falla, el error trae el número de la factura ya emitida (no su id)', async () => {
      const { service, pedidos, facturas } = crear();
      facturas.create.mockResolvedValue({ data: { id: 'fac-1', numero: '001-001-0000123' } });
      pedidos.facturarOrden.mockRejectedValue(new Error('boom'));

      const err = await service.convertirAFactura('ov-1', 'emp-1', 'u-1').catch((e: Error) => e);
      expect((err as Error).message).toMatch(/001-001-0000123/);
      expect((err as Error).message).not.toMatch(/fac-1/);
    });
  });
});
