import { crearIntegracionMock } from './__helpers__/integracion-mock';

// Apertura por ítem (spec 2026-10-13 "reglas de cuenta por dimensión", §5.2):
// con reglas de ítem en un concepto de ventas/costo, la línea de ese concepto se
// abre en una por cuenta resultante y la suma da el total de la tasa. Sin reglas
// de ítem no se leen los ítems: el asiento es el de hoy.

const lineas = (s: any) => s.asientos.crearConfirmado.mock.calls[0][3].lineas;
const deDescripcion = (s: any, d: string) => lineas(s).filter((l: any) => l.descripcion === d);
const suma = (ls: any[], campo: 'debe_moneda' | 'haber_moneda') => ls.reduce((a, l) => a + l[campo], 0);

const regla = (concepto: string, cuenta_id: string, dimension: string, valores: string[], id = 'r1') => ({
  id, concepto, orden: 1, modo: 'CUENTA', cuenta_id, estado: 'ACTIVA',
  vigente_desde: new Date('2026-01-01'), vigente_hasta: null, condiciones: [{ dimension, operador: 'EN', valores }],
}) as any;

// Árbol de categorías: "hija" es subcategoría de "padre"; "otra" va aparte.
const categorias = { findMany: jest.fn().mockResolvedValue([
  { id: 'cat-padre', padre_id: null }, { id: 'cat-hija', padre_id: 'cat-padre' }, { id: 'cat-otra', padre_id: null },
]) };
const producto = (categoria_id: string | null, tipo: string | null = 'PRODUCTO') =>
  ({ tipo, rubro_id: null, categoria_id, categoria: categoria_id ? { rubro_id: null } : null });
const itemVenta = (tasa: number, base: number, categoria_id: string | null, over: any = {}) => ({
  dtasiva: tasa, iafeciva: tasa ? 1 : 3, dbasgraviva: tasa ? base : 0, dbasexe: 0, dtotopeitem: tasa ? base * (1 + tasa / 100) : base,
  productos: producto(categoria_id), ...over,
});

const facturaVenta = (over: any = {}) => ({
  id: 'f1', empresa_id: 'emp', estado: 'Aprobado', cliente_id: 'cli1', dest: '001', dpunexp: '001', dnumdoc: '0000010',
  dfeemide: new Date('2026-10-13'), cobro_diferido: false, condicion_operacion: { codigo: 2 }, moneda: { codigo: 'PYG' }, dticam: 1,
  factura_subtotales: [{ dtotgralope: 1100, diva10: 100, diva5: 0, dsub10: 1100, dsub5: 0, dsubexe: 0, dsubexo: 0 }],
  factura_forma_pagos: [], ...over,
});
const itemsFactura = [itemVenta(10, 600, 'cat-hija'), itemVenta(10, 300, 'cat-otra'), itemVenta(10, 100, 'cat-padre')];
const baseVenta = (f: any, items: any[] = itemsFactura, extra: any = {}) => ({
  factura_cab: { findUnique: jest.fn().mockResolvedValue(f) },
  factura_det: { findMany: jest.fn().mockResolvedValue(items) },
  inm_cobros: { findFirst: jest.fn().mockResolvedValue(null) },
  categorias,
  ...extra,
});

describe('factura de venta', () => {
  it('regla CATEGORIA_PRODUCTO EN [padre] en VENTAS_10: dos líneas (regla y defecto) que suman base10, con su traza', async () => {
    const s = crearIntegracionMock(baseVenta(facturaVenta()), [], {}, [regla('VENTAS_10', 'cta-VENTAS_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre'])]);
    await s.integrarFacturaVenta('f1');
    const ventas = deDescripcion(s, 'Ventas 10%');
    expect(ventas.map((l: any) => [l.cuenta_id, l.haber_moneda, l.mapeo_concepto, l.mapeo_nivel, l.mapeo_regla_id])).toEqual([
      ['cta-VENTAS_PADRE', 700, 'VENTAS_10', 'REGLA', 'r1'],
      ['cta-VENTAS_10', 300, 'VENTAS_10', 'DEFECTO', undefined],
    ]);
    expect(suma(ventas, 'haber_moneda')).toBe(1000);
    expect(s.prisma.factura_det.findMany.mock.calls[0][0].where).toEqual({ factura_cab_id: 'f1' });
    // El resto del asiento no cambia.
    expect(deDescripcion(s, 'Clientes')[0]).toMatchObject({ cuenta_id: 'cta-CLIENTES', debe_moneda: 1100 });
    expect(deDescripcion(s, 'IVA Débito 10%')[0]).toMatchObject({ cuenta_id: 'cta-IVA_DEBITO_10', haber_moneda: 100 });
    expect(lineas(s).some((l: any) => 'dimsItem' in l || 'concepto' in l)).toBe(false);
  });

  it('sin reglas de ítem no se consultan los ítems y sale la línea única de hoy (aun con una regla de cabecera)', async () => {
    for (const reglas of [[], [regla('VENTAS_10', 'cta-VENTAS_PYG', 'MONEDA', ['PYG'])]]) {
      const s = crearIntegracionMock(baseVenta(facturaVenta()), [], {}, reglas);
      await s.integrarFacturaVenta('f1');
      expect(s.prisma.factura_det.findMany).not.toHaveBeenCalled();
      expect(deDescripcion(s, 'Ventas 10%').map((l: any) => [l.cuenta_id, l.haber_moneda])).toEqual([
        [reglas.length ? 'cta-VENTAS_PYG' : 'cta-VENTAS_10', 1000],
      ]);
    }
  });

  it('factura dentro de la vigencia de una regla de ítem ya retirada: se abre igual (rehacer asientos)', async () => {
    const retirada = { ...regla('VENTAS_10', 'cta-VENTAS_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre']), estado: 'RETIRADA', vigente_hasta: new Date('2026-11-01') };
    const s = crearIntegracionMock(baseVenta(facturaVenta()), [], {}, [retirada]);
    await s.integrarFacturaVenta('f1');
    expect(deDescripcion(s, 'Ventas 10%').map((l: any) => [l.cuenta_id, l.haber_moneda, l.mapeo_nivel])).toEqual([
      ['cta-VENTAS_PADRE', 700, 'REGLA'], ['cta-VENTAS_10', 300, 'DEFECTO'],
    ]);
  });

  it('ítem sin producto o sin categoría va a la cuenta por defecto', async () => {
    const items = [itemVenta(10, 700, 'cat-hija'), itemVenta(10, 200, null), itemVenta(10, 100, null, { productos: null })];
    const s = crearIntegracionMock(baseVenta(facturaVenta(), items), [], {}, [regla('VENTAS_10', 'cta-VENTAS_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre'])]);
    await s.integrarFacturaVenta('f1');
    expect(deDescripcion(s, 'Ventas 10%').map((l: any) => [l.cuenta_id, l.haber_moneda])).toEqual([
      ['cta-VENTAS_PADRE', 700], ['cta-VENTAS_10', 300],
    ]);
  });

  it('5% y exentas se abren cada una con los ítems de su tasa (exento con dtotopeitem)', async () => {
    const f = facturaVenta({ factura_subtotales: [{ dtotgralope: 1400, diva10: 100, diva5: 10, dsub10: 1100, dsub5: 210, dsubexe: 90, dsubexo: 0 }] });
    const items = [
      itemVenta(10, 1000, 'cat-otra'),
      itemVenta(5, 150, 'cat-hija'), itemVenta(5, 50, 'cat-otra'),
      itemVenta(0, 60, 'cat-hija'), itemVenta(0, 30, 'cat-otra'),
    ];
    const s = crearIntegracionMock(baseVenta(f, items), [], {}, [
      regla('VENTAS_5', 'cta-V5_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre']),
      regla('VENTAS_EXENTAS', 'cta-VEX_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre'], 'r2'),
    ]);
    await s.integrarFacturaVenta('f1');
    expect(deDescripcion(s, 'Ventas 10%').map((l: any) => [l.cuenta_id, l.haber_moneda])).toEqual([['cta-VENTAS_10', 1000]]);
    expect(deDescripcion(s, 'Ventas 5%').map((l: any) => [l.cuenta_id, l.haber_moneda])).toEqual([['cta-V5_PADRE', 150], ['cta-VENTAS_5', 50]]);
    expect(deDescripcion(s, 'Ventas exentas').map((l: any) => [l.cuenta_id, l.haber_moneda, l.mapeo_regla_id])).toEqual([
      ['cta-VEX_PADRE', 60, 'r2'], ['cta-VENTAS_EXENTAS', 30, undefined],
    ]);
  });

  it('venta inmobiliaria: las reglas se buscan en el concepto efectivo INM_* y todas las líneas llevan el centro de costo', async () => {
    const f = facturaVenta({ factura_subtotales: [{ dtotgralope: 210, diva10: 0, diva5: 10, dsub10: 0, dsub5: 210, dsubexe: 0, dsubexo: 0 }] });
    const s = crearIntegracionMock(baseVenta(f, [itemVenta(5, 120, 'cat-hija'), itemVenta(5, 80, 'cat-otra')], {
      inm_cobros: { findFirst: jest.fn().mockResolvedValue({ inm_operaciones: { inm_proyectos: { centro_costo_id: 'cc-1' } } }) },
      cont_mapeo_cuentas: { findUnique: jest.fn().mockImplementation(async ({ where }: any) =>
        (where.empresa_id_concepto.concepto.startsWith('INM_') ? { id: 'm' } : null)) },
    }), [], {}, [regla('INM_VENTAS_5', 'cta-INM_V5_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre'])]);
    await s.integrarFacturaVenta('f1');
    expect(deDescripcion(s, 'Ventas 5%').map((l: any) => [l.cuenta_id, l.haber_moneda, l.centro_costo_id, l.mapeo_concepto])).toEqual([
      ['cta-INM_V5_PADRE', 120, 'cc-1', 'INM_VENTAS_5'], ['cta-INM_VENTAS_5', 80, 'cc-1', 'INM_VENTAS_5'],
    ]);
  });

  it('[RF-4] venta en USD: la suma de las líneas abiertas en moneda es igual a base10 en moneda', async () => {
    const f = facturaVenta({
      moneda: { codigo: 'USD' }, dticam: 7312.37,
      factura_subtotales: [{ dtotgralope: 110.57, diva10: 10.05, diva5: 0, dsub10: 110.57, dsub5: 0, dsubexe: 0, dsubexo: 0 }],
    });
    const items = [itemVenta(10, 33.51, 'cat-hija'), itemVenta(10, 33.5, 'cat-otra'), itemVenta(10, 33.51, 'cat-padre')];
    const s = crearIntegracionMock(baseVenta(f, items), [], {}, [regla('VENTAS_10', 'cta-VENTAS_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre'])]);
    await s.integrarFacturaVenta('f1');
    const base10 = (110.57 - 10.05) * 7312.37;
    const ventas = deDescripcion(s, 'Ventas 10%');
    expect(ventas).toHaveLength(2);
    expect(suma(ventas, 'haber_moneda')).toBeCloseTo(base10, 8);
    // La línea menor va a dos decimales; la mayor se lleva el resto (base10 trae más decimales, como hoy).
    const menor = [...ventas].sort((a: any, b: any) => a.haber_moneda - b.haber_moneda)[0];
    expect(Math.abs(menor.haber_moneda * 100 - Math.round(menor.haber_moneda * 100))).toBeLessThan(1e-6);
  });
});

describe('NC de venta', () => {
  it('se abre con sus propios ítems (al debe)', async () => {
    const nc = { id: 'nc1', empresa_id: 'emp', estado: 'Aprobado', cliente_id: 'cli1', dest: '001', dpunexp: '001', dnumdoc: '1',
      dfeemide: new Date('2026-10-13'), moneda: { codigo: 'PYG' }, dticam: 1,
      nota_credito_subtotal: [{ dtotgralope: 550, diva10: 50, diva5: 0, dsub10: 550, dsub5: 0, dsubexe: 0, dsubexo: 0 }] };
    const s = crearIntegracionMock({
      nota_credito_cab: { findUnique: jest.fn().mockResolvedValue(nc) },
      nota_credito_det: { findMany: jest.fn().mockResolvedValue([itemVenta(10, 400, 'cat-hija'), itemVenta(10, 100, 'cat-otra')]) },
      categorias,
    }, [], {}, [regla('VENTAS_10', 'cta-VENTAS_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre'])]);
    await s.integrarNotaCredito('nc1');
    expect(s.prisma.nota_credito_det.findMany.mock.calls[0][0].where).toEqual({ nota_credito_cab_id: 'nc1' });
    expect(deDescripcion(s, 'Devolución ventas 10%').map((l: any) => [l.cuenta_id, l.debe_moneda, l.haber_moneda, l.mapeo_nivel])).toEqual([
      ['cta-VENTAS_PADRE', 400, 0, 'REGLA'], ['cta-VENTAS_10', 100, 0, 'DEFECTO'],
    ]);
  });
});

describe('compras', () => {
  const compra = { id: 'c1', empresa_id: 'emp', sucursal_id: 'suc-1', total: 1650, total_iva_10: 100, total_iva_5: 50,
    moneda: { codigo: 'PYG' }, cotizacion: null, fecha_emision: new Date('2026-10-13'), proveedor_id: 'p1', condicion_operacion: { codigo: 2 } };
  const itemCompra = (porcentaje_iva: number, neto: number, p: any) => ({
    porcentaje_iva, subtotal: neto * (1 + porcentaje_iva / 100), monto_iva: (neto * porcentaje_iva) / 100, productos: p,
  });
  const itemsCompra = [itemCompra(10, 800, producto('cat-otra', 'SERVICIO')), itemCompra(10, 200, producto('cat-otra')), itemCompra(5, 500, producto(null, 'SERVICIO'))];
  const baseCompra = (extra: any = {}) => ({
    compra_cab: { findUnique: jest.fn().mockResolvedValue(compra) },
    compra_det: { findMany: jest.fn().mockResolvedValue(itemsCompra) },
    config_compras: { findUnique: jest.fn().mockResolvedValue(null) },
    proveedores: { findFirst: jest.fn().mockResolvedValue({ tipo_entidad: 'PROVEEDOR_LOCAL', cuenta_contable_id: null }) },
    categorias,
    ...extra,
  });

  it('compra con regla TIPO_ITEM en INVENTARIO: una línea por cuenta que suma el neto', async () => {
    const s = crearIntegracionMock(baseCompra(), [], {}, [regla('INVENTARIO', 'cta-GASTO_SERV', 'TIPO_ITEM', ['SERVICIO'])]);
    await s.integrarFacturaCompra('c1');
    const costo = deDescripcion(s, 'Inventario/Gasto');
    expect(costo.map((l: any) => [l.cuenta_id, l.debe_moneda, l.mapeo_concepto, l.mapeo_nivel])).toEqual([
      ['cta-GASTO_SERV', 1300, 'INVENTARIO', 'REGLA'], ['cta-INVENTARIO', 200, 'INVENTARIO', 'DEFECTO'],
    ]);
    expect(s.prisma.compra_det.findMany.mock.calls[0][0].where).toEqual({ compra_id: 'c1' });
  });

  it('compra sin reglas de ítem: no lee los ítems', async () => {
    const s = crearIntegracionMock(baseCompra());
    await s.integrarFacturaCompra('c1');
    expect(s.prisma.compra_det.findMany).not.toHaveBeenCalled();
    expect(deDescripcion(s, 'Inventario/Gasto').map((l: any) => [l.cuenta_id, l.debe_moneda])).toEqual([['cta-INVENTARIO', 1500]]);
  });

  it('compra de importación: la apertura va sobre IMPORTACION_COMPRA', async () => {
    const s = crearIntegracionMock(baseCompra({ imp_componentes_costo: { findFirst: jest.fn().mockResolvedValue({ id: 'comp' }) } }), [], {}, [
      regla('IMPORTACION_COMPRA', 'cta-IMP_SERV', 'TIPO_ITEM', ['SERVICIO']),
    ]);
    await s.integrarFacturaCompra('c1');
    expect(deDescripcion(s, 'Inventario/Gasto').map((l: any) => [l.cuenta_id, l.debe_moneda, l.mapeo_concepto])).toEqual([
      ['cta-IMP_SERV', 1300, 'IMPORTACION_COMPRA'], ['cta-IMPORTACION_COMPRA', 200, 'IMPORTACION_COMPRA'],
    ]);
  });

  it('NC de compra: se abre con sus ítems y publica la SUCURSAL de su compra', async () => {
    const nc = { id: 'ncc1', empresa_id: 'emp', proveedor_id: 'p1', compra_id: 'c1', compra_cab: { sucursal_id: 'suc-1' },
      moneda: { codigo: 'PYG' }, cotizacion: null, fecha_emision: new Date('2026-10-13'), numero: '1', total: 1100, total_iva_10: 100, total_iva_5: 0 };
    const s = crearIntegracionMock({
      nota_credito_compra_cab: { findUnique: jest.fn().mockResolvedValue(nc) },
      nota_credito_compra_det: { findMany: jest.fn().mockResolvedValue([itemCompra(10, 600, producto('cat-hija')), itemCompra(10, 400, producto('cat-otra'))]) },
      proveedores: { findFirst: jest.fn().mockResolvedValue({ tipo_entidad: 'PROVEEDOR_LOCAL', cuenta_contable_id: null }) },
      categorias,
    }, [], {}, [
      regla('INVENTARIO', 'cta-INV_PADRE', 'CATEGORIA_PRODUCTO', ['cat-padre']),
      { ...regla('IVA_CREDITO_10', 'cta-IVA_SUC1', 'SUCURSAL', ['suc-1']), id: 'r2' },
    ]);
    await s.integrarNotaCreditoCompra('ncc1');
    expect(s.prisma.nota_credito_compra_det.findMany.mock.calls[0][0].where).toEqual({ nota_credito_compra_id: 'ncc1' });
    expect(deDescripcion(s, 'Reversión Inventario/Gasto').map((l: any) => [l.cuenta_id, l.haber_moneda, l.mapeo_nivel])).toEqual([
      ['cta-INV_PADRE', 600, 'REGLA'], ['cta-INVENTARIO', 400, 'DEFECTO'],
    ]);
    expect(s.mapeo.crearResolvedor.mock.calls[0][1].dimensiones.SUCURSAL).toBe('suc-1');
    expect(deDescripcion(s, 'Reversión IVA Crédito 10%')[0]).toMatchObject({ cuenta_id: 'cta-IVA_SUC1', mapeo_nivel: 'REGLA' });
  });
});
