/**
 * Pruebas contra una base real con roles y vistas aplicados (db/roles.sql, db/vistas/aplicar.sql).
 *
 *   pnpm test:db
 *
 * Necesita en .env:
 *   ERP_DATABASE_URL        rol soporte_ia (lo que usa el servicio)
 *   ERP_ADMIN_DATABASE_URL  superusuario LOCAL, solo para elegir empresas de prueba y verificar
 * Sin ERP_ADMIN_DATABASE_URL se omite. Nunca apuntarlo a producción.
 */
import 'dotenv/config';
import { Pool, PoolClient } from 'pg';
import { alcanceDe, CatalogoService } from 'src/catalogo/catalogo.service';
import { CATALOGO_VISTAS } from 'src/consultas/catalogo-vistas';
import { validarConsulta } from 'src/consultas/validar-sql';
import { envs } from 'src/config/envs';
import { ErpReadonlyService, firmarAlcance } from 'src/erp-readonly/erp-readonly.service';
import { Sesion } from 'src/sesion/sesion.types';

const ADMIN = process.env.ERP_ADMIN_DATABASE_URL;

/** Fija el alcance firmado como lo hace runScoped (usuario de prueba, sin restricción de sucursal). */
async function fijarAlcance(c: PoolClient, empresaId: string, privilegios: Set<string>) {
  const valores = [empresaId, '00000000-0000-0000-0000-000000000000', '*', '*', [...privilegios].sort().join(',')];
  await c.query(
    `SELECT set_config('app.empresa_id', $1, true), set_config('app.usuario_id', $2, true),
            set_config('app.sucursales', $3, true), set_config('app.establecimientos', $4, true),
            set_config('app.privilegios', $5, true), set_config('app.firma', $6, true)`,
    [...valores, firmarAlcance(envs.alcanceSecreto, valores)],
  );
}
if (ADMIN && !/@(localhost|127\.0\.0\.1)(:\d+)?\//.test(ADMIN)) {
  throw new Error('ERP_ADMIN_DATABASE_URL tiene que apuntar a localhost (copia local).');
}
const describeDb = ADMIN ? describe : describe.skip;

describeDb('vistas soporte_ro + catálogo (base real)', () => {
  let admin: Pool;
  let erp: ErpReadonlyService;
  let catalogo: CatalogoService;
  let empresaA: string;
  let empresaB: string;
  let usuarioA: string;

  const TODOS = new Set([
    'VEN_FAC_FACTURA_VER',
    'VEN_NC_NOTA_CREDITO_VER',
    'VEN_NR_NOTA_REMISION_VER',
    'FIS_SIF_SIFEN_VER',
  ]);

  const sesion = (empresaId: string, parcial: Partial<Sesion> = {}): Sesion => ({
    usuarioId: usuarioA,
    username: 'test',
    nombre: 'Test',
    empresaId,
    empresaRuc: null,
    empresaRazonSocial: null,
    empresaTipo: 'subsidiary',
    isSuperadmin: false,
    accesoElevado: false,
    privilegios: TODOS,
    alcance: null,
    esNovasis: false,
    ...parcial,
  });

  beforeAll(async () => {
    admin = new Pool({ connectionString: ADMIN, max: 1 });
    const empresas = await admin.query<{ empresa_id: string }>(
      `SELECT empresa_id FROM factura_cab WHERE empresa_id IS NOT NULL
        GROUP BY empresa_id ORDER BY count(*) DESC LIMIT 2`,
    );
    [empresaA, empresaB] = empresas.rows.map((r) => r.empresa_id);
    const u = await admin.query<{ id: string }>(`SELECT id FROM usuario LIMIT 1`);
    usuarioA = u.rows[0].id;

    erp = new ErpReadonlyService();
    await erp.onModuleInit();
    catalogo = new CatalogoService(erp);
  });

  afterAll(async () => {
    await erp?.onModuleDestroy();
    await admin?.end();
  });

  it('el servicio está conectado con un rol sin superusuario ni BYPASSRLS (si no, onModuleInit habría fallado)', () => {
    expect(erp).toBeDefined();
  });

  it('conexión reciclada: después de una transacción con contexto, una consulta sin contexto FALLA', async () => {
    const pool = new Pool({ connectionString: process.env.ERP_DATABASE_URL, max: 1 });
    try {
      const c = await pool.connect();
      try {
        await c.query('BEGIN');
        await fijarAlcance(c, empresaA, TODOS);
        const r = await c.query(`SELECT count(*)::int AS n FROM soporte_ro.v_documentos`);
        expect(r.rows[0].n).toBeGreaterThan(0);
        await c.query('COMMIT');
        // Misma conexión, sin set_config: la variable quedó en '' y la guarda tiene que lanzar.
        await expect(c.query(`SELECT count(*) FROM soporte_ro.v_documentos`)).rejects.toThrow(
          /app\.empresa_id no está fijado/,
        );
      } finally {
        c.release();
      }
    } finally {
      await pool.end();
    }
  });

  it('el SQL no puede cambiar el alcance: set_config a otra empresa o un privilegio extra invalida la firma', async () => {
    const pool = new Pool({ connectionString: process.env.ERP_DATABASE_URL, max: 1 });
    try {
      const c = await pool.connect();
      try {
        await c.query('BEGIN');
        await fijarAlcance(c, empresaA, new Set(['VEN_NC_NOTA_CREDITO_VER']));
        // Lo que intentaría un SQL inyectado: otra empresa en la misma consulta.
        await expect(
          c.query(
            `WITH x AS (SELECT set_config('app.empresa_id', $1, true)) SELECT count(*) FROM x, soporte_ro.v_documentos`,
            [empresaB],
          ),
        ).rejects.toThrow(/firma/);
        await c.query('ROLLBACK');
        await c.query('BEGIN');
        await fijarAlcance(c, empresaA, new Set(['VEN_NC_NOTA_CREDITO_VER']));
        await c.query(`SELECT set_config('app.privilegios', 'VEN_FAC_FACTURA_VER,VEN_NC_NOTA_CREDITO_VER', true)`);
        await expect(c.query(`SELECT count(*) FROM soporte_ro.v_documentos`)).rejects.toThrow(/firma/);
        await c.query('ROLLBACK');
      } finally {
        c.release();
      }
    } finally {
      await pool.end();
    }
  });

  it('soporte_ia no puede leer tablas de public', async () => {
    const pool = new Pool({ connectionString: process.env.ERP_DATABASE_URL, max: 1 });
    try {
      await expect(pool.query(`SELECT 1 FROM public.factura_cab LIMIT 1`)).rejects.toThrow(
        /permiso denegado|permission denied/,
      );
      await expect(pool.query(`SELECT 1 FROM public.usuario LIMIT 1`)).rejects.toThrow(
        /permiso denegado|permission denied/,
      );
    } finally {
      await pool.end();
    }
  });

  it('aislamiento: lo que lista la empresa A es todo de A', async () => {
    const r = await catalogo.listarDocumentos(sesion(empresaA), {});
    if (!r.ok) throw new Error(r.detalle);
    expect(r.documentos.length).toBeGreaterThan(0);
    const ids = r.documentos.map((d) => d.id);
    const ajenos = await admin.query(
      `SELECT id FROM (
         SELECT id, empresa_id FROM factura_cab
         UNION ALL SELECT id, empresa_id FROM nota_credito_cab
         UNION ALL SELECT id, empresa_id FROM nota_remision_cab) d
        WHERE d.id = ANY($1::uuid[]) AND d.empresa_id IS DISTINCT FROM $2`,
      [ids, empresaA],
    );
    expect(ajenos.rowCount).toBe(0);
  });

  it('un documento de B no se encuentra con la sesión de A, ni por id', async () => {
    const { rows } = await admin.query<{ id: string; nro: string }>(
      `SELECT id, dest || '-' || dpunexp || '-' || dnumdoc AS nro FROM factura_cab WHERE empresa_id = $1 LIMIT 1`,
      [empresaB],
    );
    const deB = rows[0];
    const porId = await catalogo.estadoDocumento(sesion(empresaA), 'FACTURA', deB.id);
    expect(porId).toMatchObject({ ok: false, motivo: 'no_encontrado' });

    const busqueda = await catalogo.buscarDocumento(sesion(empresaA), deB.nro, 'FACTURA');
    if (busqueda.ok) {
      // Mismo número puede existir en A: lo que importa es que no aparezca el de B.
      expect(busqueda.candidatos.map((c) => c.id)).not.toContain(deB.id);
    } else {
      expect(busqueda.motivo).toBe('no_encontrado');
    }
  });

  it('buscar por número y diagnosticar un documento real', async () => {
    const { rows } = await admin.query<{ id: string; nro: string }>(
      `SELECT id, dest || '-' || dpunexp || '-' || dnumdoc AS nro FROM factura_cab
        WHERE empresa_id = $1 AND estado_sifen IS NOT NULL ORDER BY dfeemide DESC LIMIT 1`,
      [empresaA],
    );
    const doc = rows[0];
    const busqueda = await catalogo.buscarDocumento(sesion(empresaA), `mi factura ${doc.nro}`);
    if (!busqueda.ok) throw new Error(busqueda.detalle);
    expect(busqueda.candidatos.map((c) => c.id)).toContain(doc.id);

    const estado = await catalogo.estadoDocumento(sesion(empresaA), 'FACTURA', doc.id);
    if (!estado.ok) throw new Error(estado.detalle);
    expect(estado.documento.id).toBe(doc.id);
    expect(estado.veredicto.situacion).toBeDefined();
  });

  it('sin el privilegio del tipo: sin_permiso (no "no existe")', async () => {
    const sinFacturas = sesion(empresaA, { privilegios: new Set(['VEN_NC_NOTA_CREDITO_VER']) });
    const r = await catalogo.buscarDocumento(sinFacturas, '001-001-0000001', 'FACTURA');
    expect(r).toMatchObject({ ok: false, motivo: 'sin_permiso' });
  });

  it('alcance por sucursal: con un establecimiento inexistente no ve nada', async () => {
    const acotada = sesion(empresaA, { alcance: { sucursalIds: [], establecimientos: ['999'] } });
    const r = await catalogo.listarDocumentos(acotada, {});
    if (!r.ok) throw new Error(r.detalle);
    expect(r.documentos).toHaveLength(0);
  });

  it('salud de la empresa', async () => {
    const r = await catalogo.saludEmpresa(sesion(empresaA));
    expect(r.ok).toBe(true);
  });

  it('guías: devuelve fragmentos o nada, nunca falla', async () => {
    const r = await catalogo.buscarGuia(sesion(empresaA), '¿cómo anulo una factura?');
    expect(Array.isArray(r)).toBe(true);
  });

  // ── Consultas de negocio (src/consultas) ──

  const conTodo = async () => {
    const { rows } = await admin.query<{ codigo: string }>(`SELECT codigo FROM privilegios WHERE codigo IS NOT NULL`);
    return sesion(empresaA, { privilegios: new Set(rows.map((r) => r.codigo)) });
  };

  it('cada vista del catálogo se puede consultar como soporte_ia (planificar incluido)', async () => {
    // El 24/10/2026 v_productos fallaba al PLANIFICAR: índices sobre public.f_unaccent y
    // soporte_ia sin USAGE sobre public. Esto lo atrapa para cualquier vista.
    const s = await conTodo();
    const fallas: string[] = [];
    for (const v of CATALOGO_VISTAS) {
      await erp
        .runScoped(alcanceDe(s), (q) => q(`SELECT * FROM soporte_ro.${v.vista} LIMIT 1`))
        .catch((err: Error) => fallas.push(`${v.vista}: ${err.message}`));
    }
    expect(fallas).toEqual([]);
  });

  it('el SQL del modelo corre validado y con su propio tope de tiempo', async () => {
    const s = await conTodo();
    const v = validarConsulta(
      'SELECT moneda, count(*) AS n, sum(total) AS total FROM soporte_ro.v_ventas WHERE vigente GROUP BY moneda',
      new Set(CATALOGO_VISTAS.map((x) => x.vista)),
    );
    expect(v.ok).toBe(true);
    if (!v.ok) return;
    const filas = await erp.runScoped(alcanceDe(s), async (q) => {
      await q(`SET LOCAL statement_timeout = '4s'`);
      return q<{ moneda: string; n: string }>(v.sql);
    });
    expect(filas.length).toBeGreaterThan(0);
  });

  it('aunque el validador fallara, un set_config en el SQL del modelo no cambia de empresa', async () => {
    // Se ejecuta SIN validar a propósito: la frontera es la base, no el validador.
    const s = await conTodo();
    await expect(
      erp.runScoped(alcanceDe(s), (q) =>
        q(`WITH x AS (SELECT set_config('app.empresa_id', $1, true)) SELECT count(*) FROM x, soporte_ro.v_ventas`, [
          empresaB,
        ]),
      ),
    ).rejects.toThrow(/firma/);
  });

  it('las ventas de A son todas de A, y sin privilegio no se ve ninguna', async () => {
    const s = await conTodo();
    const ids = await erp.runScoped(alcanceDe(s), (q) => q<{ id: string }>(`SELECT id FROM soporte_ro.v_ventas`));
    expect(ids.length).toBeGreaterThan(0);
    const { rows } = await admin.query<{ n: string }>(
      `SELECT count(*) AS n FROM factura_cab WHERE id = ANY($1::uuid[]) AND empresa_id IS DISTINCT FROM $2`,
      [ids.map((r) => r.id), empresaA],
    );
    expect(Number(rows[0].n)).toBe(0);

    const sinNada = sesion(empresaA, { privilegios: new Set() });
    const ninguna = await erp.runScoped(alcanceDe(sinNada), (q) => q(`SELECT 1 FROM soporte_ro.v_ventas LIMIT 1`));
    expect(ninguna).toEqual([]);
  });
});
