import { SaludContableService } from './salud-contable.service';

/**
 * Los controles de salud son lo que el contador mira antes de cerrar el mes.
 * Estos casos son los desfases reales que tenía DOBA (2026-10-08).
 */
describe('SaludContableService', () => {
  const plan = [
    { id: 'banco', codigo: '1.01.01.06.01', descripcion: 'BANCO', active: true, acepta_movimientos: true },
    { id: 'caja', codigo: '1.01.01.01', descripcion: 'CAJA', active: true, acepta_movimientos: true },
    { id: 'recaud-ch', codigo: '1.01.01.03', descripcion: 'RECAUDACIONES CHEQUES', active: true, acepta_movimientos: true },
    { id: 'proveedores', codigo: '2.01.01.01', descripcion: 'PROVEEDORES', active: true, acepta_movimientos: true },
    { id: 'old-clientes', codigo: 'OLD-1.1.2.01', descripcion: 'Clientes', active: false, acepta_movimientos: true },
    { id: 'imp-curso', codigo: '1.01.04.11', descripcion: 'IMPORTACIONES EN CURSO', active: true, acepta_movimientos: true },
  ];

  const crear = (opciones: {
    saldos: Record<string, number>;
    tesCuentas?: any[];
    cheques?: { monto: number; count: number; viejos: number };
    mapeoCheques?: Array<{ concepto: string; cuenta_id: string }>;
    mapeoTransitorias?: Array<{ concepto: string; cuenta_id: string }>;
    embarques?: Array<{ id: string; numero: string; costo_total_gs: number }>;
    docsCierre?: Array<{ id: string; origen_id: string }>;
    asientosCierre?: Array<{ documento_id: string; total_haber_pyg: number }>;
    pendienteMapeo?: { frenando: Map<string, number>; sinConfirmar: number };
    reglasActivas?: any[];
    mapeosMedio?: any[];
  }) => {
    const prisma: any = {
      cont_asientos_det: {
        groupBy: jest.fn().mockResolvedValue(
          Object.entries(opciones.saldos).map(([cuenta_id, saldo]) => ({
            cuenta_id,
            _sum: { debe_pyg: saldo > 0 ? saldo : 0, haber_pyg: saldo < 0 ? -saldo : 0 },
          })),
        ),
      },
      cont_plan_cuentas: { findMany: jest.fn().mockResolvedValue(plan) },
      tes_cuentas: {
        findMany: jest.fn().mockImplementation(({ where }: any) =>
          where?.moneda ? [] : (opciones.tesCuentas ?? []),
        ),
      },
      cont_tipo_cambio: { findFirst: jest.fn().mockResolvedValue(null) },
      cont_mapeo_cuentas: {
        findMany: jest.fn().mockImplementation(({ where }: any) => {
          if (!where?.concepto) return [];
          return where.concepto.in.includes('IMPORTACIONES_EN_CURSO') ? (opciones.mapeoTransitorias ?? []) : (opciones.mapeoCheques ?? []);
        }),
      },
      cont_mapeo_reglas: {
        findMany: jest.fn().mockImplementation(({ where }: any) =>
          (opciones.reglasActivas ?? []).filter((r) => r.empresa_id === where.empresa_id && r.estado === where.estado),
        ),
      },
      cont_mapeo_medio_pago: { findMany: jest.fn().mockResolvedValue(opciones.mapeosMedio ?? []) },
      imp_embarques: { findMany: jest.fn().mockResolvedValue(opciones.embarques ?? []) },
      cont_documentos: { findMany: jest.fn().mockResolvedValue(opciones.docsCierre ?? []) },
      cont_asientos: { findMany: jest.fn().mockResolvedValue(opciones.asientosCierre ?? []) },
      tes_cheques: {
        aggregate: jest.fn().mockResolvedValue({ _sum: { monto: opciones.cheques?.monto ?? 0 }, _count: opciones.cheques?.count ?? 0 }),
        count: jest.fn().mockResolvedValue(opciones.cheques?.viejos ?? 0),
      },
      $queryRaw: jest.fn().mockResolvedValue([]),
    };
    const conciliacion: any = {
      resumen: jest.fn().mockResolvedValue([]),
      listarRegistry: jest.fn().mockResolvedValue([]),
      fechaActivacionContabilidad: jest.fn().mockResolvedValue(new Date('2026-05-18')),
    };
    const avisos: any = {
      resumenPendiente: jest.fn().mockResolvedValue(opciones.pendienteMapeo ?? { frenando: new Map(), sinConfirmar: 0 }),
    };
    return new SaludContableService(prisma, conciliacion, avisos);
  };

  const control = (r: { controles: any[] }, clave: string) => r.controles.find((c) => c.clave === clave);

  it('un banco que no coincide con su cuenta contable es un error, con la diferencia', async () => {
    const service = crear({
      saldos: { banco: 65_208_363 },
      tesCuentas: [{ id: 't1', nombre: 'DOBA S.A.', moneda: 'PYG', saldo_actual: 233_188_813, cuenta_contable_id: 'banco' }],
    });
    const r = await service.evaluar('emp-1');
    const c = control(r, 'tesoreria_vs_contable');
    expect(c.estado).toBe('ERROR');
    expect(c.items[0].monto).toBe(167_980_450);
  });

  it('una cuenta de tesorería sin cuenta contable también es un error', async () => {
    const service = crear({
      saldos: {},
      tesCuentas: [{ id: 't2', nombre: 'Caja chica', moneda: 'PYG', saldo_actual: 0, cuenta_contable_id: null }],
    });
    const c = control(await service.evaluar('emp-1'), 'tesoreria_vs_contable');
    expect(c.estado).toBe('ERROR');
    expect(c.items[0].texto).toMatch(/no tiene cuenta contable/);
  });

  it('caja con saldo negativo es un error; un pasivo con saldo acreedor no', async () => {
    const service = crear({ saldos: { caja: -198_000_000, proveedores: -50_000_000, banco: 10 } });
    const c = control(await service.evaluar('emp-1'), 'activos_negativos');
    expect(c.estado).toBe('ERROR');
    expect(c.items.map((i: any) => i.ref)).toEqual(['caja']);
  });

  it('un medio de pago mapeado a una cuenta desactivada es un error del control de mapeo, con dónde se corrige', async () => {
    const roto = { medio_pago_id: 'mp', flujo: 'COBRO', cuenta_contable_id: 'c', medio_pago: { codigo: 5, descripcion: 'Transferencia' },
      cuenta: { codigo: '1.01.02', descripcion: 'Banco viejo', active: false, acepta_movimientos: true } };
    const c = control(await crear({ saldos: {}, mapeosMedio: [roto] }).evaluar('emp-1'), 'mapeo');
    expect(c.estado).toBe('ERROR');
    expect(c.items).toEqual([{ texto: 'Medio Transferencia (al cobrar): apunta a 1.01.02 Banco viejo, que está desactivada' }]);
    expect(c.como_resolver).toContain('Cuentas por medio de pago');
    expect(c.ruta).toBe('/contabilidad?tab=mapeo');

    const sano = control(await crear({ saldos: {}, mapeosMedio: [{ ...roto, cuenta: { ...roto.cuenta, active: true } }] }).evaluar('emp-1'), 'mapeo');
    expect(sano.estado).toBe('OK');
  });

  it('los documentos frenados por un medio de pago se nombran legibles', async () => {
    const c = control(
      await crear({ saldos: {}, pendienteMapeo: { frenando: new Map([['MEDIO_PAGO_PAGO', 2]]), sinConfirmar: 0 } }).evaluar('emp-1'),
      'mapeo_pendiente',
    );
    expect(c.items[0].texto).toBe('cuentas por medio de pago (al pagar): 2 documentos sin asiento');
  });

  it('una cuenta desactivada con saldo se avisa: esos montos no salen en los reportes', async () => {
    const service = crear({ saldos: { 'old-clientes': 302_285_832 } });
    const c = control(await service.evaluar('emp-1'), 'cuentas_inactivas');
    expect(c.estado).toBe('ALERTA');
    expect(c.items[0].texto).toMatch(/OLD-1.1.2.01.*desactivada/);
  });

  it('cartera de cheques: avisa diferencia, cheques viejos sin depositar y conceptos en cuentas distintas', async () => {
    const service = crear({
      saldos: { 'recaud-ch': -147_372_220 },
      cheques: { monto: 612_668_787, count: 104, viejos: 68 },
      mapeoCheques: [
        { concepto: 'CHEQUES_EN_CARTERA', cuenta_id: 'recaud-ch' },
        { concepto: 'CHEQUES_EN_TRANSITO', cuenta_id: 'otra' },
      ],
    });
    const c = control(await service.evaluar('emp-1'), 'cartera_cheques');
    expect(c.estado).toBe('ALERTA');
    expect(c.items.map((i: any) => i.texto).join(' ')).toMatch(/cuentas distintas.*68 cheques vencidos/s);
  });

  it('si un control falla, los demás se calculan igual', async () => {
    const service = crear({ saldos: {} });
    (service as any).prisma.$queryRaw.mockRejectedValue(new Error('timeout'));
    const r = await service.evaluar('emp-1');
    expect(r.controles).toHaveLength(13);
    expect(control(r, 'anulados_con_asiento').resumen).toMatch(/No se pudo evaluar/);
    expect(control(r, 'activos_negativos').estado).toBe('OK');
  });
  it('la cuenta puente de importaciones (fuera de 1.01.01/1.01.03) en negativo también es un error', async () => {
    const service = crear({
      saldos: { 'imp-curso': -507_271_894 },
      mapeoTransitorias: [{ concepto: 'IMPORTACIONES_EN_CURSO', cuenta_id: 'imp-curso' }],
    });
    const c = control(await service.evaluar('emp-1'), 'activos_negativos');
    expect(c.estado).toBe('ERROR');
    expect(c.items.map((i: any) => i.ref)).toEqual(['imp-curso']);
    expect(c.como_resolver).toMatch(/cuenta transitoria de activo no puede quedar en negativo/);
  });

  it('sin mapear la puente, una cuenta 1.01.04 en negativo no entra en el control', async () => {
    const service = crear({ saldos: { 'imp-curso': -507_271_894 } });
    const c = control(await service.evaluar('emp-1'), 'activos_negativos');
    expect(c.estado).toBe('OK');
  });

  it('importación cerrada cuyo traslado difiere del costo: alerta con la diferencia', async () => {
    const service = crear({
      saldos: {},
      embarques: [
        { id: 'e1', numero: 'IMP-001', costo_total_gs: 1_000_000 },
        { id: 'e2', numero: 'IMP-002', costo_total_gs: 500_000 },
      ],
      docsCierre: [
        { id: 'd1', origen_id: 'e1' },
        { id: 'd2', origen_id: 'e2' },
      ],
      asientosCierre: [
        { documento_id: 'd1', total_haber_pyg: 600_000 },
        { documento_id: 'd2', total_haber_pyg: 500_000.5 },
      ],
    });
    const c = control(await service.evaluar('emp-1'), 'importacion_cierre_diferencia');
    expect(c.estado).toBe('ALERTA');
    expect(c.items).toHaveLength(1);
    expect(c.items[0].ref).toBe('e1');
    expect(c.items[0].monto).toBe(-400_000);
    expect(c.items[0].texto).toMatch(/IMP-001/);
    expect(c.como_resolver).toMatch(/Mercaderías/);
  });

  it('conceptos sin mapear que frenan documentos: error con ruta a Mapeo', async () => {
    const service = crear({
      saldos: {},
      pendienteMapeo: { frenando: new Map([['IMPORTACION_COMPRA', 3], ['INVENTARIO', 1]]), sinConfirmar: 5 },
    });
    const c = control(await service.evaluar('emp-1'), 'mapeo_pendiente');
    expect(c.estado).toBe('ERROR');
    expect(c.ruta).toBe('/contabilidad?tab=mapeo');
    expect(c.items.map((i: any) => i.texto).join(' ')).toMatch(/IMPORTACION_COMPRA.*3 documentos.*INVENTARIO.*1 documento/s);
  });

  it('solo mapeos sin confirmar: alerta; nada pendiente: OK', async () => {
    const alerta = control(
      await crear({ saldos: {}, pendienteMapeo: { frenando: new Map(), sinConfirmar: 4 } }).evaluar('emp-1'),
      'mapeo_pendiente',
    );
    expect(alerta.estado).toBe('ALERTA');
    expect(alerta.resumen).toMatch(/4/);
    expect(control(await crear({ saldos: {} }).evaluar('emp-1'), 'mapeo_pendiente').estado).toBe('OK');
  });

  it('una regla ACTIVA con cuenta desactivada o de agrupación es ERROR en mapeo pendiente, con ruta a Mapeo', async () => {
    const regla = (id: string, orden: number, cuenta: any, extra: any = {}) => ({
      id, empresa_id: 'emp-1', concepto: 'VENTAS_10', orden, estado: 'ACTIVA', modo: 'CUENTA', cuenta_id: cuenta?.id ?? null, cuenta, ...extra,
    });
    const service = crear({
      saldos: {},
      reglasActivas: [
        regla('r1', 1, { id: 'old', codigo: 'OLD-4.1', active: false, acepta_movimientos: true, empresa_id: 'emp-1' }),
        regla('r2', 2, { id: 'grupo', codigo: '4.1', active: true, acepta_movimientos: false, empresa_id: null }),
        regla('r3', 3, { id: 'ok', codigo: '4.1.1.02', active: true, acepta_movimientos: true, empresa_id: 'emp-1' }),
        regla('r4', 1, null, { concepto: 'IMPORTACION_GASTO', modo: 'SUMAR_A_PRINCIPAL' }),
        regla('r5', 4, { id: 'ajena', codigo: '9.9', active: true, acepta_movimientos: true, empresa_id: 'emp-2' }),
        regla('r6', 1, { id: 'old', codigo: 'OLD-4.1', active: false, acepta_movimientos: true }, { empresa_id: 'emp-2' }),
      ],
    });
    const c = control(await service.evaluar('emp-1'), 'mapeo_pendiente');
    expect(c.estado).toBe('ERROR');
    expect(c.ruta).toBe('/contabilidad?tab=mapeo');
    const textos = c.items.map((i: any) => i.texto);
    expect(textos).toEqual([
      expect.stringMatching(/VENTAS_10.*regla n\.º 1.*OLD-4\.1.*desactivada/),
      expect.stringMatching(/VENTAS_10.*regla n\.º 2.*4\.1.*agrupación/),
      expect.stringMatching(/VENTAS_10.*regla n\.º 4.*no es de la empresa/),
    ]);
    expect(c.items.map((i: any) => i.ref)).toEqual(['r1', 'r2', 'r5']);
    expect(c.resumen).toMatch(/3 reglas activas/);
    // [I4] Una regla activa no se edita: el arreglo es retirarla y duplicarla con otra cuenta.
    expect(c.como_resolver).toMatch(/retirala y duplicala con otra cuenta/);
    expect(c.como_resolver).not.toMatch(/(?<!no )se edita/);
  });

  describe('categorías de tesorería con la cuenta global', () => {
    const sqlDe = (call: any[]) => (call[0] as string[]).join('?');
    const conFilas = (filas: any[]) => {
      const service = crear({ saldos: {} });
      (service as any).prisma.$queryRaw.mockImplementation((strings: string[]) =>
        Promise.resolve(strings.join('?').includes('tes_categoria_cuenta_contable') ? filas : []),
      );
      return service;
    };

    it('lista cada categoría con la cuenta global usada y la cantidad de movimientos, con ruta a Categorías de Movimiento', async () => {
      const service = conFilas([
        { categoria: 'Comisiones bancarias', codigo: '5.2.01', descripcion: 'GASTOS BANCARIOS', cuenta_de_la_empresa: true, cantidad: 7, monto: 150_000 },
        { categoria: 'Préstamo recibido', codigo: '2.1.05', descripcion: 'PRÉSTAMOS', cuenta_de_la_empresa: false, cantidad: 1, monto: 9_000_000 },
      ]);
      const c = control(await service.evaluar('emp-1'), 'categorias_cuenta_global');
      expect(c.estado).toBe('ALERTA');
      expect(c.resumen).toMatch(/2 categorías de tesorería.*8 movimientos/);
      expect(c.items.map((i: any) => i.texto)).toEqual([
        'Comisiones bancarias: cuenta global 5.2.01 GASTOS BANCARIOS en 7 movimientos',
        expect.stringMatching(/^Préstamo recibido: cuenta global 2\.1\.05 PRÉSTAMOS en 1 movimiento \(.*no es del plan de cuentas de esta empresa\)$/),
      ]);
      expect(c.items[0].monto).toBe(150_000);
      expect(c.ruta).toBe('/configuracion?seccion=tes-categorias-config');
      expect(c.como_resolver).toMatch(/Configuración → Tesorería y Bancos → Categorías de Movimiento/);
    });

    it('sin categorías con la cuenta global: OK', async () => {
      const c = control(await conFilas([]).evaluar('emp-1'), 'categorias_cuenta_global');
      expect(c.estado).toBe('OK');
      expect(c.items).toEqual([]);
    });

    it('una sola consulta filtrada por la empresa, con el criterio del integrador', async () => {
      const service = conFilas([]);
      await service.evaluar('emp-1');
      const llamadas = (service as any).prisma.$queryRaw.mock.calls.filter((c: any[]) => sqlDe(c).includes('tes_categoria_cuenta_contable'));
      expect(llamadas).toHaveLength(1);
      const [call] = llamadas;
      const sql = sqlDe(call);
      // Parámetros: la empresa (movimientos y mapeo de la categoría) y los orígenes que se asientan por su documento.
      expect(call.slice(1).filter((v: any) => v === 'emp-1').length).toBeGreaterThanOrEqual(2);
      expect(call.slice(1)).toContainEqual(expect.arrayContaining(['gasto_cab', 'TRANSFERENCIA_ORIGEN']));
      expect(sql).toMatch(/m\.estado = 'CONFIRMADO'/);
      expect(sql).toMatch(/d\.cuenta_contable_id IS NULL/);
      expect(sql).toMatch(/d\.concepto_contable IS NULL/);
      expect(sql).toMatch(/c\.cuenta_contable_id IS NOT NULL/);
      expect(sql).toMatch(/cc\.id IS NULL/);
      expect(sql).toMatch(/c\.empresa_id IS NULL/);
      expect(sql).toMatch(/tipo_pago_importacion/);
    });
  });
});
