import { MapeoCuentasService } from './mapeo-cuentas.service';
import { FaltaMapeoError } from './falta-mapeo.error';

const crear = ({ mapeo, reglas = [], categorias = [] }: any) => {
  const s = Object.create(MapeoCuentasService.prototype) as any;
  s.prisma = {
    cont_mapeo_cuentas: { findUnique: jest.fn().mockResolvedValue(mapeo) },
    cont_mapeo_reglas: { findMany: jest.fn().mockResolvedValue(reglas) },
    cont_plan_cuentas: { findFirst: jest.fn().mockImplementation(({ where }) => Promise.resolve(
      where.id === 'ajena' ? null : { id: where.id, codigo: where.id === 'muerta' ? '9.9' : '4.1', active: where.id !== 'muerta', acepta_movimientos: where.id !== 'grupo' })) },
    categorias: { findMany: jest.fn().mockResolvedValue(categorias) },
  };
  return s as MapeoCuentasService;
};
const def = { cuenta_id: 'def', modo: 'CUENTA', cuenta: { codigo: '4.1.1.01', active: true, acepta_movimientos: true } };
const regla = (over: any = {}) => ({ id: 'r1', concepto: 'VENTAS_10', orden: 1, modo: 'CUENTA', cuenta_id: 'usd', estado: 'ACTIVA',
  vigente_desde: new Date('2026-01-01'), vigente_hasta: null, condiciones: [{ dimension: 'MONEDA', operador: 'EN', valores: ['USD'] }], ...over });

it('sin contexto: idéntico a hoy, traza DEFECTO', async () => {
  const r = await crear({ mapeo: def, reglas: [regla()] }).resolverConcepto('emp', 'VENTAS_10');
  expect(r).toEqual({ modo: 'CUENTA', cuentaId: 'def', traza: { concepto: 'VENTAS_10', nivel: 'DEFECTO' } });
});

it('con contexto aplica la regla y lo dice en la traza', async () => {
  const res = await crear({ mapeo: def, reglas: [regla()] }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  expect(await res.cuenta('VENTAS_10')).toBe('usd');
  expect(res.anotar([{ cuenta_id: 'usd', concepto: 'VENTAS_10' }])[0]).toMatchObject({ mapeo_concepto: 'VENTAS_10', mapeo_nivel: 'REGLA', mapeo_regla_id: 'r1' });
});

it('[RF-2] regla retirada después de la fecha del documento sigue aplicando a ese documento', async () => {
  const res = await crear({ mapeo: def, reglas: [regla({ estado: 'RETIRADA', vigente_hasta: new Date('2026-11-01') })] })
    .crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  expect(await res.cuenta('VENTAS_10')).toBe('usd');
});

it('regla con cuenta desactivada → FaltaMapeoError CUENTA_INUTILIZABLE que nombra la regla', async () => {
  const res = await crear({ mapeo: def, reglas: [regla({ cuenta_id: 'muerta' })] }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  const err = await res.cuenta('VENTAS_10').catch((e) => e);
  expect(err).toBeInstanceOf(FaltaMapeoError);
  expect(err.motivo).toBe('CUENTA_INUTILIZABLE');
  expect(err.message).toMatch(/^No hay cuenta mapeada para el concepto/);
  expect(err.message).toMatch(/regla n\.º 1/);
  // [I1] El error dice qué regla fue, para la constancia y los avisos.
  expect(err.regla).toEqual({ id: 'r1', orden: 1 });
});

it('[I1] el defecto inutilizable no trae regla en el error', async () => {
  const res = await crear({ mapeo: { ...def, cuenta: { ...def.cuenta, active: false } } }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'PYG' } });
  const err = await res.cuenta('VENTAS_10').catch((e) => e);
  expect(err.motivo).toBe('CUENTA_INUTILIZABLE');
  expect(err.regla).toBeUndefined();
});

it('sin fila de mapeo y ninguna regla aplica → SIN_MAPEO', async () => {
  const res = await crear({ mapeo: null }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'PYG' } });
  await expect(res.cuenta('VENTAS_10')).rejects.toMatchObject({ motivo: 'SIN_MAPEO' });
});

it('lee las reglas una sola vez por documento', async () => {
  const s = crear({ mapeo: def, reglas: [regla()] }) as any;
  const res = await s.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  await res.cuenta('VENTAS_10'); await res.cuenta('VENTAS_10'); await res.cuenta('VENTAS_5');
  expect(s.prisma.cont_mapeo_reglas.findMany).toHaveBeenCalledTimes(1);
  expect(s.prisma.cont_mapeo_reglas.findMany.mock.calls[0][0]).toMatchObject({
    where: { empresa_id: 'emp' }, orderBy: [{ orden: 'asc' }, { id: 'asc' }],
  });
});

it('marcar deja FICHA/MEDIO_PAGO en la traza', async () => {
  const res = await crear({ mapeo: def }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {} });
  res.marcar('prov', 'FICHA');
  expect(res.anotar([{ cuenta_id: 'prov' }])[0]).toMatchObject({ mapeo_nivel: 'FICHA' });
});

it('marcar con origen: la línea FICHA guarda de qué ficha sale en mapeo_concepto; MEDIO_PAGO sin concepto', async () => {
  const res = await crear({ mapeo: def }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {} });
  res.marcar('prov', 'FICHA', 'FICHA_PROVEEDOR');
  // La primera marca de una cuenta manda, con su origen.
  res.marcar('prov', 'FICHA', 'FICHA_CXP_CONGELADA');
  res.marcar('banco', 'MEDIO_PAGO');
  res.marcar('vieja', 'FICHA');
  const [prov, banco, vieja] = res.anotar([{ cuenta_id: 'prov' }, { cuenta_id: 'banco' }, { cuenta_id: 'vieja' }]);
  expect(prov).toEqual({ cuenta_id: 'prov', mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_PROVEEDOR' });
  expect(banco).toEqual({ cuenta_id: 'banco', mapeo_nivel: 'MEDIO_PAGO' });
  expect(vieja).toEqual({ cuenta_id: 'vieja', mapeo_nivel: 'FICHA' });
});

it('prepararCategorias: hereda por padre (con ciclo en padre_id) y no muta el contexto del llamador', async () => {
  const cats = [{ id: 'hija', padre_id: 'madre' }, { id: 'madre', padre_id: 'hija' }, { id: 'sola', padre_id: null }];
  const rc = (id: string, cat: string) => regla({ id, cuenta_id: id, condiciones: [{ dimension: 'CATEGORIA_PRODUCTO', operador: 'EN', valores: [cat] }] });
  const contexto: any = { fecha: new Date('2026-10-13'), dimensiones: {} };
  const res = await crear({ mapeo: def, categorias: cats, reglas: [rc('cm', 'madre'), rc('cs', 'sola')] }).crearResolvedor('emp', contexto);
  await res.prepararCategorias(['hija', 'sola']);
  expect(contexto.ancestros).toBeUndefined();
  expect(await res.cuenta('VENTAS_10', { CATEGORIA_PRODUCTO: 'hija' })).toBe('cm');
  expect(await res.cuenta('VENTAS_10', { CATEGORIA_PRODUCTO: 'sola' })).toBe('cs');
});

it('cuenta de regla de otra empresa → CUENTA_INUTILIZABLE; la consulta filtra por empresa', async () => {
  const s = crear({ mapeo: def, reglas: [regla({ cuenta_id: 'ajena' })] }) as any;
  const res = await s.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  await expect(res.cuenta('VENTAS_10')).rejects.toMatchObject({ motivo: 'CUENTA_INUTILIZABLE' });
  expect(s.prisma.cont_plan_cuentas.findFirst.mock.calls[0][0].where).toEqual({ id: 'ajena', OR: [{ empresa_id: 'emp' }, { empresa_id: null }] });
});

it('dos resoluciones a la misma cuenta de regla → una sola consulta de la cuenta', async () => {
  const s = crear({ mapeo: def, reglas: [regla(), regla({ id: 'r2', concepto: 'VENTAS_5', orden: 1 })] }) as any;
  const res = await s.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  await res.cuenta('VENTAS_10'); await res.cuenta('VENTAS_5'); await res.cuenta('VENTAS_10');
  expect(s.prisma.cont_plan_cuentas.findFirst).toHaveBeenCalledTimes(1);
});

it('BORRADOR se ignora sin incluirBorradores en resolverConcepto', async () => {
  const s = crear({ mapeo: def, reglas: [regla({ estado: 'BORRADOR' })] });
  const ctx = { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } };
  expect(await s.resolverConcepto('emp', 'VENTAS_10', ctx)).toMatchObject({ cuentaId: 'def', traza: { nivel: 'DEFECTO' } });
  expect(await s.resolverConcepto('emp', 'VENTAS_10', { ...ctx, incluirBorradores: true })).toMatchObject({ cuentaId: 'usd', traza: { nivel: 'REGLA' } });
});

it('regla en modo SUMAR_A_PRINCIPAL', async () => {
  const s = crear({ mapeo: def, reglas: [regla({ modo: 'SUMAR_A_PRINCIPAL', cuenta_id: null })] });
  expect(await s.resolverConcepto('emp', 'VENTAS_10', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } }))
    .toEqual({ modo: 'SUMAR_A_PRINCIPAL', traza: { concepto: 'VENTAS_10', nivel: 'REGLA', reglaId: 'r1' } });
});

it('regla con cuenta de agrupación → CUENTA_INUTILIZABLE que nombra la regla', async () => {
  const res = await crear({ mapeo: def, reglas: [regla({ cuenta_id: 'grupo' })] }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  const err = await res.cuenta('VENTAS_10').catch((e) => e);
  expect(err.motivo).toBe('CUENTA_INUTILIZABLE');
  expect(err.message).toMatch(/regla n\.º 1.*agrupación/);
});

it('misma cuenta por dos caminos: cada línea lleva la traza de su concepto, en cualquier orden', async () => {
  for (const orden of [['VENTAS_10', 'VENTAS_5'], ['VENTAS_5', 'VENTAS_10']]) {
    const s = crear({ mapeo: { ...def, cuenta_id: 'usd' }, reglas: [regla()] });
    const res = await s.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
    for (const c of orden) await res.cuenta(c); // VENTAS_10 por regla, VENTAS_5 por defecto; misma cuenta 'usd'
    const [l10, l5] = res.anotar([{ cuenta_id: 'usd', concepto: 'VENTAS_10' }, { cuenta_id: 'usd', concepto: 'VENTAS_5' }]);
    expect(l10).toEqual({ cuenta_id: 'usd', mapeo_concepto: 'VENTAS_10', mapeo_nivel: 'REGLA', mapeo_regla_id: 'r1' });
    expect(l5).toEqual({ cuenta_id: 'usd', mapeo_concepto: 'VENTAS_5', mapeo_nivel: 'DEFECTO' });
  }
});

it('una línea sin concepto ni marca queda sin traza; con concepto, el concepto le gana a la marca de la cuenta', async () => {
  const res = await crear({ mapeo: def }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {} });
  await res.cuenta('VENTAS_10');
  res.marcar('def', 'MEDIO_PAGO');
  expect(res.anotar([{ cuenta_id: 'otra' }])[0]).toEqual({ cuenta_id: 'otra' });
  expect(res.anotar([{ cuenta_id: 'def' }])[0]).toEqual({ cuenta_id: 'def', mapeo_nivel: 'MEDIO_PAGO' });
  expect(res.anotar([{ cuenta_id: 'def', concepto: 'VENTAS_10' }])[0]).toEqual({ cuenta_id: 'def', mapeo_concepto: 'VENTAS_10', mapeo_nivel: 'DEFECTO' });
});

it('por ítem: la traza se busca por concepto y dimensiones del ítem', async () => {
  const rc = regla({ cuenta_id: 'cat', condiciones: [{ dimension: 'TIPO_ITEM', operador: 'EN', valores: ['SERVICIO'] }] });
  const res = await crear({ mapeo: def, reglas: [rc] }).crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {} });
  await res.cuenta('VENTAS_10', { TIPO_ITEM: 'SERVICIO' });
  await res.cuenta('VENTAS_10', { TIPO_ITEM: 'BIEN' });
  const [a, b] = res.anotar([
    { cuenta_id: 'cat', concepto: 'VENTAS_10', dimsItem: { TIPO_ITEM: 'SERVICIO' } },
    { cuenta_id: 'def', concepto: 'VENTAS_10', dimsItem: { TIPO_ITEM: 'BIEN' } },
  ]);
  expect(a).toMatchObject({ mapeo_nivel: 'REGLA', mapeo_regla_id: 'r1' });
  expect(b).toEqual({ cuenta_id: 'def', mapeo_concepto: 'VENTAS_10', mapeo_nivel: 'DEFECTO' });
});

it('tieneReglasDeItem mira solo reglas activas (o borrador en vista previa) del concepto', async () => {
  const rItem = regla({ condiciones: [{ dimension: 'RUBRO', operador: 'EN', valores: ['x'] }] });
  const s = crear({ mapeo: def, reglas: [rItem, regla({ id: 'r2', concepto: 'VENTAS_5' })] });
  const res = await s.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {} });
  expect(res.tieneReglasDeItem('VENTAS_10')).toBe(true);
  expect(res.tieneReglasDeItem('VENTAS_5')).toBe(false);
  const b = crear({ mapeo: def, reglas: [{ ...rItem, estado: 'BORRADOR' }] });
  expect((await b.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {} })).tieneReglasDeItem('VENTAS_10')).toBe(false);
  expect((await b.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {}, incluirBorradores: true })).tieneReglasDeItem('VENTAS_10')).toBe(true);
});

it('tieneReglasDeItem considera una regla de ítem retirada solo si su vigencia incluye la fecha del documento', async () => {
  const retirada = regla({ estado: 'RETIRADA', vigente_hasta: new Date('2026-11-01'), condiciones: [{ dimension: 'CATEGORIA_PRODUCTO', operador: 'EN', valores: ['x'] }] });
  const s = crear({ mapeo: def, reglas: [retirada] });
  expect((await s.crearResolvedor('emp', { fecha: new Date('2026-10-13'), dimensiones: {} })).tieneReglasDeItem('VENTAS_10')).toBe(true);
  expect((await s.crearResolvedor('emp', { fecha: new Date('2026-11-05'), dimensiones: {} })).tieneReglasDeItem('VENTAS_10')).toBe(false);
});
