import * as XLSX from 'xlsx';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { CuentasPagarReportesService } from './cuentas-pagar-reportes.service';
import { ExtractoProveedorService } from './extracto-proveedor.service';

jest.mock('src/utils/alcance-sucursal', () => ({ alcanceSucursalUsuario: jest.fn().mockResolvedValue(null) }));

const d = (s: string) => new Date(`${s}T00:00:00Z`);

// Factura 356 (2 cuotas de 4.583.500 = 9.167.000), compra 1549 en una sola cuota.
const cuota = (o: Record<string, unknown>) => ({
  id: 'cp',
  proveedor_id: 'prov',
  compra_id: null,
  origen_tipo: 'gasto',
  origen_id: 'g-356',
  numero_cuota: 1,
  total_cuotas: 1,
  fecha_emision: d('2025-12-31'),
  compra_cab: null,
  moneda: { codigo: 'PYG' },
  monto_original: 0,
  saldo_pendiente: 0,
  ...o,
});

function crear(data: { cuentas?: unknown[]; pagos?: unknown[]; ncs?: unknown[]; compensaciones?: unknown[]; proveedor?: unknown; gastos?: unknown[] } = {}) {
  const prisma = {
    proveedores: { findFirst: jest.fn().mockResolvedValue(data.proveedor === undefined ? { id: 'prov', personas: { razon_social: 'BALBINO BALBUENA GIMENEZ', ruc: '862074', dv: '1', email: '' } } : data.proveedor) },
    cuentas_pagar: { findMany: jest.fn().mockResolvedValue(data.cuentas ?? []) },
    gasto_cab: {
      findMany: jest.fn().mockResolvedValue([
        { id: 'g-356', numero_factura: '001-001-0000356', fecha_emision: d('2025-12-31'), sucursal_id: 's', descripcion: '', forma_pago: 'CREDITO', medio_pago: null, gasto_det: [{ tipo_gasto: { descripcion: 'HONORARIOS' } }] },
        ...(data.gastos ?? []),
      ]),
    },
    pagos_proveedor: { findMany: jest.fn().mockResolvedValue(data.pagos ?? []) },
    nota_credito_compra_cab: { findMany: jest.fn().mockResolvedValue(data.ncs ?? []) },
    recibo_cobro_facturas_compra_aplicadas: { findMany: jest.fn().mockResolvedValue(data.compensaciones ?? []) },
    empresas: { findUnique: jest.fn().mockResolvedValue({ razon_social: 'DOBA S.A.' }) },
  };
  return { s: new ExtractoProveedorService(prisma as never, new CuentasPagarReportesService(prisma as never)), prisma };
}

const compra1549 = { establecimiento: '001', punto_expedicion: '001', numero_factura: '0001549', fecha_emision: d('2026-02-24'), sucursal_id: 's', observaciones: 'MATERIALES' };

describe('ExtractoProveedorService', () => {
  it('ordena por fecha, lleva el saldo corrido, el de cada factura y el saldo anterior', async () => {
    const { s } = crear({
      cuentas: [
        cuota({ id: 'c1', numero_cuota: 1, total_cuotas: 2, monto_original: 4_583_500, saldo_pendiente: 0 }),
        cuota({ id: 'c2', numero_cuota: 2, total_cuotas: 2, monto_original: 4_583_500, saldo_pendiente: 0 }),
        cuota({ id: 'c3', compra_id: 'k-1549', origen_tipo: 'compra', origen_id: null, compra_cab: compra1549, monto_original: 11_054_200, saldo_pendiente: 6_054_200 }),
      ],
      pagos: [
        // La OP 27 pagó 9.000 en enero y la OP 31 el resto (dos filas, una por cuota).
        { cuenta_pagar_id: 'c1', fecha_pago: d('2026-01-09'), numero_recibo: 'OP-27', monto: 9_000, referencia: null, numero_cheque: null, medio_pago: { descripcion: 'Transferencia' } },
        { cuenta_pagar_id: 'c1', fecha_pago: d('2026-02-06'), numero_recibo: 'OP-31', monto: 4_574_500, referencia: null, numero_cheque: '4512', medio_pago: { descripcion: 'Cheque' } },
        { cuenta_pagar_id: 'c2', fecha_pago: d('2026-02-06'), numero_recibo: 'OP-31', monto: 4_583_500, referencia: null, numero_cheque: '4512', medio_pago: { descripcion: 'Cheque' } },
      ],
      ncs: [{ compra_id: 'k-1549', gasto_id: null, establecimiento: '001', punto_expedicion: '001', numero: '0001975', fecha_emision: d('2026-03-10'), motivo: 'Devolución', total: 5_000_000, moneda: { codigo: 'PYG' } }],
    });

    const ex = await s.extracto('emp', 'prov', undefined, { desde: '2026-02-01' });
    const b = ex.monedas[0];

    // Antes de febrero: factura 356 (9.167.000) − pago de 9.000.
    expect(b.saldo_anterior).toBe(9_158_000);
    expect(b.movimientos.map((m) => [m.fecha, m.tipo, m.comprobante, m.debito, m.credito, m.saldo_documento, m.saldo])).toEqual([
      // Las dos filas de la OP 31 (misma factura) van en una sola línea.
      ['2026-02-06', 'pago', 'OP-31', 0, 9_158_000, 0, 0],
      ['2026-02-24', 'factura', '001-001-0001549', 11_054_200, 0, 11_054_200, 11_054_200],
      ['2026-03-10', 'nota_credito', '001-001-0001975', 0, 5_000_000, 6_054_200, 6_054_200],
    ]);
    expect(b.movimientos[0].concepto).toBe('Pago · Cheque · cheque 4512');
    expect(b.movimientos[0].documento).toBe('001-001-0000356');
    expect(b.movimientos[2].documento).toBe('001-001-0001549');
    expect([b.total_debito, b.total_credito, b.saldo_final]).toEqual([11_054_200, 14_158_000, 6_054_200]);
    expect(b.diferencia).toBe(0);
  });

  it('si los movimientos no explican la deuda real, agrega la línea de diferencia al final', async () => {
    // El saldo real quedó en 0 aunque no hay ningún pago registrado (p. ej. ajuste viejo).
    const { s } = crear({ cuentas: [cuota({ id: 'c1', monto_original: 1_000_000, saldo_pendiente: 0 })] });
    const b = (await s.extracto('emp', 'prov', undefined, { desde: '2025-01-01' })).monedas[0];

    const ultima = b.movimientos.at(-1);
    expect(ultima).toEqual(expect.objectContaining({ tipo: 'diferencia', credito: 1_000_000, saldo: 0 }));
    expect(b.saldo_final).toBe(b.saldo_pendiente_hoy);
    expect(b.diferencia).toBe(-1_000_000);
  });

  it('un extracto que no llega a hoy no inventa la línea de diferencia, pero la informa', async () => {
    const { s } = crear({ cuentas: [cuota({ id: 'c1', monto_original: 1_000_000, saldo_pendiente: 0 })] });
    const b = (await s.extracto('emp', 'prov', undefined, { desde: '2025-01-01', hasta: '2026-01-31' })).monedas[0];

    expect(b.movimientos.some((m) => m.tipo === 'diferencia')).toBe(false);
    expect(b.saldo_final).toBe(1_000_000);
    expect(b.diferencia).toBe(-1_000_000);
  });

  it('no mezcla monedas: un bloque por moneda, guaraníes primero', async () => {
    const { s } = crear({
      cuentas: [
        cuota({ id: 'usd', compra_id: 'k-usd', origen_tipo: 'compra', origen_id: null, compra_cab: compra1549, moneda: { codigo: 'USD' }, monto_original: 500, saldo_pendiente: 500 }),
        cuota({ id: 'pyg', monto_original: 9_167_000, saldo_pendiente: 9_167_000 }),
      ],
    });
    const ex = await s.extracto('emp', 'prov', undefined, { desde: '2025-01-01' });
    expect(ex.monedas.map((b) => [b.moneda, b.saldo_final])).toEqual([
      ['PYG', 9_167_000],
      ['USD', 500],
    ]);
  });

  it('las compensaciones en recibos de cobro bajan la factura compensada', async () => {
    const { s } = crear({
      cuentas: [cuota({ id: 'c3', compra_id: 'k-1549', origen_tipo: 'compra', origen_id: null, compra_cab: compra1549, monto_original: 11_054_200, saldo_pendiente: 10_054_200 })],
      compensaciones: [{ compra_cab_id: 'k-1549', monto: 1_000_000, recibo: { numero_recibo: '001-001-0000487', fecha_emision: new Date('2026-05-12T15:00:00Z'), fecha_registro: null } }],
    });
    const b = (await s.extracto('emp', 'prov', undefined, { desde: '2026-01-01' })).monedas[0];
    expect(b.movimientos.at(-1)).toEqual(expect.objectContaining({ tipo: 'compensacion', fecha: '2026-05-12', credito: 1_000_000, saldo_documento: 10_054_200 }));
    expect(b.diferencia).toBe(0);
  });

  it('un gasto de contado se cancela el mismo día aunque no tenga fila de pago', async () => {
    const { s } = crear({
      cuentas: [cuota({ id: 'c374', origen_id: 'g-374', monto_original: 2_896_667, monto_pagado: 2_896_667, saldo_pendiente: 0 })],
      gastos: [{ id: 'g-374', numero_factura: '0000374', fecha_emision: d('2026-05-05'), sucursal_id: 's', descripcion: '', forma_pago: 'CONTADO', medio_pago: { descripcion: 'Efectivo' }, gasto_det: [] }],
    });
    const b = (await s.extracto('emp', 'prov', undefined, { desde: '2026-01-01' })).monedas[0];
    expect(b.movimientos.map((m) => [m.tipo, m.debito, m.credito, m.saldo])).toEqual([
      ['factura', 2_896_667, 0, 2_896_667],
      ['pago', 0, 2_896_667, 0],
    ]);
    expect(b.movimientos[1].concepto).toBe('Pagado al registrar el gasto (contado) · Efectivo');
    expect(b.diferencia).toBe(0);
  });

  it('un contado que se mandó a orden de pago no se cuenta dos veces', async () => {
    const { s } = crear({
      cuentas: [cuota({ id: 'c1', origen_id: 'g-1', monto_original: 500_000, monto_pagado: 500_000, saldo_pendiente: 0 })],
      gastos: [{ id: 'g-1', numero_factura: '0000001', fecha_emision: d('2026-05-05'), sucursal_id: 's', descripcion: '', forma_pago: 'CONTADO', medio_pago: null, gasto_det: [] }],
      pagos: [{ cuenta_pagar_id: 'c1', fecha_pago: d('2026-05-10'), numero_recibo: 'OP-9', monto: 500_000, referencia: null, numero_cheque: null, medio_pago: { descripcion: 'Transferencia' } }],
    });
    const b = (await s.extracto('emp', 'prov', undefined, { desde: '2026-01-01' })).monedas[0];
    expect(b.movimientos.filter((m) => m.tipo === 'pago').map((m) => m.comprobante)).toEqual(['OP-9']);
    expect(b.saldo_final).toBe(0);
  });

  it('marca como posible duplicado la misma factura cargada como gasto y como compra', async () => {
    const compra1583 = { ...compra1549, numero_factura: '0001583', punto_expedicion: '001', establecimiento: '002' };
    const { s } = crear({
      cuentas: [
        cuota({ id: 'gasto', origen_id: 'g-1583', monto_original: 18_860_137, saldo_pendiente: 18_860_137 }),
        cuota({ id: 'compra', compra_id: 'k-1583', origen_tipo: 'compra', origen_id: null, compra_cab: compra1583, monto_original: 18_860_137, saldo_pendiente: 18_860_137 }),
        cuota({ id: 'otra', monto_original: 9_167_000, saldo_pendiente: 9_167_000 }),
      ],
      gastos: [{ id: 'g-1583', numero_factura: '0001583', fecha_emision: d('2026-05-29'), sucursal_id: 's', descripcion: '', forma_pago: 'CREDITO', medio_pago: null, gasto_det: [] }],
    });
    const b = (await s.extracto('emp', 'prov', undefined, { desde: '2025-01-01' })).monedas[0];
    expect(b.movimientos.filter((m) => m.posible_duplicado).map((m) => m.comprobante).sort()).toEqual(['0001583', '002-001-0001583']);
    expect(b.movimientos.find((m) => m.comprobante === '001-001-0000356')?.posible_duplicado).toBeUndefined();
  });

  it('valida el rango y el proveedor', async () => {
    await expect(crear().s.extracto('emp', 'prov', undefined, { desde: '2026-05-01', hasta: '2026-04-01' })).rejects.toBeInstanceOf(BadRequestException);
    await expect(crear({ proveedor: null }).s.extracto('emp', 'x', undefined)).rejects.toBeInstanceOf(NotFoundException);
  });

  it('el Excel trae el saldo anterior, cada movimiento y el saldo final', async () => {
    const { s } = crear({ cuentas: [cuota({ id: 'c1', monto_original: 9_167_000, saldo_pendiente: 9_167_000 })] });
    const wb = XLSX.read(await s.extractoXlsx('emp', 'prov', undefined, { desde: '2025-01-01' }));
    const filas = XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets.Extracto, { header: 1, defval: '' });

    expect(filas.find((r) => r[0] === 'Fecha')).toEqual(['Fecha', 'Comprobante', 'Tipo', 'Aplica a', 'Concepto', 'Débito', 'Crédito', 'Saldo factura', 'Saldo']);
    expect(filas.find((r) => r[1] === '001-001-0000356')).toEqual(['31/12/2025', '001-001-0000356', 'Factura', '', 'HONORARIOS', 9_167_000, '', 9_167_000, 9_167_000]);
    expect(filas.find((r) => String(r[4]).startsWith('Saldo al'))?.[8]).toBe(9_167_000);
  });
});
