import { BadRequestException, NotFoundException } from '@nestjs/common';
import { MapeoCuentasService } from './mapeo-cuentas.service';
import { ReglasMapeoService } from './reglas-mapeo.service';

/**
 * API de reglas de cuenta por dimensión (spec 2026-10-13 §6.1, §8): cada
 * decisión del contador queda con constancia y en audit_logs, una regla nunca
 * se borra si estuvo activa y nada sale de la empresa.
 */
const EMP = 'emp-1';
const OTRA = 'emp-2';
const USER = 'user-1';

type Regla = Record<string, any>;

const crear = (inicial: Regla[] = [], opciones: { cuentas?: Regla[]; mapeos?: string[]; asientosPosteriores?: number } = {}) => {
  const reglas: Regla[] = inicial.map((r) => ({
    modo: 'CUENTA',
    estado: 'BORRADOR',
    vigente_desde: new Date('2026-01-01'),
    vigente_hasta: null,
    descripcion: null,
    activado_at: null,
    activado_por: null,
    retirado_at: null,
    retirado_por: null,
    creado_por: USER,
    created_at: new Date('2026-10-01'),
    ...r,
  }));
  const cuentas: Regla[] = opciones.cuentas ?? [
    { id: 'c-ok', empresa_id: EMP, codigo: '4.1.1.02', descripcion: 'VENTAS EXTERIOR', active: true, acepta_movimientos: true },
    { id: 'c-baja', empresa_id: EMP, codigo: 'OLD-4.1', descripcion: 'VIEJA', active: false, acepta_movimientos: true },
    { id: 'c-grupo', empresa_id: EMP, codigo: '4.1', descripcion: 'VENTAS', active: true, acepta_movimientos: false },
    { id: 'c-otra', empresa_id: OTRA, codigo: '9.9', descripcion: 'AJENA', active: true, acepta_movimientos: true },
  ];
  const coincide = (r: Regla, where: Regla = {}) =>
    Object.entries(where).every(([k, v]) => {
      if (v && typeof v === 'object' && 'in' in v) return (v.in as unknown[]).includes(r[k]);
      if (v && typeof v === 'object' && 'not' in v) return r[k] !== v.not;
      return r[k] === v;
    });
  const conRelaciones = (r: Regla) => ({
    ...r,
    cuenta: cuentas.find((c) => c.id === r.cuenta_id) ?? null,
    creador: r.creado_por ? { nombres: 'Ana', apellidos: 'Contadora' } : null,
    activador: r.activado_por ? { nombres: 'Ana', apellidos: 'Contadora' } : null,
    retirador: r.retirado_por ? { nombres: 'Ana', apellidos: 'Contadora' } : null,
  });
  const prisma: any = {
    cont_mapeo_reglas: {
      findFirst: jest.fn(async ({ where }: any) => {
        const r = reglas.find((x) => coincide(x, where));
        return r ? conRelaciones(r) : null;
      }),
      findMany: jest.fn(async ({ where }: any = {}) =>
        reglas
          .filter((x) => coincide(x, where))
          .sort((a, b) => a.orden - b.orden)
          .map(conRelaciones),
      ),
      aggregate: jest.fn(async ({ where }: any) => ({
        _max: { orden: reglas.filter((x) => coincide(x, where)).reduce((m, x) => Math.max(m, x.orden), 0) || null },
      })),
      create: jest.fn(async ({ data }: any) => {
        const r = { id: `r-${reglas.length + 1}`, ...data };
        reglas.push(r);
        return conRelaciones(r);
      }),
      update: jest.fn(async ({ where, data }: any) => {
        const r = reglas.find((x) => x.id === where.id)!;
        Object.assign(r, data);
        return conRelaciones(r);
      }),
      delete: jest.fn(async ({ where }: any) => {
        const i = reglas.findIndex((x) => x.id === where.id);
        return reglas.splice(i, 1)[0];
      }),
    },
    cont_plan_cuentas: {
      findFirst: jest.fn(async ({ where }: any) => {
        const c = cuentas.find((x) => x.id === where.id);
        if (!c) return null;
        const empresas = (where.OR ?? []).map((o: any) => o.empresa_id);
        return empresas.length && !empresas.includes(c.empresa_id) ? null : c;
      }),
    },
    cont_mapeo_cuentas: {
      findFirst: jest.fn(async ({ where }: any) =>
        (opciones.mapeos ?? []).includes(where.concepto) ? { concepto: where.concepto } : null,
      ),
    },
    audit_logs: { create: jest.fn(async () => ({})) },
    cont_asientos: { count: jest.fn(async () => opciones.asientosPosteriores ?? 0) },
    $transaction: jest.fn(async (arg: any) => (typeof arg === 'function' ? arg(prisma) : Promise.all(arg))),
  };
  const avisos: any = { sincronizar: jest.fn(async () => undefined) };
  const mapeo = Object.create(MapeoCuentasService.prototype) as MapeoCuentasService;
  const service = new ReglasMapeoService(prisma, avisos, mapeo);
  return { service, prisma, avisos, reglas };
};

const condMoneda = [{ dimension: 'MONEDA', operador: 'EN', valores: ['USD'] }];
const audits = (prisma: any, action: string) =>
  prisma.audit_logs.create.mock.calls.map((c: any) => c[0].data).filter((d: any) => d.action === action);

describe('ReglasMapeoService.crear', () => {
  it('crea en BORRADOR con orden = último + 1, constancia de quién y auditoría', async () => {
    const { service, prisma, reglas } = crear([{ id: 'r-a', empresa_id: EMP, concepto: 'VENTAS_10', orden: 3, condiciones: condMoneda, cuenta_id: 'c-ok' }]);
    const r = await service.crear(EMP, USER, {
      concepto: 'VENTAS_10',
      condiciones: condMoneda as any,
      cuenta_id: 'c-ok',
      vigente_desde: '2026-10-01',
    });
    expect(r).toMatchObject({ estado: 'BORRADOR', orden: 4, creado_por: USER, empresa_id: EMP, modo: 'CUENTA' });
    expect(reglas).toHaveLength(2);
    const [a] = audits(prisma, 'CONT_REGLA_CREAR');
    expect(a).toMatchObject({ empresa_id: EMP, user_id: USER, entity_type: 'cont_mapeo_reglas', entity_id: r.id });
    expect(a.new_value).toMatchObject({ concepto: 'VENTAS_10', cuenta_codigo: '4.1.1.02', modo: 'CUENTA', orden: 4, vigente_desde: '2026-10-01' });
  });

  it('valida las condiciones (validarCondiciones)', async () => {
    const { service } = crear();
    await expect(
      service.crear(EMP, USER, { concepto: 'VENTAS_10', condiciones: [] as any, cuenta_id: 'c-ok', vigente_desde: '2026-10-01' }),
    ).rejects.toThrow(/al menos una condición/);
    await expect(
      service.crear(EMP, USER, {
        concepto: 'VENTAS_10',
        condiciones: [{ dimension: 'COLOR', operador: 'EN', valores: ['x'] }] as any,
        cuenta_id: 'c-ok',
        vigente_desde: '2026-10-01',
      }),
    ).rejects.toThrow(/dimensión desconocida/);
  });

  it('[I2] una dimensión de ítem en un concepto que no se abre por ítem → 400 y no se guarda', async () => {
    const { service, reglas } = crear([], { mapeos: ['CLIENTES'] });
    await expect(
      service.crear(EMP, USER, {
        concepto: 'CLIENTES',
        condiciones: [{ dimension: 'CATEGORIA_PRODUCTO', operador: 'EN', valores: ['cat'] }] as any,
        cuenta_id: 'c-ok',
        vigente_desde: '2026-10-01',
      }),
    ).rejects.toThrow(BadRequestException);
    expect(reglas).toHaveLength(0);
    await expect(
      service.crear(EMP, USER, {
        concepto: 'VENTAS_10',
        condiciones: [{ dimension: 'CATEGORIA_PRODUCTO', operador: 'EN', valores: ['cat'] }] as any,
        cuenta_id: 'c-ok',
        vigente_desde: '2026-10-01',
      }),
    ).resolves.toMatchObject({ estado: 'BORRADOR' });
  });

  it('el concepto tiene que existir en MAPEO_DEFAULT o en el mapeo de la empresa', async () => {
    const { service } = crear([], { mapeos: ['CONCEPTO_PROPIO'] });
    await expect(
      service.crear(EMP, USER, { concepto: 'INVENTADO', condiciones: condMoneda as any, cuenta_id: 'c-ok', vigente_desde: '2026-10-01' }),
    ).rejects.toBeInstanceOf(BadRequestException);
    await expect(
      service.crear(EMP, USER, { concepto: 'CONCEPTO_PROPIO', condiciones: condMoneda as any, cuenta_id: 'c-ok', vigente_desde: '2026-10-01' }),
    ).resolves.toMatchObject({ concepto: 'CONCEPTO_PROPIO' });
  });

  it('sumar a la principal solo en CONCEPTOS_CON_MODO, y sin cuenta', async () => {
    const { service } = crear();
    await expect(
      service.crear(EMP, USER, { concepto: 'VENTAS_10', condiciones: condMoneda as any, modo: 'SUMAR_A_PRINCIPAL', vigente_desde: '2026-10-01' }),
    ).rejects.toThrow(/no admite/);
    const r = await service.crear(EMP, USER, {
      concepto: 'IMPORTACION_GASTO',
      condiciones: condMoneda as any,
      modo: 'SUMAR_A_PRINCIPAL',
      cuenta_id: 'c-ok',
      vigente_desde: '2026-10-01',
    });
    expect(r).toMatchObject({ modo: 'SUMAR_A_PRINCIPAL', cuenta_id: null });
  });

  it('modo cuenta exige una cuenta de la empresa o global', async () => {
    const { service } = crear();
    await expect(
      service.crear(EMP, USER, { concepto: 'VENTAS_10', condiciones: condMoneda as any, vigente_desde: '2026-10-01' }),
    ).rejects.toThrow(/Elegí una cuenta/);
    await expect(
      service.crear(EMP, USER, { concepto: 'VENTAS_10', condiciones: condMoneda as any, cuenta_id: 'c-otra', vigente_desde: '2026-10-01' }),
    ).rejects.toBeInstanceOf(NotFoundException);
  });
});

describe('ReglasMapeoService.editar', () => {
  it('un BORRADOR se edita, sigue en BORRADOR y audita CONT_REGLA_EDITAR con antes y después', async () => {
    const { service, prisma } = crear([{ id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok' }]);
    const r = await service.editar(EMP, USER, 'r1', {
      concepto: 'VENTAS_10',
      condiciones: [{ dimension: 'MONEDA', operador: 'EN', valores: ['USD', 'EUR'] }] as any,
      cuenta_id: 'c-ok',
      vigente_desde: '2026-11-01',
    });
    expect(r.estado).toBe('BORRADOR');
    const [a] = audits(prisma, 'CONT_REGLA_EDITAR');
    expect(a.old_value).toMatchObject({ estado: 'BORRADOR', vigente_desde: '2026-01-01' });
    expect(a.new_value).toMatchObject({ estado: 'BORRADOR', vigente_desde: '2026-11-01', condiciones: [{ valores: ['USD', 'EUR'] }] });
  });

  it('una ACTIVA no se edita: 400 "retirala y creá una nueva", sin tocarla', async () => {
    const { service, reglas, prisma } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'ACTIVA', activado_at: new Date(), activado_por: USER },
    ]);
    const dto = { concepto: 'VENTAS_10', condiciones: condMoneda as any, cuenta_id: 'c-ok', vigente_desde: '2026-11-01' };
    await expect(service.editar(EMP, USER, 'r1', dto)).rejects.toThrow(/La regla está activa: retirala y creá una nueva \(podés usar Duplicar\)/);
    expect(reglas[0]).toMatchObject({ estado: 'ACTIVA' });
    expect(prisma.cont_mapeo_reglas.update).not.toHaveBeenCalled();
    expect(audits(prisma, 'CONT_REGLA_EDITAR')).toHaveLength(0);
  });

  it('una RETIRADA no se edita; una de otra empresa es 404', async () => {
    const { service } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'RETIRADA', vigente_hasta: new Date('2026-10-10') },
      { id: 'r2', empresa_id: OTRA, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-otra' },
    ]);
    const dto = { concepto: 'VENTAS_10', condiciones: condMoneda as any, cuenta_id: 'c-ok', vigente_desde: '2026-10-01' };
    await expect(service.editar(EMP, USER, 'r1', dto)).rejects.toBeInstanceOf(BadRequestException);
    await expect(service.editar(EMP, USER, 'r2', dto)).rejects.toBeInstanceOf(NotFoundException);
  });
});

describe('ReglasMapeoService.activar', () => {
  it('[I2] un borrador viejo con dimensión de ítem en un concepto que no se abre por ítem no se activa', async () => {
    const { service, reglas } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'CLIENTES', orden: 1, condiciones: [{ dimension: 'RUBRO', operador: 'EN', valores: ['x'] }], cuenta_id: 'c-ok' },
    ]);
    await expect(service.activar(EMP, USER, 'r1')).rejects.toThrow(/solo vale/);
    expect(reglas[0].estado).toBe('BORRADOR');
  });

  it('con cuenta desactivada o de agrupación → 400, sin tocar la regla', async () => {
    const { service, reglas } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-baja' },
      { id: 'r2', empresa_id: EMP, concepto: 'VENTAS_10', orden: 2, condiciones: condMoneda, cuenta_id: 'c-grupo' },
    ]);
    await expect(service.activar(EMP, USER, 'r1')).rejects.toThrow(/desactivada/);
    await expect(service.activar(EMP, USER, 'r2')).rejects.toThrow(/agrupación/);
    expect(reglas.map((r) => r.estado)).toEqual(['BORRADOR', 'BORRADOR']);
  });

  it('deja constancia (activado_por/at) y audita CONT_REGLA_ACTIVAR con la regla completa', async () => {
    const { service, prisma, avisos } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 2, condiciones: condMoneda, cuenta_id: 'c-ok', vigente_desde: new Date('2026-10-01') },
    ]);
    const r = await service.activar(EMP, USER, 'r1');
    expect(r).toMatchObject({ estado: 'ACTIVA', activado_por: USER });
    expect(r.activado_at).toBeInstanceOf(Date);
    const [a] = audits(prisma, 'CONT_REGLA_ACTIVAR');
    expect(a).toMatchObject({ empresa_id: EMP, user_id: USER, entity_type: 'cont_mapeo_reglas', entity_id: 'r1' });
    expect(a.new_value).toEqual(
      expect.objectContaining({
        concepto: 'VENTAS_10',
        condiciones: condMoneda,
        cuenta_codigo: '4.1.1.02',
        modo: 'CUENTA',
        orden: 2,
        vigente_desde: '2026-10-01',
        vigente_hasta: null,
        estado: 'ACTIVA',
      }),
    );
    expect(a.old_value).toMatchObject({ estado: 'BORRADOR' });
    expect(avisos.sincronizar).toHaveBeenCalledWith(EMP);
  });

  it('modo sumar en un concepto que no lo admite → 400', async () => {
    const { service } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, modo: 'SUMAR_A_PRINCIPAL', cuenta_id: null },
    ]);
    await expect(service.activar(EMP, USER, 'r1')).rejects.toThrow(/no admite/);
  });

  it('una regla de otra empresa es 404', async () => {
    const { service } = crear([{ id: 'r1', empresa_id: OTRA, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-otra' }]);
    await expect(service.activar(EMP, USER, 'r1')).rejects.toBeInstanceOf(NotFoundException);
  });

  it('si sincronizar los avisos falla, la activación igual queda', async () => {
    const { service, avisos } = crear([{ id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok' }]);
    avisos.sincronizar.mockRejectedValueOnce(new Error('caída'));
    await expect(service.activar(EMP, USER, 'r1')).resolves.toMatchObject({ estado: 'ACTIVA' });
  });
});

describe('ReglasMapeoService.retirar y borrar', () => {
  it('retirar no borra: RETIRADA, vigente_hasta = hoy y constancia', async () => {
    const { service, prisma, reglas } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'ACTIVA', activado_at: new Date('2026-10-02') },
    ]);
    const r = await service.retirar(EMP, USER, 'r1');
    expect(reglas).toHaveLength(1);
    expect(prisma.cont_mapeo_reglas.delete).not.toHaveBeenCalled();
    expect(r).toMatchObject({ estado: 'RETIRADA', retirado_por: USER });
    const hoy = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Asuncion' });
    expect(r.vigente_hasta.toISOString().slice(0, 10)).toBe(hoy);
    const [a] = audits(prisma, 'CONT_REGLA_RETIRAR');
    expect(a.new_value).toMatchObject({ estado: 'RETIRADA', vigente_hasta: hoy });
  });

  it('[retiro] acepta una fecha de fin elegida y cuenta los asientos con la regla desde esa fecha', async () => {
    const { service, prisma } = crear(
      [{ id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'ACTIVA', activado_at: new Date('2026-10-02'), vigente_desde: new Date('2026-10-01') }],
      { asientosPosteriores: 3 },
    );
    const r = await service.retirar(EMP, USER, 'r1', { vigenteHasta: '2026-10-20' });
    expect(r.vigente_hasta.toISOString().slice(0, 10)).toBe('2026-10-20');
    expect(r.asientos_desde_fin).toBe(3);
    const where = prisma.cont_asientos.count.mock.calls[0][0].where;
    expect(where).toMatchObject({ empresa_id: EMP, lineas: { some: { mapeo_regla_id: 'r1' } } });
    expect(where.fecha.gte.toISOString().slice(0, 10)).toBe('2026-10-20');
  });

  it('[retiro] fin igual al inicio vale ("nunca aplicó"); anterior al inicio → 400', async () => {
    const base = { empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'ACTIVA', activado_at: new Date('2026-10-02'), vigente_desde: new Date('2026-10-10') };
    const { service, reglas } = crear([{ ...base, id: 'r1' }, { ...base, id: 'r2', orden: 2 }]);
    await expect(service.retirar(EMP, USER, 'r1', { vigenteHasta: '2026-10-09' })).rejects.toThrow(/anterior/);
    expect(reglas[0].estado).toBe('ACTIVA');
    const r = await service.retirar(EMP, USER, 'r2', { vigenteHasta: '2026-10-10' });
    expect(r).toMatchObject({ estado: 'RETIRADA' });
    expect(r.vigente_hasta.toISOString().slice(0, 10)).toBe('2026-10-10');
    await expect(service.retirar(EMP, USER, 'r1', { vigenteHasta: 'mañana' })).rejects.toThrow(BadRequestException);
  });

  it('retirar una que nunca estuvo activa → 400 (se borra)', async () => {
    const { service } = crear([{ id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok' }]);
    await expect(service.retirar(EMP, USER, 'r1')).rejects.toThrow(/borrala/);
  });

  it('borrar una que estuvo activa → 400 "retirala"; una nunca activada se borra', async () => {
    const { service, reglas } = crear([
      { id: 'r1', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'RETIRADA', activado_at: new Date('2026-10-02'), vigente_hasta: new Date('2026-10-10') },
      { id: 'r2', empresa_id: EMP, concepto: 'VENTAS_10', orden: 2, condiciones: condMoneda, cuenta_id: 'c-ok' },
    ]);
    await expect(service.borrar(EMP, USER, 'r1')).rejects.toThrow(/retirala/);
    await service.borrar(EMP, USER, 'r2');
    expect(reglas.map((r) => r.id)).toEqual(['r1']);
  });

  it('borrar una de otra empresa es 404', async () => {
    const { service, reglas } = crear([{ id: 'r1', empresa_id: OTRA, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-otra' }]);
    await expect(service.borrar(EMP, USER, 'r1')).rejects.toBeInstanceOf(NotFoundException);
    expect(reglas).toHaveLength(1);
  });
});

describe('ReglasMapeoService.reordenar', () => {
  it('reasigna 1..n en el orden pedido (las no listadas van después) y audita CONT_REGLA_ORDEN', async () => {
    const { service, prisma, reglas } = crear([
      { id: 'a', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok' },
      { id: 'b', empresa_id: EMP, concepto: 'VENTAS_10', orden: 2, condiciones: condMoneda, cuenta_id: 'c-ok' },
      { id: 'c', empresa_id: EMP, concepto: 'VENTAS_10', orden: 3, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'RETIRADA', vigente_hasta: new Date('2026-10-01') },
    ]);
    await service.reordenar(EMP, USER, { concepto: 'VENTAS_10', ids: ['b', 'a'] });
    expect(Object.fromEntries(reglas.map((r) => [r.id, r.orden]))).toEqual({ b: 1, a: 2, c: 3 });
    expect(prisma.$transaction).toHaveBeenCalled();
    const [a] = audits(prisma, 'CONT_REGLA_ORDEN');
    expect(a.entity_type).toBe('cont_mapeo_reglas');
    expect(a.old_value.reglas.map((r: any) => [r.id, r.orden])).toEqual([['a', 1], ['b', 2], ['c', 3]]);
    expect(a.new_value.reglas.map((r: any) => [r.id, r.orden])).toEqual([['b', 1], ['a', 2], ['c', 3]]);
  });

  it('un id de otra empresa o de otro concepto → 404', async () => {
    const { service } = crear([
      { id: 'a', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok' },
      { id: 'x', empresa_id: OTRA, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-otra' },
    ]);
    await expect(service.reordenar(EMP, USER, { concepto: 'VENTAS_10', ids: ['x', 'a'] })).rejects.toBeInstanceOf(NotFoundException);
  });
});

describe('ReglasMapeoService.listar', () => {
  it('filtra por empresa y trae frase, cuenta y constancia', async () => {
    const { service, prisma } = crear([
      { id: 'a', empresa_id: EMP, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'ACTIVA', activado_por: USER, activado_at: new Date() },
      { id: 'x', empresa_id: OTRA, concepto: 'VENTAS_10', orden: 1, condiciones: condMoneda, cuenta_id: 'c-otra' },
    ]);
    prisma.empresas_sucursales = { findMany: jest.fn(async () => []) };
    prisma.categorias = { findMany: jest.fn(async () => []) };
    prisma.rubros = { findMany: jest.fn(async () => []) };
    const lista = await service.listar(EMP, 'VENTAS_10');
    expect(lista.map((r: any) => r.id)).toEqual(['a']);
    expect(lista[0]).toMatchObject({
      frase: 'Si moneda es USD → 4.1.1.02 VENTAS EXTERIOR',
      cuenta: { codigo: '4.1.1.02', descripcion: 'VENTAS EXTERIOR' },
      creado_por_nombre: 'Ana Contadora',
      activado_por_nombre: 'Ana Contadora',
    });
  });
});

describe('ReglasMapeoService.probador', () => {
  const conMapeo = (reglas: Regla[], mapeo: Regla | null) => {
    const t = crear(reglas);
    const m = Object.create(MapeoCuentasService.prototype) as any;
    m.prisma = {
      cont_mapeo_reglas: { findMany: jest.fn(async ({ where }: any) => reglas.filter((r) => r.empresa_id === where.empresa_id)) },
      cont_mapeo_cuentas: { findUnique: jest.fn(async () => mapeo) },
      cont_plan_cuentas: t.prisma.cont_plan_cuentas,
      categorias: { findMany: jest.fn(async () => [{ id: 'hija', padre_id: 'padre' }, { id: 'padre', padre_id: null }]) },
    };
    (t.service as any).mapeo = m;
    t.prisma.categorias = { findMany: jest.fn(async () => [{ id: 'padre', descripcion: 'BEBIDAS' }]) };
    return t;
  };
  const base = { empresa_id: EMP, concepto: 'VENTAS_10', modo: 'CUENTA', vigente_desde: new Date('2026-01-01'), vigente_hasta: null };
  const defecto = { cuenta_id: 'c-def', modo: 'CUENTA', cuenta: { codigo: '4.1.1.01', active: true, acepta_movimientos: true } };

  it('dice qué regla aplica, con la cuenta y la traza', async () => {
    const { service } = conMapeo([{ ...base, id: 'r1', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'ACTIVA' }], defecto);
    const r = await service.probador(EMP, { concepto: 'VENTAS_10', dimensiones: { MONEDA: 'USD' } });
    expect(r).toMatchObject({ cuentaId: 'c-ok', cuenta: { codigo: '4.1.1.02' }, traza: { nivel: 'REGLA', reglaId: 'r1' } });
  });

  it('un borrador solo aplica con incluirBorradores; si no, por defecto', async () => {
    const { service, prisma } = conMapeo([{ ...base, id: 'r1', orden: 1, condiciones: condMoneda, cuenta_id: 'c-ok', estado: 'BORRADOR' }], defecto);
    prisma.cont_plan_cuentas.findFirst.mockImplementation(async ({ where }: any) =>
      where.id === 'c-def' ? { id: 'c-def', codigo: '4.1.1.01', descripcion: 'VENTAS' } : { id: 'c-ok', codigo: '4.1.1.02', descripcion: 'VENTAS EXTERIOR', active: true, acepta_movimientos: true },
    );
    const sin = await service.probador(EMP, { concepto: 'VENTAS_10', dimensiones: { MONEDA: 'USD' } });
    expect(sin).toMatchObject({ cuentaId: 'c-def', traza: { nivel: 'DEFECTO' } });
    const con = await service.probador(EMP, { concepto: 'VENTAS_10', dimensiones: { MONEDA: 'USD' }, incluirBorradores: true });
    expect(con).toMatchObject({ cuentaId: 'c-ok', traza: { nivel: 'REGLA', reglaId: 'r1' } });
  });

  it('una categoría hija cumple la regla de su padre', async () => {
    const { service } = conMapeo(
      [{ ...base, id: 'r1', orden: 1, condiciones: [{ dimension: 'CATEGORIA_PRODUCTO', operador: 'EN', valores: ['padre'] }], cuenta_id: 'c-ok', estado: 'ACTIVA' }],
      defecto,
    );
    const r = await service.probador(EMP, { concepto: 'VENTAS_10', dimensiones: { CATEGORIA_PRODUCTO: 'hija' } });
    expect(r).toMatchObject({ traza: { nivel: 'REGLA', reglaId: 'r1' } });
  });

  it('una regla con cuenta desactivada devuelve la falta con su motivo, sin lanzar', async () => {
    const { service } = conMapeo([{ ...base, id: 'r1', orden: 1, condiciones: condMoneda, cuenta_id: 'c-baja', estado: 'ACTIVA' }], defecto);
    const r = await service.probador(EMP, { concepto: 'VENTAS_10', dimensiones: { MONEDA: 'USD' } });
    expect(r).toEqual({ falta: { motivo: 'CUENTA_INUTILIZABLE', mensaje: expect.stringMatching(/^No hay cuenta mapeada para el concepto.*regla n\.º 1/) } });
  });

  it('[I2] dimensiones de ítem en un concepto que no se abre por ítem: no se usan y se advierte', async () => {
    const { service } = conMapeo([], defecto);
    const r: any = await service.probador(EMP, { concepto: 'CLIENTES', dimensiones: { MONEDA: 'USD', RUBRO: 'x' } });
    expect(r.advertencia).toMatch(/CLIENTES/);
    const v: any = await service.probador(EMP, { concepto: 'VENTAS_10', dimensiones: { RUBRO: 'x' } });
    expect(v.advertencia).toBeUndefined();
  });

  it('una dimensión desconocida → 400', async () => {
    const { service } = conMapeo([], defecto);
    await expect(service.probador(EMP, { concepto: 'VENTAS_10', dimensiones: { COLOR: 'rojo' } as any })).rejects.toBeInstanceOf(BadRequestException);
  });
});
