import { BadRequestException, NotFoundException } from '@nestjs/common';
import { MapeoCuentasService } from 'src/contabilidad/services/mapeo-cuentas.service';
import { ReglasMapeoService } from 'src/contabilidad/services/reglas-mapeo.service';
import { RecontabilizacionService } from './recontabilizacion.service';
import { VistaPreviaMapeoService } from './vista-previa-mapeo.service';
import { VistaPreviaMapeoController } from './vista-previa-mapeo.controller';
import { PATH_METADATA } from '@nestjs/common/constants';
import { REQUIRE_PERMISSION_KEY } from 'src/auth/decorators/require-permission.decorator';

/**
 * Vista previa de reglas con un documento real (spec 2026-10-13 §6.2): corre el
 * integrador sobre el Prisma sombra, nunca escribe, y muestra el asiento
 * vigente al lado del que saldría hoy, con la traza de cada línea.
 */
const EMP = 'emp-1';

const crear = (opciones: { reglas?: any[]; vigente?: boolean } = {}) => {
  const reglas = opciones.reglas ?? [];
  const escrituras = () => ({ create: jest.fn(), update: jest.fn(), upsert: jest.fn(), delete: jest.fn(), deleteMany: jest.fn(), updateMany: jest.fn() });
  const cuentas: Record<string, any> = {
    'c-gasto': { id: 'c-gasto', codigo: '6.1.01', descripcion: 'GASTOS GENERALES', active: true, acepta_movimientos: true, empresa_id: EMP },
    'c-usd': { id: 'c-usd', codigo: '6.1.09', descripcion: 'GASTOS EXTERIOR', active: true, acepta_movimientos: true, empresa_id: EMP },
    caja: { id: 'caja', codigo: '1.01.01.01', descripcion: 'CAJA', active: true, acepta_movimientos: true, empresa_id: EMP },
  };
  const prisma: any = {
    cont_origen_registry: {
      findUnique: jest.fn(async ({ where }: any) =>
        where.origen_tipo === 'gasto_cab'
          ? {
              origen_tipo: 'gasto_cab',
              activo: true,
              descripcion: 'Gastos',
              tabla_origen: 'gasto_cab',
              campo_fecha: 'fecha_emision',
              campo_monto: 'total',
              campo_numero: 'numero_factura',
              filtro_extra: { anulado: false },
              service_name: 'ContabilidadIntegracionService',
              service_method: 'integrarGasto',
              arg_style: 'id',
            }
          : null,
      ),
      // Registro para "Probar con un documento": activos e inactivos, uno sin
      // integrador para simular y uno de los que no se recontabilizan.
      findMany: jest.fn(async ({ where }: any) =>
        [
          { origen_tipo: 'factura_cab', descripcion: 'Factura de venta', modulo: 'VENTAS', service_name: 'ContabilidadIntegracionService', service_method: 'integrarFactura', activo: true },
          { origen_tipo: 'gasto_cab', descripcion: 'Gastos', modulo: 'COMPRAS', service_name: 'ContabilidadIntegracionService', service_method: 'integrarGasto', activo: true },
          { origen_tipo: 'rendicion_adelanto', descripcion: 'Rendición', modulo: 'RRHH', service_name: 'RendicionViaticosContabilidadService', service_method: 'x', activo: true },
          { origen_tipo: 'viejo', descripcion: 'Viejo', modulo: 'X', service_name: 'ContabilidadIntegracionService', service_method: 'integrarGasto', activo: false },
        ].filter((r) => where?.activo === undefined || r.activo === where.activo),
      ),
    },
    gasto_cab: {
      ...escrituras(),
      findFirst: jest.fn(async ({ where }: any) => (where.id === 'g1' && where.empresa_id === EMP ? { id: 'g1' } : null)),
      findUnique: jest.fn(async () => ({ id: 'g1', moneda: 'USD', total: 100 })),
      findMany: jest.fn(async () => [
        { id: 'g1', numero_factura: '001-001-0000123', fecha_emision: new Date('2026-10-01'), total: 100 },
      ]),
    },
    cont_documentos: {
      ...escrituras(),
      findFirst: jest.fn(async () =>
        opciones.vigente === false
          ? null
          : {
              id: 'doc-1',
              numero_documento: 'G-123',
              asiento: {
                fecha: new Date('2026-10-01'),
                lineas: [
                  { cuenta_id: 'c-gasto', debe_pyg: 100, haber_pyg: 0, descripcion: null, mapeo_concepto: 'GASTOS_GENERALES', mapeo_regla_id: null, mapeo_nivel: 'DEFECTO', cuenta: { codigo: '6.1.01', descripcion: 'GASTOS GENERALES' } },
                  { cuenta_id: 'caja', debe_pyg: 0, haber_pyg: 100, descripcion: null, mapeo_concepto: null, mapeo_regla_id: null, mapeo_nivel: 'MEDIO_PAGO', cuenta: { codigo: '1.01.01.01', descripcion: 'CAJA' } },
                ],
              },
            },
      ),
      findMany: jest.fn(async () => []),
    },
    cont_asientos: escrituras(),
    // Saldos para el impacto por cuenta de `simular`.
    cont_asientos_det: { groupBy: jest.fn(async () => []) },
    audit_logs: escrituras(),
    cont_mapeo_reglas: {
      ...escrituras(),
      findMany: jest.fn(async ({ where }: any) =>
        reglas
          .filter((r) => r.empresa_id === where.empresa_id && (!where.id || where.id.in.includes(r.id)))
          .map((r) => ({ ...r, cuenta: cuentas[r.cuenta_id] ?? null, creador: null, activador: null, retirador: null })),
      ),
    },
    cont_mapeo_cuentas: {
      ...escrituras(),
      findUnique: jest.fn(async () => ({ cuenta_id: 'c-gasto', modo: 'CUENTA', cuenta: cuentas['c-gasto'] })),
    },
    cont_plan_cuentas: {
      findFirst: jest.fn(async ({ where }: any) => cuentas[where.id] ?? null),
      findMany: jest.fn(async ({ where }: any) => where.id.in.map((id: string) => cuentas[id]).filter(Boolean)),
    },
  };

  // Integrador mínimo con la misma forma que los reales: resuelve por el mapeo
  // (con reglas), anota la traza, crea el documento y graba el asiento.
  const mapeo = Object.create(MapeoCuentasService.prototype) as any;
  mapeo.prisma = prisma;
  const integrador: any = {
    prisma,
    mapeo,
    asientos: { crearConfirmado: jest.fn() },
    opcionesMapeo: undefined,
    async integrarGasto(this: any, id: string) {
      const g = await this.prisma.gasto_cab.findUnique({ where: { id } });
      const res = await this.mapeo.crearResolvedor(EMP, {
        fecha: new Date('2026-10-01'),
        dimensiones: { MONEDA: g.moneda },
        incluirBorradores: this.opcionesMapeo?.incluirBorradores,
      });
      const cuenta = await res.cuenta('GASTOS_GENERALES');
      const lineas = res.anotar([
        { cuenta_id: cuenta, concepto: 'GASTOS_GENERALES', debe_moneda: 100, haber_moneda: 0 },
        { cuenta_id: res.marcar('caja', 'MEDIO_PAGO'), debe_moneda: 0, haber_moneda: 100 },
      ]);
      const doc = await this.prisma.cont_documentos.create({ data: { origen_id: id } });
      await this.asientos.crearConfirmado(EMP, null, doc.id, { fecha: '2026-10-01', lineas });
    },
  };
  const asientos: any = { validarPartidaDoble: jest.fn(), validarLineas: jest.fn(async () => undefined) };
  const tipoCambio: any = { convertirAPYG: jest.fn(async (_e: string, m: number) => m) };
  const recontabilizacion = new RecontabilizacionService(prisma, asientos, tipoCambio, { log: jest.fn() } as any, integrador, {} as any);
  const reglasService = new ReglasMapeoService(prisma, { sincronizar: jest.fn() } as any, mapeo);
  const conciliacion: any = { enriquecerItems: jest.fn(async () => new Map()) };
  const service = new VistaPreviaMapeoService(prisma, recontabilizacion, reglasService, conciliacion);
  return { service, prisma, integrador, conciliacion };
};

const reglaUsd = (estado: string) => ({
  id: 'r1',
  empresa_id: EMP,
  concepto: 'GASTOS_GENERALES',
  orden: 1,
  condiciones: [{ dimension: 'MONEDA', operador: 'EN', valores: ['USD'] }],
  modo: 'CUENTA',
  cuenta_id: 'c-usd',
  estado,
  vigente_desde: new Date('2026-01-01'),
  vigente_hasta: null,
});

const noEscribio = (prisma: any) => {
  for (const modelo of ['gasto_cab', 'cont_documentos', 'cont_asientos', 'audit_logs', 'cont_mapeo_reglas', 'cont_mapeo_cuentas']) {
    for (const m of ['create', 'update', 'upsert', 'delete', 'deleteMany', 'updateMany']) {
      expect(prisma[modelo][m]).not.toHaveBeenCalled();
    }
  }
};

describe('VistaPreviaMapeoService.vistaPrevia', () => {
  it('un documento de otra empresa → 404', async () => {
    const { service, integrador } = crear();
    await expect(service.vistaPrevia(EMP, { origenTipo: 'gasto_cab', origenId: 'g-ajeno' })).rejects.toBeInstanceOf(NotFoundException);
    expect(integrador.asientos.crearConfirmado).not.toHaveBeenCalled();
  });

  it('un origen que no está en el registro → 400 claro', async () => {
    const { service } = crear();
    await expect(service.vistaPrevia(EMP, { origenTipo: 'tes_transferencias', origenId: 't1' })).rejects.toThrow(
      /no se puede simular todavía/,
    );
  });

  it('sin borradores: igual al vigente, con la traza por línea, y no escribe nada', async () => {
    const { service, prisma, integrador } = crear({ reglas: [reglaUsd('BORRADOR')] });
    const r = await service.vistaPrevia(EMP, { origenTipo: 'gasto_cab', origenId: 'g1' });
    expect(r.estado).toBe('IGUAL');
    expect(r.saldria!.map((l) => [l.codigo, l.debe, l.haber, l.traza?.nivel])).toEqual([
      ['6.1.01', 100, 0, 'DEFECTO'],
      ['1.01.01.01', 0, 100, 'MEDIO_PAGO'],
    ]);
    expect(r.vigente!.map((l) => [l.codigo, l.traza?.nivel])).toEqual([
      ['6.1.01', 'DEFECTO'],
      ['1.01.01.01', 'MEDIO_PAGO'],
    ]);
    // El integrador real (no su sombra) nunca se llama.
    expect(integrador.asientos.crearConfirmado).not.toHaveBeenCalled();
    noEscribio(prisma);
  });

  it('con incluirBorradores aplica la regla borrador y la línea dice cuál', async () => {
    const { service, prisma } = crear({ reglas: [reglaUsd('BORRADOR')] });
    const r = await service.vistaPrevia(EMP, { origenTipo: 'gasto_cab', origenId: 'g1', incluirBorradores: true });
    expect(r.estado).toBe('CAMBIA');
    const gasto = r.saldria!.find((l) => l.debe > 0)!;
    expect(gasto).toMatchObject({
      cuenta_id: 'c-usd',
      codigo: '6.1.09',
      traza: { concepto: 'GASTOS_GENERALES', nivel: 'REGLA', regla_id: 'r1', regla: { orden: 1, estado: 'BORRADOR', frase: 'Si moneda es USD → 6.1.09 GASTOS EXTERIOR' } },
    });
    expect(r.diferencias.map((d) => [d.codigo, d.antes, d.despues])).toEqual([
      ['6.1.01', 100, 0],
      ['6.1.09', 0, 100],
    ]);
    noEscribio(prisma);
  });

  it('con incluirBorradores el borrador aplica aunque su vigencia empiece después del documento', async () => {
    const { service } = crear({ reglas: [{ ...reglaUsd('BORRADOR'), vigente_desde: new Date('2027-01-01') }] });
    const con = await service.vistaPrevia(EMP, { origenTipo: 'gasto_cab', origenId: 'g1', incluirBorradores: true });
    expect(con.estado).toBe('CAMBIA');
    expect(con.saldria!.find((l) => l.debe > 0)).toMatchObject({ cuenta_id: 'c-usd', traza: { nivel: 'REGLA', regla_id: 'r1' } });
    // Una ACTIVA con esa misma vigencia no aplica al documento de 2026-10-01.
    const activa = crear({ reglas: [{ ...reglaUsd('ACTIVA'), vigente_desde: new Date('2027-01-01') }] });
    const r = await activa.service.vistaPrevia(EMP, { origenTipo: 'gasto_cab', origenId: 'g1', incluirBorradores: true });
    expect(r.estado).toBe('IGUAL');
  });

  it('un documento sin asiento vigente: vigente null y lo que saldría', async () => {
    const { service } = crear({ vigente: false });
    const r = await service.vistaPrevia(EMP, { origenTipo: 'gasto_cab', origenId: 'g1' });
    expect(r.vigente).toBeNull();
    expect(r.saldria).toHaveLength(2);
    expect(r.estado).toBe('CAMBIA');
  });

  it('si hoy no se podría asentar, ERROR con el mensaje (regla con cuenta desactivada)', async () => {
    const { service, prisma } = crear({ reglas: [{ ...reglaUsd('ACTIVA'), cuenta_id: 'c-baja' }] });
    prisma.cont_plan_cuentas.findFirst.mockImplementation(async ({ where }: any) =>
      where.id === 'c-baja' ? { codigo: 'OLD-6', active: false, acepta_movimientos: true } : null,
    );
    const r = await service.vistaPrevia(EMP, { origenTipo: 'gasto_cab', origenId: 'g1' });
    expect(r.estado).toBe('ERROR');
    expect(r.error).toMatch(/^No hay cuenta mapeada para el concepto.*regla n\.º 1/);
    expect(r.saldria).toBeNull();
  });
});

describe('VistaPreviaMapeoService.probar', () => {
  it('toma los últimos N del tipo de documento (más recientes primero) y resume', async () => {
    const { service, prisma } = crear({ reglas: [reglaUsd('BORRADOR')] });
    prisma.cont_documentos.findMany.mockResolvedValue([
      { id: 'doc-1', origen_id: 'g1', numero_documento: 'G-1', asiento: { id: 'a1', fecha: new Date('2026-10-01'), lineas: [
        { cuenta_id: 'c-gasto', debe_pyg: 100, haber_pyg: 0 },
        { cuenta_id: 'caja', debe_pyg: 0, haber_pyg: 100 },
      ] } },
    ]);
    const sin = await service.probar(EMP, { origenTipo: 'gasto_cab', ultimos: 20 });
    expect(prisma.cont_documentos.findMany).toHaveBeenCalledWith(expect.objectContaining({ orderBy: { created_at: 'desc' }, take: 20 }));
    expect(sin).toMatchObject({ origen_tipo: 'gasto_cab', analizados: 1, iguales: 1, cambian: 0, error: 0 });
    expect(sin.items).toEqual([expect.objectContaining({ origen_id: 'g1', estado: 'IGUAL' })]);

    const con = await service.probar(EMP, { origenTipo: 'gasto_cab', ultimos: 20, incluirBorradores: true });
    expect(con).toMatchObject({ iguales: 0, cambian: 1 });
    noEscribio(prisma);
  });

  it('más de 100 se recorta a 100; un origen no registrado → 400', async () => {
    const { service, prisma } = crear();
    await service.probar(EMP, { origenTipo: 'gasto_cab', ultimos: 500 });
    expect(prisma.cont_documentos.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 100 }));
    await expect(service.probar(EMP, { origenTipo: 'pago_bancard', ultimos: 10 })).rejects.toBeInstanceOf(BadRequestException);
  });
});

describe('VistaPreviaMapeoService.buscarDocumentos', () => {
  it('busca por número en la tabla del origen, solo de la empresa, hasta 20', async () => {
    const { service, prisma } = crear();
    const r = await service.buscarDocumentos(EMP, 'gasto_cab', '123');
    const args = prisma.gasto_cab.findMany.mock.calls[0][0];
    expect(args.where).toMatchObject({ empresa_id: EMP, anulado: false, numero_factura: { contains: '123', mode: 'insensitive' } });
    expect(args.take).toBe(20);
    expect(args.orderBy).toEqual({ fecha_emision: 'desc' });
    expect(r).toEqual([{ id: 'g1', numero: '001-001-0000123', fecha: new Date('2026-10-01'), total: 100, contraparte: null }]);
  });

  it('un origen no registrado → 400', async () => {
    const { service } = crear();
    await expect(service.buscarDocumentos(EMP, 'cxp_empleado_pago', '1')).rejects.toThrow(/no se puede simular todavía/);
  });
});

describe('VistaPreviaMapeoService.origenesSimulables', () => {
  it('lista los orígenes activos del registro con integrador que se puede correr en sombra, sin las rendiciones', async () => {
    const { service, prisma } = crear();
    const r = await service.origenesSimulables();
    expect(prisma.cont_origen_registry.findMany.mock.calls[0][0].where).toEqual({ activo: true });
    // factura_cab no tiene método en el integrador de prueba: no se ofrece.
    expect(r).toEqual([{ origen_tipo: 'gasto_cab', descripcion: 'Gastos', modulo: 'COMPRAS' }]);
  });

  it('el endpoint GET origenes-simulables pide CONT_MAP_MAPEO_VER', () => {
    const handler = VistaPreviaMapeoController.prototype.origenesSimulables;
    expect(Reflect.getMetadata(PATH_METADATA, handler)).toBe('origenes-simulables');
    expect(Reflect.getMetadata(REQUIRE_PERMISSION_KEY, handler)).toEqual({ modulo: 'CONTABILIDAD', privilegio: 'CONT_MAP_MAPEO_VER' });
  });
});
