import { conFaltaDeMapeoComoError, crearColectorAsientos, crearIntegradorSombra, crearPrismaSombra } from './integrador-sombra';
import { agrupar, mismasLineas } from './recontabilizacion.service';

/**
 * La simulación de la recontabilización corre los integradores reales sobre un
 * Prisma que no puede escribir. Si alguna de estas garantías se rompe, simular
 * modificaría la contabilidad de la empresa.
 */
describe('Prisma sombra', () => {
  const real = () => ({
    factura_cab: {
      findUnique: jest.fn().mockResolvedValue({ id: 'f1', total: 100 }),
      update: jest.fn(),
      create: jest.fn(),
    },
    cont_documentos: { findFirst: jest.fn().mockResolvedValue({ id: 'doc-viejo' }), create: jest.fn() },
    $transaction: jest.fn(),
    $executeRaw: jest.fn(),
  });

  it('deja pasar las lecturas a la base real', async () => {
    const r = real();
    const sombra = crearPrismaSombra(r);
    await expect(sombra.factura_cab.findUnique({ where: { id: 'f1' } } as any)).resolves.toEqual({ id: 'f1', total: 100 });
  });

  it('rechaza cualquier escritura que no sea el documento contable', () => {
    const r = real();
    const sombra = crearPrismaSombra(r);
    expect(() => sombra.factura_cab.update({} as any)).toThrow(/escritura bloqueada en factura_cab.update/);
    expect(() => (sombra as any).$executeRaw``).toThrow(/bloqueada/);
    expect(r.factura_cab.update).not.toHaveBeenCalled();
  });

  it('hace como si el documento no tuviera asiento, para saltear la idempotencia', async () => {
    const r = real();
    const sombra = crearPrismaSombra(r);
    await expect(sombra.cont_documentos.findFirst({} as any)).resolves.toBeNull();
    const creado = await (sombra.cont_documentos as any).create({ data: { tipo: 'X' } });
    expect(creado.id).toEqual(expect.any(String));
    expect(r.cont_documentos.create).not.toHaveBeenCalled();
  });

  it('corre la transacción sobre la sombra, no sobre la base', async () => {
    const r = real();
    const sombra = crearPrismaSombra(r);
    const resultado = await (sombra as any).$transaction(async (tx: any) => {
      expect(() => tx.factura_cab.create({})).toThrow(/bloqueada/);
      return 'ok';
    });
    expect(resultado).toBe('ok');
    expect(r.$transaction).not.toHaveBeenCalled();
  });
});

describe('colector de asientos', () => {
  const deps = () => ({
    convertirAPYG: jest.fn().mockImplementation(async (_e: string, monto: number, moneda: string) =>
      moneda === 'USD' ? monto * 7_350 : monto,
    ),
    validarPartidaDoble: jest.fn().mockImplementation((d: number, h: number) => {
      if (Math.abs(d - h) > 1) throw new Error('El asiento no está balanceado');
    }),
    validarLineas: jest.fn().mockResolvedValue(undefined),
  });

  it('captura el asiento convertido a guaraníes en vez de grabarlo', async () => {
    const d = deps();
    const colector = crearColectorAsientos(d);
    await colector.crearConfirmado('emp-1', null, 'doc', {
      fecha: '2026-10-08',
      moneda_origen: 'USD',
      lineas: [
        { cuenta_id: 'banco-usd', debe_moneda: 20, haber_moneda: 0 },
        { cuenta_id: 'gastos', debe_moneda: 0, haber_moneda: 20 },
      ],
    });
    expect(colector.capturados[0].lineas.map((l) => [l.cuenta_id, l.debe_pyg, l.haber_pyg])).toEqual([
      ['banco-usd', 147_000, 0],
      ['gastos', 0, 147_000],
    ]);
    expect(d.validarLineas).toHaveBeenCalled();
  });

  it('un asiento que no cuadra falla igual que el real', async () => {
    const colector = crearColectorAsientos(deps());
    await expect(
      colector.crearConfirmado('emp-1', null, 'doc', {
        fecha: '2026-10-08',
        lineas: [{ cuenta_id: 'a', debe_moneda: 100, haber_moneda: 0 }],
      }),
    ).rejects.toThrow(/no está balanceado/);
    expect(colector.capturados).toHaveLength(0);
  });

  it('el integrador sombra usa el Prisma sombra y el colector, y conserva las otras dependencias', async () => {
    class IntegradorFalso {
      constructor(
        public prisma: any,
        public asientos: any,
        public mapeo: any,
      ) {}
      async integrar(id: string) {
        const cuenta = await this.mapeo.getCuenta();
        await this.prisma.$transaction(async (tx: any) => {
          const doc = await tx.cont_documentos.create({ data: { origen_id: id } });
          await this.asientos.crearConfirmado('emp-1', null, doc.id, {
            fecha: '2026-10-08',
            lineas: [
              { cuenta_id: cuenta, debe_moneda: 5, haber_moneda: 0 },
              { cuenta_id: 'otra', debe_moneda: 0, haber_moneda: 5 },
            ],
          });
        });
      }
    }
    const asientosReal = { crearConfirmado: jest.fn() };
    const real = new IntegradorFalso(
      { cont_documentos: { create: jest.fn() }, $transaction: jest.fn() },
      asientosReal,
      { getCuenta: async () => 'caja' },
    );
    const colector = crearColectorAsientos(deps());
    const sombra = crearIntegradorSombra(real, crearPrismaSombra(real.prisma), colector);

    await sombra.integrar('f1');

    expect(colector.capturados[0].lineas[0].cuenta_id).toBe('caja');
    expect(asientosReal.crearConfirmado).not.toHaveBeenCalled();
    expect(real.prisma.cont_documentos.create).not.toHaveBeenCalled();
  });
});

describe('comparación de asientos', () => {
  it('compara por cuenta, sumando líneas repetidas y con tolerancia de 1 Gs', () => {
    const guardado = agrupar([
      { cuenta_id: 'iva', debe_pyg: 0, haber_pyg: 50_000 },
      { cuenta_id: 'iva', debe_pyg: 0, haber_pyg: 10_000 },
      { cuenta_id: 'caja', debe_pyg: 60_000.4, haber_pyg: 0 },
    ]);
    const nuevo = agrupar([
      { cuenta_id: 'iva', debe_pyg: 0, haber_pyg: 60_000 },
      { cuenta_id: 'caja', debe_pyg: 60_001, haber_pyg: 0 },
    ]);
    expect(mismasLineas(guardado, nuevo)).toBe(true);
  });

  it('otra cuenta es un cambio', () => {
    const guardado = agrupar([
      { cuenta_id: 'caja', debe_pyg: 100, haber_pyg: 0 },
      { cuenta_id: 'ventas', debe_pyg: 0, haber_pyg: 100 },
    ]);
    const nuevo = agrupar([
      { cuenta_id: 'banco', debe_pyg: 100, haber_pyg: 0 },
      { cuenta_id: 'ventas', debe_pyg: 0, haber_pyg: 100 },
    ]);
    expect(mismasLineas(guardado, nuevo)).toBe(false);
  });
});

describe('falta de mapeo en simulación y recontabilización', () => {
  /**
   * Desde 2026-10-13 el integrador se traga la falta de mapeo (deja constancia y
   * avisa) para no frenar la operación del usuario. En la simulación y al rehacer
   * asientos eso es peligroso: "no generó nada" se leería como "al documento ya no
   * le corresponde asiento" (SOBRA) y el asiento vigente se quitaría.
   */
  const { ContabilidadIntegracionService } = jest.requireActual('../contabilidad/services/integracion.service');
  const { FaltaMapeoError } = jest.requireActual('../contabilidad/services/falta-mapeo.error');
  const integrador = () => {
    const s = Object.create(ContabilidadIntegracionService.prototype);
    s.logger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() };
    s.prisma = {};
    s.avisosMapeo = { registrarFaltaMapeo: jest.fn().mockResolvedValue(undefined) };
    s.integrar = function (id: string) {
      return this.conAvisoDeMapeo('gasto_cab', id, async () => {
        throw new FaltaMapeoError('emp-1', ['GASTOS_GENERALES']);
      });
    };
    return s;
  };

  it('el integrador real se la traga y avisa', async () => {
    const real = integrador();
    await expect(real.integrar('g1')).resolves.toBeUndefined();
    expect(real.avisosMapeo.registrarFaltaMapeo).toHaveBeenCalled();
  });

  it('la sombra la deja ver como error y no avisa (no escribe)', async () => {
    const real = integrador();
    const sombra = crearIntegradorSombra(real, crearPrismaSombra({}), crearColectorAsientos({
      convertirAPYG: async (_e: string, m: number) => m,
      validarPartidaDoble: () => undefined,
    } as any));
    await expect((sombra as any).integrar('g1')).rejects.toBeInstanceOf(FaltaMapeoError);
    expect(real.avisosMapeo.registrarFaltaMapeo).not.toHaveBeenCalled();
  });

  it('conFaltaDeMapeoComoError la deja ver; con registrar, además avisa', async () => {
    const real = integrador();
    await expect((conFaltaDeMapeoComoError(real) as any).integrar('g1')).rejects.toBeInstanceOf(FaltaMapeoError);
    expect(real.avisosMapeo.registrarFaltaMapeo).not.toHaveBeenCalled();

    await expect((conFaltaDeMapeoComoError(real, { registrar: true }) as any).integrar('g1')).rejects.toBeInstanceOf(FaltaMapeoError);
    expect(real.avisosMapeo.registrarFaltaMapeo).toHaveBeenCalledWith({
      empresaId: 'emp-1', origenTipo: 'gasto_cab', origenId: 'g1', conceptos: ['GASTOS_GENERALES'],
    });
  });
});

describe('sombra para la vista previa de reglas', () => {
  it('el colector conserva la traza de cada línea', async () => {
    const colector = crearColectorAsientos({ convertirAPYG: async (_e, m) => m, validarPartidaDoble: () => undefined });
    await colector.crearConfirmado('emp', null, 'doc', {
      fecha: '2026-10-01',
      lineas: [
        { cuenta_id: 'a', debe_moneda: 10, haber_moneda: 0, mapeo_concepto: 'VENTAS_10', mapeo_regla_id: 'r1', mapeo_nivel: 'REGLA' },
        { cuenta_id: 'b', debe_moneda: 0, haber_moneda: 10 },
      ],
    });
    expect(colector.capturados[0].lineas).toEqual([
      expect.objectContaining({ cuenta_id: 'a', mapeo_concepto: 'VENTAS_10', mapeo_regla_id: 'r1', mapeo_nivel: 'REGLA' }),
      expect.objectContaining({ cuenta_id: 'b', mapeo_concepto: null, mapeo_regla_id: null, mapeo_nivel: null }),
    ]);
  });

  it('opcionesMapeo va solo en la copia, nunca en el integrador real', () => {
    class Integrador {
      opcionesMapeo?: { incluirBorradores?: boolean };
      prisma = {};
    }
    const real = new Integrador();
    const colector = crearColectorAsientos({ convertirAPYG: async (_e, m) => m, validarPartidaDoble: () => undefined });
    const sombra = crearIntegradorSombra(real, {}, colector, { opcionesMapeo: { incluirBorradores: true } });
    expect(sombra.opcionesMapeo).toEqual({ incluirBorradores: true });
    expect(real.opcionesMapeo).toBeUndefined();
    expect(crearIntegradorSombra(real, {}, colector).opcionesMapeo).toBeUndefined();
  });
});
