import { RecontabilizacionService } from './recontabilizacion.service';

/**
 * Aplicar una recontabilización aparta el asiento vigente y le pide al
 * integrador uno nuevo. Si el nuevo no sale, el documento NO puede quedar sin
 * asiento: se restaura el anterior.
 */
describe('RecontabilizacionService.aplicar', () => {
  const crear = (opciones: { generaNuevo: boolean; lanza?: boolean; dejaBorrador?: boolean }) => {
    const docs: any[] = [];
    const prisma: any = {
      cont_origen_registry: {
        findUnique: jest.fn().mockResolvedValue({
          origen_tipo: 'gasto_cab',
          activo: true,
          descripcion: 'Gastos',
          tabla_origen: 'gasto_cab',
          service_name: 'ContabilidadIntegracionService',
          service_method: 'integrarGasto',
          arg_style: null,
        }),
      },
      cont_documentos: {
        findMany: jest.fn().mockImplementation(({ where }: any) =>
          where?.estado === 'CONFIRMADO'
            ? [{ id: 'doc-viejo', origen_id: 'g1', numero_documento: 'G-1', asiento: { id: 'as-viejo', fecha: new Date('2026-06-01'), lineas: [] } }]
            : docs.filter((d) => d.origen_id === where.origen_id),
        ),
        findFirst: jest.fn().mockImplementation(({ where }: any) =>
          docs.find((d) => d.origen_id === where.origen_id && (!where.estado || d.estado === where.estado)) ?? null,
        ),
        update: jest.fn().mockResolvedValue({}),
        deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
      },
      cont_asientos: {
        update: jest.fn().mockResolvedValue({}),
        deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
        // Fotos para la auditoría: el viejo pagaba con Caja, el nuevo con Banco.
        findFirst: jest.fn().mockResolvedValue({ id: 'as-nuevo' }),
        findUnique: jest.fn().mockImplementation(({ where }: any) => {
          const cuentaPago = where.id === 'as-viejo' ? { codigo: '1.01.01.01', descripcion: 'CAJA' } : { codigo: '1.01.01.06.01', descripcion: 'BANCO' };
          return Promise.resolve({
            id: where.id,
            numero: where.id === 'as-viejo' ? 12 : 150,
            fecha: new Date('2026-06-01'),
            glosa: 'Gasto G-1',
            total_debe_pyg: 100000,
            periodo: { mes: 6, anio: 2026 },
            documento: { numero_documento: 'G-1' },
            lineas: [
              { debe_pyg: 100000, haber_pyg: 0, descripcion: null, cuenta: { codigo: '10.04', descripcion: 'PUBLICIDAD' } },
              { debe_pyg: 0, haber_pyg: 100000, descripcion: null, cuenta: cuentaPago },
            ],
          });
        }),
      },
      cont_recontabilizaciones: { create: jest.fn().mockResolvedValue({}) },
      $transaction: jest.fn().mockImplementation((arg: any) => (typeof arg === 'function' ? arg(prisma) : Promise.all(arg))),
    };
    const integrador = {
      integrarGasto: jest.fn().mockImplementation(async () => {
        if (opciones.lanza) throw new Error('No hay cuenta mapeada para el concepto "X"');
        if (opciones.generaNuevo) docs.push({ id: 'doc-nuevo', origen_id: 'g1', estado: 'CONFIRMADO' });
        if (opciones.dejaBorrador) docs.push({ id: 'doc-borrador', origen_id: 'g1', estado: 'BORRADOR' });
      }),
    };
    const audit = { log: jest.fn() };
    const service = new RecontabilizacionService(
      prisma,
      {} as any,
      {} as any,
      audit as any,
      integrador as any,
      {} as any,
    );
    return { service, prisma, audit };
  };

  it('reemplaza: el viejo queda REEMPLAZADO y se registra el par', async () => {
    const { service, prisma } = crear({ generaNuevo: true });

    const r = await service.aplicar('emp-1', 'gasto_cab', ['g1'], 'user-1');

    expect(r.items[0]).toMatchObject({ ok: true, doc_nuevo_id: 'doc-nuevo' });
    expect(prisma.cont_documentos.update).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: 'doc-viejo' }, data: expect.objectContaining({ estado: 'REEMPLAZADO', origen_id: null }) }),
    );
    expect(prisma.cont_recontabilizaciones.create).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ doc_anterior_id: 'doc-viejo', doc_nuevo_id: 'doc-nuevo' }) }),
    );
  });

  it('audita cada documento: asiento anterior, nuevo y qué cuentas cambiaron', async () => {
    const { service, audit } = crear({ generaNuevo: true });

    await service.aplicar('emp-1', 'gasto_cab', ['g1'], 'user-1');

    const doc = audit.log.mock.calls.map((c: any) => c[0]).find((e: any) => e.action === 'CONT_RECONTABILIZAR_DOC');
    expect(doc).toMatchObject({ entity_id: 'g1', user_id: 'user-1' });
    expect(doc.descripcion).toContain('Gastos G-1: asiento N° 12 (6/2026');
    expect(doc.descripcion).toContain('reemplazado por N° 150');
    expect(doc.descripcion).toContain('1.01.01.01 CAJA: -100.000 → 0');
    expect(doc.descripcion).toContain('1.01.01.06.01 BANCO: 0 → -100.000');
    expect(doc.old_value.asiento.lineas).toHaveLength(2);
    expect(doc.new_value.cuentas_que_cambian).toHaveLength(2);
  });

  it('si falla, también queda auditado con el motivo y que se conservó el asiento', async () => {
    const { service, audit } = crear({ generaNuevo: false, lanza: true });

    await service.aplicar('emp-1', 'gasto_cab', ['g1'], 'user-1');

    const falla = audit.log.mock.calls.map((c: any) => c[0]).find((e: any) => e.action === 'CONT_RECONTABILIZAR_FALLA');
    expect(falla.descripcion).toMatch(/No hay cuenta mapeada.*Se conservó el asiento N° 12/);
  });

  it('si el integrador falla, el asiento anterior vuelve a quedar vigente', async () => {
    const { service, prisma } = crear({ generaNuevo: false, lanza: true });

    const r = await service.aplicar('emp-1', 'gasto_cab', ['g1'], 'user-1');

    expect(r.items[0].ok).toBe(false);
    expect(r.items[0].error).toMatch(/No hay cuenta mapeada/);
    expect(prisma.cont_documentos.update).toHaveBeenLastCalledWith(
      expect.objectContaining({ where: { id: 'doc-viejo' }, data: expect.objectContaining({ estado: 'CONFIRMADO', origen_id: 'g1' }) }),
    );
    expect(prisma.cont_asientos.update).toHaveBeenLastCalledWith(
      expect.objectContaining({ where: { id: 'as-viejo' }, data: expect.objectContaining({ estado: 'CONFIRMADO' }) }),
    );
    expect(prisma.cont_recontabilizaciones.create).not.toHaveBeenCalled();
  });

  it('un borrador que deja el integrador se borra antes de restaurar, y no cuenta como "sobrante"', async () => {
    const { service, prisma } = crear({ generaNuevo: false, dejaBorrador: true });

    const r = await service.aplicar('emp-1', 'gasto_cab', ['g1'], 'user-1', { quitarSobrantes: true });

    expect(r.items[0].ok).toBe(false);
    expect(prisma.cont_documentos.deleteMany).toHaveBeenCalledWith({ where: { id: { in: ['doc-borrador'] } } });
    expect(prisma.cont_documentos.update).toHaveBeenLastCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ estado: 'CONFIRMADO', origen_id: 'g1' }) }),
    );
  });

  it('sin pedirlo, un documento que hoy no genera asiento no pierde el que tiene', async () => {
    const { service, prisma } = crear({ generaNuevo: false });

    const r = await service.aplicar('emp-1', 'gasto_cab', ['g1'], 'user-1');

    expect(r.items[0].ok).toBe(false);
    expect(prisma.cont_documentos.update).toHaveBeenLastCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ estado: 'CONFIRMADO' }) }),
    );
  });

  it('con quitar sobrantes, el asiento de un documento anulado se quita sin reemplazo', async () => {
    const { service, prisma } = crear({ generaNuevo: false });

    const r = await service.aplicar('emp-1', 'gasto_cab', ['g1'], 'user-1', { quitarSobrantes: true });

    expect(r.items[0]).toMatchObject({ ok: true, quitado: true });
    expect(prisma.cont_recontabilizaciones.create).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ doc_nuevo_id: null }) }),
    );
  });

  it('las rendiciones de viáticos no se recontabilizan', async () => {
    const { service } = crear({ generaNuevo: true });
    await expect(service.aplicar('emp-1', 'rendicion_adelanto', ['r1'])).rejects.toThrow(/viáticos/);
  });
});
