import { CATALOGO_ASIENTOS } from './catalogo-asientos';
import { MAPEO_DEFAULT } from '../seeds/plan-cuentas-paraguay.seed';
import { MapeoCuentasService } from './mapeo-cuentas.service';
import { lastValueFrom, of } from 'rxjs';
import { BigIntSerializerInterceptor } from '../../common/interceptors/bigint-serializer.interceptor';

/**
 * El catálogo es lo que el contador ve en pantalla antes de que exista un asiento
 * real: si un concepto está mal escrito, muestra "falta mapear" sobre una cuenta
 * que en realidad sí está mapeada, y si un asiento no cuadra, enseña algo que la
 * contabilidad nunca aceptaría.
 */
// Cada forma posible del asiento (la base y sus variantes) se valida igual.
const FORMAS = CATALOGO_ASIENTOS.flatMap((s) => [
  { clave: s.clave, lineas: s.lineas },
  ...(s.variantes ?? []).map((v) => ({ clave: `${s.clave}[${v.siMapeado}]`, lineas: v.lineas })),
]);

describe('Catálogo de asientos automáticos', () => {
  it('todos los conceptos existen en el mapeo del sistema', () => {
    const conocidos = new Set(Object.keys(MAPEO_DEFAULT));
    const desconocidos = FORMAS.flatMap((s) =>
      s.lineas
        .filter((l) => l.concepto && !conocidos.has(l.concepto))
        .map((l) => `${s.clave} → ${l.concepto}`),
    );
    const condiciones = CATALOGO_ASIENTOS.flatMap((s) => [
      s.soloSiMapeado,
      ...(s.variantes ?? []).map((v) => v.siMapeado),
    ]).filter((c): c is string => !!c && !conocidos.has(c));
    desconocidos.push(...condiciones);

    expect(desconocidos).toEqual([]);
  });

  it('cada situación cuadra: el debe del ejemplo es igual al haber', () => {
    const descuadradas = FORMAS.filter((s) => {
      const debe = s.lineas.filter((l) => l.lado === 'DEBE').reduce((a, l) => a + l.monto, 0);
      const haber = s.lineas.filter((l) => l.lado === 'HABER').reduce((a, l) => a + l.monto, 0);
      return debe !== haber;
    }).map((s) => s.clave);

    expect(descuadradas).toEqual([]);
  });

  it('toda línea tiene cuenta: o un concepto del mapeo, o de dónde sale', () => {
    const sinOrigen = FORMAS.flatMap((s) =>
      s.lineas.filter((l) => !l.concepto && !l.cuentaDinamica).map(() => s.clave),
    );

    expect(sinOrigen).toEqual([]);
  });

  it('cada situación tiene al menos una línea de cada lado y una clave única', () => {
    const claves = CATALOGO_ASIENTOS.map((s) => s.clave);
    expect(new Set(claves).size).toBe(claves.length);

    for (const s of FORMAS) {
      expect(s.lineas.some((l) => l.lado === 'DEBE')).toBe(true);
      expect(s.lineas.some((l) => l.lado === 'HABER')).toBe(true);
    }
  });

  it('el cierre de la importación traslada lo que entró a la puente, no el costo total, y sin saldo no hay asiento', () => {
    const cierre = CATALOGO_ASIENTOS.find((s) => s.clave === 'CIERRE_IMPORTACION')!;
    expect(cierre.nota).toMatch(/entr[óo] a la cuenta puente/i);
    expect(cierre.nota).toMatch(/saldo/i);
    expect(cierre.nota).not.toMatch(/^Por el costo total/);
  });

  it('la nota de crédito de una compra de importación revierte contra IMPORTACION_COMPRA', () => {
    const nc = CATALOGO_ASIENTOS.find((s) => s.clave === 'NOTA_CREDITO_COMPRA_IMPORTACION')!;
    expect(nc).toBeDefined();
    expect(nc.lineas.find((l) => l.lado === 'HABER' && l.concepto === 'IMPORTACION_COMPRA')).toBeDefined();
    expect(nc.lineas.map((l) => l.concepto)).not.toContain('INVENTARIO');
  });
});

describe('MapeoCuentasService.previsualizacionAsientos', () => {
  const crear = (
    mapeos: Array<{ concepto: string; codigo: string; descripcion: string }>,
    tes: { cuentas?: any[]; categorias?: any[]; mapeosCategoria?: any[]; plan?: any[]; mediosPago?: any[]; reglas?: any[] } = {},
  ) => {
    const prisma: any = {
      cont_mapeo_cuentas: {
        findMany: jest.fn().mockResolvedValue(
          mapeos.map((m) => ({
            concepto: m.concepto,
            cuenta: { codigo: m.codigo, descripcion: m.descripcion },
          })),
        ),
      },
      tes_cuentas: { findMany: jest.fn().mockResolvedValue(tes.cuentas ?? []) },
      tes_categorias: { findMany: jest.fn().mockResolvedValue(tes.categorias ?? []) },
      tes_categoria_cuenta_contable: { findMany: jest.fn().mockResolvedValue(tes.mapeosCategoria ?? []) },
      cont_plan_cuentas: { findMany: jest.fn().mockResolvedValue(tes.plan ?? []) },
      cont_mapeo_reglas: { findMany: jest.fn().mockResolvedValue(tes.reglas ?? []) },
      empresas_sucursales: {
        findMany: jest.fn(async ({ where }: any) => (where.empresa_id === 'emp-1' ? [{ id: 'suc-cde', descripcion: 'Ciudad del Este' }] : [])),
      },
    };
    const service = Object.create(MapeoCuentasService.prototype) as any;
    service.prisma = prisma;
    service.mediosPago = {
      listar: jest.fn().mockResolvedValue({ medios: tes.mediosPago ?? [], total_configurados: 0 }),
    };
    return service;
  };

  it('pega la cuenta mapeada en cada línea y suma los totales', async () => {
    const service = crear([
      { concepto: 'CAJA_GENERAL', codigo: '1.01.01.01', descripcion: 'CAJA GENERAL' },
      { concepto: 'VENTAS_10', codigo: '4.01.01', descripcion: 'VENTAS GRAVADAS 10%' },
      { concepto: 'IVA_DEBITO_10', codigo: '2.01.03.01.02', descripcion: 'IVA A PAGAR' },
    ]);

    const { situaciones } = await service.previsualizacionAsientos('emp-1');
    const venta = situaciones.find((s: any) => s.clave === 'VENTA_CONTADO');
    const caja = venta.lineas.find((l: any) => l.concepto === 'CAJA_GENERAL');

    expect(caja.cuenta).toEqual({ codigo: '1.01.01.01', descripcion: 'CAJA GENERAL' });
    expect(caja.sin_mapear).toBe(false);
    expect(venta.total_debe).toBe(1_100_000);
    expect(venta.total_haber).toBe(1_100_000);
  });

  it('marca las líneas cuyo concepto no está mapeado todavía', async () => {
    const service = crear([{ concepto: 'CAJA_GENERAL', codigo: '1.01.01.01', descripcion: 'CAJA GENERAL' }]);

    const { situaciones } = await service.previsualizacionAsientos('emp-1');
    const venta = situaciones.find((s: any) => s.clave === 'VENTA_CONTADO');
    const ventas10 = venta.lineas.find((l: any) => l.concepto === 'VENTAS_10');

    expect(ventas10.sin_mapear).toBe(true);
    expect(ventas10.cuenta).toBeNull();
    expect(venta.conceptos_sin_mapear).toEqual(expect.arrayContaining(['VENTAS_10', 'IVA_DEBITO_10']));
  });

  it('una cuenta que no sale del mapeo no cuenta como falta', async () => {
    const service = crear([]);

    const { situaciones } = await service.previsualizacionAsientos('emp-1');
    const transferencia = situaciones.find((s: any) => s.clave === 'TRANSFERENCIA_TES');
    const destino = transferencia.lineas[0];

    expect(destino.sin_mapear).toBe(false);
    expect(destino.cuenta_dinamica).toMatch(/cuenta destino/i);
    // Las cuentas de tesorería no faltan; la diferencia de cambio sí sale del
    // mapeo (antes eran códigos fijos del plan modelo).
    expect(transferencia.conceptos_sin_mapear).toEqual(['DIFERENCIA_CAMBIO_PERDIDA', 'DIFERENCIA_CAMBIO_GANADA']);
  });

  it('trae las cuentas y categorías reales de tesorería, con y sin cuenta contable', async () => {
    const service = crear([], {
      cuentas: [
        { id: 'tc-1', nombre: 'DOBA S.A.', moneda: 'PYG', cuenta_contable_id: 'pc-1' },
        { id: 'tc-2', nombre: 'DOBA S.A. DOLARES', moneda: 'USD', cuenta_contable_id: null },
      ],
      categorias: [
        { id: 'cat-1', nombre: 'Depósito Bancario', tipo: 'INGRESO', cuenta_contable_id: 'pc-2' },
        { id: 'cat-2', nombre: 'Cheque Debitado', tipo: 'EGRESO', cuenta_contable_id: null },
      ],
      plan: [
        { id: 'pc-1', codigo: '1.01.01.06.01', descripcion: 'BANCO FAMILIAR SAECA' },
        { id: 'pc-2', codigo: '1.01.01.02', descripcion: 'RECAUDACIONES EFECTIVO' },
      ],
    });

    const { tesoreria } = await service.previsualizacionAsientos('emp-1');

    expect(tesoreria.cuentas).toEqual([
      { id: 'tc-1', nombre: 'DOBA S.A.', cuenta: { codigo: '1.01.01.06.01', descripcion: 'BANCO FAMILIAR SAECA' } },
      // La moneda se agrega al nombre para distinguir la cuenta en dólares.
      { id: 'tc-2', nombre: 'DOBA S.A. DOLARES (USD)', cuenta: null },
    ]);
    expect(tesoreria.categorias[0].cuenta).toEqual({ codigo: '1.01.01.02', descripcion: 'RECAUDACIONES EFECTIVO' });
    expect(tesoreria.categorias[1].cuenta).toBeNull();
  });

  it('el mapeo por empresa de una categoría gana sobre la cuenta propia', async () => {
    const service = crear([], {
      categorias: [{ id: 'cat-1', nombre: 'Otro Ingreso', tipo: 'INGRESO', cuenta_contable_id: 'pc-1' }],
      mapeosCategoria: [{ categoria_id: 'cat-1', cuenta_contable_id: 'pc-2' }],
      plan: [
        { id: 'pc-1', codigo: '1.01.01.06.01', descripcion: 'BANCO FAMILIAR SAECA' },
        { id: 'pc-2', codigo: '1.01.03.06.01', descripcion: 'ANTICIPOS A PROVEEDORES LOCALES' },
      ],
    });

    const { tesoreria } = await service.previsualizacionAsientos('emp-1');

    expect(tesoreria.categorias[0].cuenta.codigo).toBe('1.01.03.06.01');
  });

  it('las líneas de tesorería dicen de qué dato salen', async () => {
    const service = crear([]);

    const { situaciones } = await service.previsualizacionAsientos('emp-1');
    const ingreso = situaciones.find((s: any) => s.clave === 'MOVIMIENTO_TES_INGRESO');

    expect(ingreso.lineas.map((l: any) => l.fuente)).toEqual(['CUENTA_TESORERIA', 'CATEGORIA_TESORERIA']);
  });

  it('manda los medios de pago con la cuenta que la empresa eligió para cada uno', async () => {
    const service = crear([], {
      mediosPago: [
        {
          medio_pago_id: 'mp-1',
          codigo: 1,
          descripcion: 'Efectivo',
          cobro: { cuenta_id: 'pc-1', codigo: '1.01.01.02', descripcion: 'RECAUDACIONES EFECTIVO', usable: true },
          pago: null,
        },
      ],
    });

    const { medios_pago, situaciones } = await service.previsualizacionAsientos('emp-1');

    expect(medios_pago[0].cobro.codigo).toBe('1.01.01.02');
    // Y la línea del debe avisa que su cuenta sale del medio de pago.
    const venta = situaciones.find((s: any) => s.clave === 'VENTA_CONTADO');
    expect(venta.lineas[0].fuente).toBe('MEDIO_PAGO_COBRO');
  });

  it('dos categorías con la misma cuenta no comparten el objeto', async () => {
    // Compartir la referencia hacía que la API serializara la segunda como
    // "[Circular]" y la pantalla la mostrara sin cuenta.
    const service = crear([], {
      categorias: [
        { id: 'cat-1', nombre: 'Depósito Bancario', tipo: 'INGRESO', cuenta_contable_id: 'pc-1' },
        { id: 'cat-2', nombre: 'Otro Ingreso', tipo: 'INGRESO', cuenta_contable_id: 'pc-1' },
      ],
      plan: [{ id: 'pc-1', codigo: '1.01.01.02', descripcion: 'RECAUDACIONES EFECTIVO' }],
    });

    const { tesoreria } = await service.previsualizacionAsientos('emp-1');

    expect(tesoreria.categorias[0].cuenta).toEqual(tesoreria.categorias[1].cuenta);
    expect(tesoreria.categorias[0].cuenta).not.toBe(tesoreria.categorias[1].cuenta);
    // Lo que importa: sobrevive a la serialización sin "[Circular]".
    expect(JSON.parse(JSON.stringify(tesoreria)).categorias[1].cuenta.codigo).toBe('1.01.01.02');
  });
  it('cada forma llega con su arreglo de líneas tras el serializador de la API', async () => {
    // La forma vigente compartía el arreglo con `lineas` y el interceptor mandaba
    // la segunda aparición como "[Circular]": la pantalla de Mapeo se rompía.
    const r = await crear([]).previsualizacionAsientos('emp-1');
    const serializado: any = await lastValueFrom(
      new BigIntSerializerInterceptor().intercept(null as any, { handle: () => of(r) }) as any,
    );

    expect(JSON.stringify(serializado)).not.toContain('[Circular]');
    for (const s of serializado.situaciones) {
      expect(Array.isArray(s.lineas)).toBe(true);
      for (const f of s.formas) expect(Array.isArray(f.lineas)).toBe(true);
    }
  });
  describe('reglas_por_concepto: "+ n excepciones" del catálogo (spec §6.3)', () => {
    const regla = (extra: any) => ({
      id: 'r1',
      empresa_id: 'emp-1',
      concepto: 'VENTAS_10',
      orden: 1,
      condiciones: [{ dimension: 'SUCURSAL', operador: 'EN', valores: ['suc-cde'] }],
      modo: 'CUENTA',
      cuenta_id: 'c-ext',
      estado: 'ACTIVA',
      vigente_desde: new Date('2026-01-01'),
      vigente_hasta: null,
      cuenta: { codigo: '4.01.02', descripcion: 'VENTAS EXTERIOR' },
      ...extra,
    });

    it('una regla activa vigente hoy aparece con su frase (con el nombre de la sucursal) y su estado', async () => {
      const service = crear([], { reglas: [regla({})] });
      const r = await service.previsualizacionAsientos('emp-1');
      expect(r.reglas_por_concepto.VENTAS_10).toEqual([
        { frase: 'Si sucursal es Ciudad del Este → 4.01.02 VENTAS EXTERIOR', estado: 'ACTIVA' },
      ]);
      // Solo se piden las activas de la empresa.
      const where = service.prisma.cont_mapeo_reglas.findMany.mock.calls[0][0].where;
      expect(where).toMatchObject({ empresa_id: 'emp-1', estado: 'ACTIVA' });
    });

    it('una activa que todavía no rige hoy no cuenta; sin reglas, objeto vacío', async () => {
      const futura = crear([], { reglas: [regla({ vigente_desde: new Date('2099-01-01') })] });
      expect((await futura.previsualizacionAsientos('emp-1')).reglas_por_concepto).toEqual({});
      expect((await crear([]).previsualizacionAsientos('emp-1')).reglas_por_concepto).toEqual({});
    });
  });

  describe('la forma del asiento sigue al mapeo actual', () => {
    const IMPORTACION = [
      { concepto: 'INVENTARIO', codigo: '1.01.04.01.01', descripcion: 'Mercaderías' },
      { concepto: 'IMPORTACION_TRIBUTOS', codigo: '1.01.04.11', descripcion: 'Importaciones en curso' },
      { concepto: 'IMPORTACION_PAGO_PROVEEDOR', codigo: '1.01.04.11', descripcion: 'Importaciones en curso' },
      { concepto: 'IVA_CREDITO_10', codigo: '1.01.03.05.03', descripcion: 'IVA crédito fiscal' },
    ];
    const PUENTE = [
      { concepto: 'IMPORTACIONES_EN_CURSO', codigo: '1.01.04.11', descripcion: 'Importaciones en curso' },
      { concepto: 'ANTICIPO_DESPACHO', codigo: '1.01.03.06.03', descripcion: 'Anticipos de despacho' },
    ];
    const situacion = (r: any, clave: string) => r.situaciones.find((s: any) => s.clave === clave);
    const cuentas = (s: any, lado: string) =>
      s.lineas.filter((l: any) => l.lado === lado).map((l: any) => l.cuenta?.codigo ?? l.cuenta_dinamica);

    it('sin cuenta puente el despacho carga IMPORTACION_TRIBUTOS contra el proveedor y no hay cierre de importación', async () => {
      const r = await crear(IMPORTACION).previsualizacionAsientos('emp');

      expect(cuentas(situacion(r, 'DESPACHO_IMPORTACION'), 'DEBE')).toContain('1.01.04.11');
      expect(cuentas(situacion(r, 'DESPACHO_IMPORTACION'), 'HABER')).toEqual(['Cuenta del proveedor del embarque']);
      expect(situacion(r, 'CIERRE_IMPORTACION').aplica).toBe(false);
    });

    it('con la cuenta puente el despacho va a importaciones en curso y el cierre la descarga a mercaderías', async () => {
      const r = await crear([...IMPORTACION, ...PUENTE]).previsualizacionAsientos('emp');

      const despacho = situacion(r, 'DESPACHO_IMPORTACION');
      expect(cuentas(despacho, 'DEBE')).toContain('1.01.04.11');
      expect(cuentas(despacho, 'DEBE')).not.toContain('1.01.04.01.01');
      expect(cuentas(despacho, 'HABER')).toEqual(['1.01.03.06.03']);

      const cierre = situacion(r, 'CIERRE_IMPORTACION');
      expect(cierre.aplica).toBe(true);
      expect(cuentas(cierre, 'DEBE')).toEqual(['1.01.04.01.01']);
      expect(cuentas(cierre, 'HABER')).toEqual(['1.01.04.11']);

      expect(cuentas(situacion(r, 'PAGO_IMPORTACION_PROVEEDOR'), 'DEBE')).toEqual(['1.01.04.11']);
      expect(cuentas(situacion(r, 'PAGO_IMPORTACION_DESPACHO'), 'DEBE')).toEqual(['1.01.03.06.03']);
    });
  });
  describe('sin respaldo: la línea sin mapear muestra la recomendación', () => {
    const linea = (r: any, clave: string, concepto: string) =>
      r.situaciones.find((s: any) => s.clave === clave).lineas.find((l: any) => l.concepto === concepto);

    it('la compra de importación sin mapear es una falta con la cuenta recomendada', async () => {
      const r = await crear([{ concepto: 'IMPORTACIONES_EN_CURSO', codigo: '1.01.04.11', descripcion: 'Importaciones en curso' }]).previsualizacionAsientos('emp');
      const l = linea(r, 'COMPRA_IMPORTACION_CREDITO', 'IMPORTACION_COMPRA');
      expect(l.sin_mapear).toBe(true);
      expect(l.recomendacion.codigo).toBe('1.01.04.11');
      expect(l).not.toHaveProperty('respaldo');
      expect(l).not.toHaveProperty('concepto_respaldo');
    });

    it('el cheque sin CHEQUES_EN_CARTERA ya no cae a caja', async () => {
      const r = await crear([{ concepto: 'CAJA_GENERAL', codigo: '1.01.01.01', descripcion: 'Caja' }]).previsualizacionAsientos('emp');
      const l = linea(r, 'VENTA_CONTADO', 'CHEQUES_EN_CARTERA');
      expect(l.sin_mapear).toBe(true);
      expect(l.cuenta).toBeNull();
    });

    it('el despacho usa los conceptos de importación', async () => {
      const r = await crear([]).previsualizacionAsientos('emp');
      const conceptos = r.situaciones.find((s: any) => s.clave === 'DESPACHO_IMPORTACION').lineas.map((l: any) => l.concepto);
      expect(conceptos).toEqual(expect.arrayContaining(['IMPORTACION_TRIBUTOS', 'IMPORTACION_ANTICIPO_IRE']));
    });

    it('la orden de pago no inventa cuenta para el cheque', async () => {
      const r = await crear([]).previsualizacionAsientos('emp');
      const cheque = linea(r, 'ORDEN_PAGO', 'CHEQUES_A_PAGAR');
      expect(cheque.sin_mapear).toBe(true);
      expect(cheque.cuenta_dinamica).toBeNull();
    });

    it('el anticipo de despacho y la compra al contado de importación muestran su falta', async () => {
      const r = await crear([]).previsualizacionAsientos('emp');
      expect(linea(r, 'PAGO_IMPORTACION_DESPACHO', 'ANTICIPO_DESPACHO').sin_mapear).toBe(true);
      expect(linea(r, 'COMPRA_IMPORTACION_CONTADO', 'CAJA_GENERAL').sin_mapear).toBe(true);
    });

    it('en modo SUMAR_A_PRINCIPAL la cuenta es null aunque quede un cuenta_id residual', async () => {
      const service = crear([]);
      service.prisma.cont_mapeo_cuentas.findMany.mockResolvedValue([
        { concepto: 'IVA_NO_DEDUCIBLE', modo: 'SUMAR_A_PRINCIPAL', cuenta: { codigo: '9.9', descripcion: 'Residual' } },
      ]);
      const l = linea(await service.previsualizacionAsientos('emp'), 'GASTO', 'IVA_NO_DEDUCIBLE');
      expect(l.cuenta).toBeNull();
      expect(l.sin_mapear).toBe(false);
    });

    it('IVA no deducible sumado al gasto no es una falta', async () => {
      const service = crear([]);
      service.prisma.cont_mapeo_cuentas.findMany.mockResolvedValue([{ concepto: 'IVA_NO_DEDUCIBLE', modo: 'SUMAR_A_PRINCIPAL', cuenta: null }]);
      const r = await service.previsualizacionAsientos('emp');
      const l = linea(r, 'GASTO', 'IVA_NO_DEDUCIBLE');
      expect(l.sin_mapear).toBe(false);
      expect(l.cuenta).toBeNull();
      expect(l.cuenta_dinamica).toBe('Se suma a la cuenta principal');
    });

    it('IVA no deducible sin mapear recomienda sumar a la principal', async () => {
      const r = await crear([]).previsualizacionAsientos('emp');
      const l = linea(r, 'GASTO', 'IVA_NO_DEDUCIBLE');
      expect(l.sin_mapear).toBe(true);
      expect(l.recomendacion.texto).toMatch(/Sumar a la cuenta principal/);
    });
  });
});
