import { ActividadesService } from './actividades/actividades.service';
import { CatalogosService } from './catalogos/catalogos.service';
import { FechasEspecialesService } from './fechas/fechas-especiales.service';
import { LeadsService } from './leads/leads.service';
import { OportunidadesService } from './oportunidades/oportunidades.service';
import { ReportesCrmService } from './reportes/reportes-crm.service';
import { TimelineService } from './timeline/timeline.service';

/**
 * Aislamiento entre empresas (ticket CRM-21).
 *
 * Todas las tablas del CRM están particionadas por `empresa_id`. Un olvido no se
 * nota en la pantalla —cada empresa de prueba ve sus propios datos— y recién
 * aparece cuando un cliente lee los leads de otro. Esto comprueba, servicio por
 * servicio, que el filtro viaja hasta Prisma.
 *
 * Es un test de forma, no de datos: mira el `where` que recibe Prisma. Si alguien
 * agrega una consulta nueva sin `empresa_id`, este archivo no la ve. Lo que sí
 * garantiza es que las de F1 no se pierdan en un refactor.
 */

const EMPRESA = 'emp-1';

/** Busca `empresa_id` en el where, incluso anidado (`personas: { empresa_id }`). */
function filtraPorEmpresa(where: unknown, empresa = EMPRESA): boolean {
  if (!where || typeof where !== 'object') return false;
  for (const [clave, valor] of Object.entries(where as Record<string, unknown>)) {
    if (clave === 'empresa_id' && valor === empresa) return true;
    if (valor && typeof valor === 'object' && filtraPorEmpresa(valor, empresa)) return true;
  }
  return false;
}

const tabla = (filas: unknown[] = []) => ({
  findMany: jest.fn().mockResolvedValue(filas),
  findFirst: jest.fn().mockResolvedValue(null),
  findUnique: jest.fn().mockResolvedValue(null),
  count: jest.fn().mockResolvedValue(0),
  aggregate: jest.fn().mockResolvedValue({ _sum: {}, _count: { _all: 0 } }),
  groupBy: jest.fn().mockResolvedValue([]),
  create: jest.fn().mockResolvedValue({ id: 'x' }),
  update: jest.fn().mockResolvedValue({ id: 'x' }),
  updateMany: jest.fn().mockResolvedValue({ count: 0 }),
});

/** Todas las llamadas de lectura que recibió cualquier tabla del mock. */
function wheresLeidos(prisma: Record<string, any>): { tabla: string; metodo: string; where: unknown }[] {
  const salida: { tabla: string; metodo: string; where: unknown }[] = [];
  for (const [nombre, modelo] of Object.entries(prisma)) {
    if (!modelo || typeof modelo !== 'object') continue;
    for (const metodo of ['findMany', 'findFirst', 'count', 'aggregate', 'groupBy']) {
      const fn = (modelo as Record<string, any>)[metodo];
      if (!fn?.mock) continue;
      for (const llamada of fn.mock.calls) {
        salida.push({ tabla: nombre, metodo, where: llamada[0]?.where });
      }
    }
  }
  return salida;
}

/**
 * Tablas que no llevan `empresa_id` propio y se filtran por su padre. Cada una
 * tiene que estar justificada: la lista es la excepción, no la regla.
 */
const SIN_EMPRESA_PROPIA: Record<string, string> = {
  // Se consultan por id ya validado contra la empresa en el paso anterior.
  productos: 'catálogo global de la empresa, se filtra por los ids que trae la oportunidad',
  vendedores_cobradores: 'se consulta por id ya acotado, o por usuario_id del propio token',
  moneda: 'catálogo global del sistema',
  crm_oportunidad_items: 'cuelga de la oportunidad, que ya se validó',
  perfiles_privilegios: 'pertenece al usuario del token, no a la empresa',
};

const auditFalso = { log: jest.fn().mockResolvedValue(undefined) } as never;

describe('CRM — aislamiento por empresa', () => {
  const revisar = (prisma: Record<string, any>) => {
    const lecturas = wheresLeidos(prisma);
    expect(lecturas.length).toBeGreaterThan(0);

    const sinFiltro = lecturas.filter(
      (l) => !SIN_EMPRESA_PROPIA[l.tabla] && !filtraPorEmpresa(l.where),
    );
    // El mensaje nombra la consulta exacta: sin eso, el fallo no dice dónde mirar.
    expect(sinFiltro.map((l) => `${l.tabla}.${l.metodo}`)).toEqual([]);
  };

  it('LeadsService.findAll filtra por empresa', async () => {
    const prisma: any = { crm_leads: tabla(), clientes: tabla(), crm_oportunidades: tabla() };
    await new LeadsService(prisma, auditFalso, {} as never).findAll(EMPRESA, { texto: 'ana' });
    revisar(prisma);
  });

  it('OportunidadesService.findAll filtra por empresa', async () => {
    const prisma: any = {
      crm_oportunidades: tabla(),
      crm_etapas: tabla(),
      crm_pipelines: tabla(),
      vendedores_cobradores: tabla(),
    };
    await new OportunidadesService(prisma, auditFalso, {} as never, {} as never).findAll(
      EMPRESA,
      { verAjenas: true },
      {},
    );
    revisar(prisma);
  });

  it('ActividadesService.agenda filtra por empresa', async () => {
    const prisma: any = { crm_actividades: tabla(), crm_oportunidades: tabla() };
    await new ActividadesService(prisma, auditFalso).agenda(EMPRESA, 'u-1');
    revisar(prisma);
  });

  it('FechasEspecialesService.agenda filtra por empresa', async () => {
    const prisma: any = { crm_fechas_especiales: tabla(), crm_fechas_comerciales: tabla() };
    await new FechasEspecialesService(prisma).agenda(EMPRESA);
    revisar(prisma);
  });

  it('CatalogosService lista cada catálogo filtrando por empresa', async () => {
    const prisma: any = {
      crm_origenes: tabla(),
      crm_motivos: tabla(),
      crm_calendarios: tabla(),
      crm_sla_politicas: tabla(),
    };
    const service = new CatalogosService(prisma, auditFalso);
    await service.findOrigenes(EMPRESA);
    await service.findMotivos(EMPRESA);
    await service.findCalendarios(EMPRESA);
    await service.findSla(EMPRESA);
    revisar(prisma);
  });

  it('ReportesCrmService calcula los cinco reportes filtrando por empresa', async () => {
    const prisma: any = {
      crm_etapas: tabla(),
      crm_oportunidades: tabla(),
      crm_origenes: tabla(),
      crm_leads: tabla(),
      crm_motivos: tabla(),
      vendedores_cobradores: tabla(),
    };
    await new ReportesCrmService(prisma).resumen(EMPRESA, { verAjenas: true }, {});
    revisar(prisma);
  });

  /**
   * El timeline es el más sensible: junta facturas, cobros y presupuestos. Una
   * fuente sin filtro mostraría movimientos de otra empresa en la ficha.
   */
  it('TimelineService.timeline filtra todas sus fuentes por empresa', async () => {
    const prisma: any = {
      clientes: { ...tabla(), findFirst: jest.fn().mockResolvedValue({ id: 'cli-1' }) },
      crm_actividades: tabla(),
      crm_oportunidades: tabla(),
      presupuesto_cab: tabla(),
      factura_cab: tabla(),
      nota_credito_cab: tabla(),
      recibos_cobro: tabla(),
      crm_leads: tabla(),
    };
    await new TimelineService(prisma).timeline('cli-1', EMPRESA, {});
    revisar(prisma);
  });

  describe('el detector no se deja engañar', () => {
    it('reconoce el filtro anidado que usa clientes', () => {
      expect(filtraPorEmpresa({ deleted: false, personas: { empresa_id: EMPRESA } })).toBe(true);
    });

    it('no acepta el empresa_id de otra empresa', () => {
      expect(filtraPorEmpresa({ empresa_id: 'emp-2' })).toBe(false);
    });

    it('un where sin empresa es un fallo', () => {
      expect(filtraPorEmpresa({ estado: 'abierta' })).toBe(false);
      expect(filtraPorEmpresa(undefined)).toBe(false);
    });
  });
});
