import { ContabilidadConciliacionService } from './contabilidad-conciliacion.service';
import { correccionesManuales } from './correccion-manual';

/**
 * Regenerar un registro cuyo asiento se revirtió (DOBA, oct-2026): el documento
 * REVERTIDO ocupaba el par (origen_tipo, origen_id) y el integrador lo tomaba
 * como "ya tiene asiento". El mensaje pedía anularlo, y un revertido no se anula.
 */

type Doc = { id: string; origen_id: string | null; estado: string; numero_documento?: string };

const GLOSA = 'Ingreso tesorería — PRESTAMO N°4';
const REVERTIDO_EL = new Date('2026-08-27T19:43:07Z');

function crear(opciones: { generaNuevo?: boolean; lanza?: boolean; correccionManual?: boolean } = {}) {
  const docs: Doc[] = [{ id: 'doc-rev', origen_id: 'm1', estado: 'REVERTIDO', numero_documento: '5B0A5ED0' }];
  const coincide = (d: Doc, where: any) =>
    (where.origen_id === undefined ||
      (typeof where.origen_id === 'object'
        ? where.origen_id.in
          ? where.origen_id.in.includes(d.origen_id)
          : d.origen_id !== null
        : d.origen_id === where.origen_id)) &&
    (!where.estado || d.estado === where.estado);

  const prisma: any = {
    cont_origen_registry: {
      findUnique: jest.fn().mockResolvedValue({
        origen_tipo: 'tes_movimientos',
        activo: true,
        descripcion: 'Movimientos de tesorería',
        tabla_origen: 'tes_movimientos',
        service_name: 'ContabilidadIntegracionService',
        service_method: 'integrarMovimientoTesoreria',
        arg_style: 'id',
        campo_numero: null,
        campo_fecha: null,
        campo_monto: null,
      }),
    },
    cont_documentos: {
      findFirst: jest.fn().mockImplementation(({ where }: any) => docs.find((d) => coincide(d, where)) ?? null),
      findMany: jest.fn().mockImplementation(({ where }: any) =>
        docs.filter((d) => coincide(d, where)).map((d) => ({ ...d, asiento: { glosa: GLOSA, updated_at: REVERTIDO_EL } })),
      ),
      update: jest.fn().mockImplementation(({ where, data }: any) => {
        Object.assign(docs.find((d) => d.id === where.id)!, data);
        return {};
      }),
    },
    cont_asientos: {
      findFirst: jest.fn().mockResolvedValue(null),
      findMany: jest.fn().mockResolvedValue(
        opciones.correccionManual
          ? [{ id: 'as-210', numero: 210, glosa: `CORRECCIÓN: ${GLOSA}`, created_at: new Date('2026-08-27T19:43:48Z') }]
          : [],
      ),
    },
  };

  const integrador = {
    integrarMovimientoTesoreria: jest.fn().mockImplementation(async (id: string) => {
      if (opciones.lanza) throw new Error('No hay período abierto');
      // Igual que el integrador real: si encuentra cualquier documento, no crea nada.
      if (docs.some((d) => d.origen_id === id)) return;
      if (opciones.generaNuevo) docs.push({ id: 'doc-nuevo', origen_id: id, estado: 'CONFIRMADO' });
    }),
  };
  const audit = { log: jest.fn().mockResolvedValue(undefined) };
  const avisos = { sincronizar: jest.fn().mockResolvedValue(undefined) };
  const service = new ContabilidadConciliacionService(
    prisma, {} as any, {} as any, {} as any, audit as any, integrador as any, {} as any, avisos as any,
  );
  jest.spyOn(service as any, 'diagnosticarRegistro').mockResolvedValue({ motivo: 'SIN_MAPEO', descripcion: 'falta mapeo', contexto: {} });
  return { service, docs, integrador, audit, avisos };
}

describe('ContabilidadConciliacionService.regenerarUno con documento REVERTIDO', () => {
  it('libera el revertido y genera el asiento nuevo', async () => {
    const { service, docs, integrador } = crear({ generaNuevo: true });

    const r = await service.regenerarUno('emp-1', 'tes_movimientos', 'm1', 'u1');

    expect(r).toEqual({ id: 'm1', ok: true, cont_documento_id: 'doc-nuevo' });
    expect(integrador.integrarMovimientoTesoreria).toHaveBeenCalledWith('m1');
    // El revertido queda en el historial, sin vínculo al registro.
    expect(docs.find((d) => d.id === 'doc-rev')).toMatchObject({ estado: 'REVERTIDO', origen_id: null });
  });

  it('si no se genera nada, el revertido vuelve a quedar vinculado', async () => {
    const { service, docs } = crear({ generaNuevo: false });

    const r = await service.regenerarUno('emp-1', 'tes_movimientos', 'm1', 'u1');

    expect(r.ok).toBe(false);
    expect(r.motivo).toBe('SIN_MAPEO');
    expect(docs.find((d) => d.id === 'doc-rev')?.origen_id).toBe('m1');
  });

  it('si el integrador falla, también lo revincula', async () => {
    const { service, docs } = crear({ lanza: true });

    const r = await service.regenerarUno('emp-1', 'tes_movimientos', 'm1', 'u1');

    expect(r.ok).toBe(false);
    expect(r.error).toContain('No hay período abierto');
    expect(docs.find((d) => d.id === 'doc-rev')?.origen_id).toBe('m1');
  });

  it('no regenera lo que ya se corrigió a mano: se duplicaría', async () => {
    const { service, docs, integrador } = crear({ generaNuevo: true, correccionManual: true });

    const r = await service.regenerarUno('emp-1', 'tes_movimientos', 'm1', 'u1');

    expect(r.ok).toBe(false);
    expect(r.motivo).toBe('YA_CORREGIDO');
    expect(r.error).toContain('N° 210');
    expect(integrador.integrarMovimientoTesoreria).not.toHaveBeenCalled();
    expect(docs.find((d) => d.id === 'doc-rev')?.origen_id).toBe('m1');
  });
});

describe('regenerar con éxito cierra las alertas de falta de mapeo [RF-3]', () => {
  it('regenerarUno con asiento nuevo sincroniza los avisos de la empresa', async () => {
    const { service, avisos } = crear({ generaNuevo: true });

    await service.regenerarUno('emp-1', 'tes_movimientos', 'm1', 'u1');

    expect(avisos.sincronizar).toHaveBeenCalledTimes(1);
    expect(avisos.sincronizar).toHaveBeenCalledWith('emp-1');
  });

  it('si no se generó nada no sincroniza', async () => {
    const { service, avisos } = crear({ generaNuevo: false });

    await service.regenerarUno('emp-1', 'tes_movimientos', 'm1', 'u1');

    expect(avisos.sincronizar).not.toHaveBeenCalled();
  });

  it('el lote sincroniza una sola vez, no por documento', async () => {
    const { service, avisos } = crear({ generaNuevo: true });

    const r = await service.regenerarLote('emp-1', { origen_tipo: 'tes_movimientos', ids: ['m1', 'm2'] }, 'u1');

    expect(r.ok).toBe(2);
    expect(avisos.sincronizar).toHaveBeenCalledTimes(1);
  });

  it('regenerar todos sincroniza una sola vez para todos los orígenes', async () => {
    const { service, avisos } = crear({ generaNuevo: true });
    jest.spyOn(service, 'listarRegistry').mockResolvedValue([
      { origen_tipo: 'tes_movimientos' },
      { origen_tipo: 'tes_movimientos' },
    ] as any);
    jest.spyOn(service, 'resumen').mockResolvedValue([{ pendientes: 1, pendientes_aplicables: 1 }] as any);
    let n = 0;
    jest.spyOn(service, 'detalle').mockImplementation(async () => ({ items: [{ id: `m${++n}` }] }) as any);

    const r = await service.regenerarTodos('emp-1', {}, 'u1');

    expect(r.total_ok).toBe(2);
    expect(avisos.sincronizar).toHaveBeenCalledTimes(1);
  });

  it('si sincronizar falla, la regeneración igual se informa como exitosa', async () => {
    const { service, avisos } = crear({ generaNuevo: true });
    avisos.sincronizar.mockRejectedValue(new Error('redis caído'));

    const r = await service.regenerarUno('emp-1', 'tes_movimientos', 'm1', 'u1');

    expect(r.ok).toBe(true);
  });
});

describe('correccionesManuales', () => {
  const prismaCon = (manuales: any[]) =>
    ({
      cont_documentos: {
        findMany: jest.fn().mockResolvedValue([{ origen_id: 'm1', asiento: { glosa: GLOSA, updated_at: REVERTIDO_EL } }]),
      },
      cont_asientos: { findMany: jest.fn().mockResolvedValue(manuales) },
    }) as any;

  it('encuentra el asiento manual "CORRECCIÓN: <glosa>" cargado después de revertir', async () => {
    const r = await correccionesManuales(
      prismaCon([{ id: 'as-210', numero: 210, glosa: `CORRECCIÓN: ${GLOSA}`, created_at: new Date('2026-08-27T19:43:48Z') }]),
      'emp-1',
      'tes_movimientos',
    );
    expect(r.get('m1')).toEqual({ asiento_id: 'as-210', numero: 210 });
  });

  it('ignora uno con la misma glosa anterior a la reversión', async () => {
    const r = await correccionesManuales(
      prismaCon([{ id: 'as-9', numero: 9, glosa: `CORRECCIÓN: ${GLOSA}`, created_at: new Date('2026-08-01T10:00:00Z') }]),
      'emp-1',
      'tes_movimientos',
    );
    expect(r.size).toBe(0);
  });

  it('sin corrección cargada, el registro sigue pendiente', async () => {
    const r = await correccionesManuales(prismaCon([]), 'emp-1', 'tes_movimientos');
    expect(r.size).toBe(0);
  });
});
