// PRUEBA DEL FRENO DE LA PUERTA DEL PANEL (2026-07-29)
//
// Antes de esto, el panel de la imprenta aguantaba intentos infinitos desde
// internet: cinco curl seguidos daban 401 401 401 401 401, sin bloqueo, sin
// registro y sin espera.
//
// La prueba va en dos partes a proposito:
//   A. Por la puerta de verdad (HTTP contra el panel publicado), que es lo que
//      hace quien ataca.
//   B. Por dentro (llamando la funcion en la base con direcciones de mentira
//      fijas), porque desde esta maquina la direccion CAMBIA EN CADA PETICION
//      (Cloudflare WARP), y por HTTP no hay forma de probar el freno por
//      bloque de direcciones. Ese hallazgo esta en la migration
//      20260729080000.
//
// Usa un operador de mentira, NUNCA el de Gilberto: probar con su usuario lo
// dejaria trancado 15 minutos. Limpia todo lo que crea.
import { readFileSync } from 'node:fs';

const REF = 'szzhhlevtrcambcoytiz';
const PAT = readFileSync(new URL('../.sb_token', import.meta.url), 'utf8').trim();
const PANEL = process.env.PANEL ?? 'https://sello.gustitoxpress.app';

const USUARIO = 'pruebafreno';
const CLAVE = 'FrenoDePrueba2026!';
const VENTANA_MIN = 15;
const TOPE_USU = 8; // los mismos numeros que estan en fiscal.entrar_panel
const TOPE_IP = 15;

let fallos = 0;
const check = (cond, msg) => {
  console.log((cond ? '  OK  ' : '  XX  ') + msg);
  if (!cond) fallos++;
};

async function sql(query) {
  const r = await fetch(`https://api.supabase.com/v1/projects/${REF}/database/query`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${PAT}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query }),
  });
  const t = await r.text();
  if (!r.ok) throw new Error(t.slice(0, 300));
  return JSON.parse(t);
}
const uno = async (q) => (await sql(q))[0];
const lit = (s) => `'${String(s).replace(/'/g, "''")}'`;

let galleta = '';
async function api(accion, datos) {
  const r = await fetch(`${PANEL}/api/admin`, {
    method: 'POST',
    headers: { 'content-type': 'application/json', ...(galleta ? { cookie: galleta } : {}) },
    body: JSON.stringify({ accion, datos: datos || {} }),
  });
  const set = r.headers.get('set-cookie');
  if (set) galleta = set.split(';')[0];
  return { estado: r.status, cuerpo: await r.json().catch(() => ({})) };
}
const entrar = (usuario, clave) => api('entrar', { usuario, clave });

// Todo lo que crea la prueba lleva nombre reconocible, para borrar SOLO eso al
// final y no llevarse por delante el registro de entradas de verdad.
const MIOS = `(quien like 'prueba%' or quien = 'nadieconesenombre')`;
const limpiarIntentos = () => sql(`delete from fiscal.intentos where tipo='panel' and ${MIOS};`);
const limpiar = () =>
  sql(`delete from fiscal.intentos where tipo='panel' and ${MIOS};
       delete from fiscal.operadores where usuario like 'prueba%';`);

// Corre una tanda de intentos DENTRO de la base, en orden garantizado. Se hace
// con un bucle en plpgsql y no con subconsultas porque el planificador puede
// reordenar subconsultas y darle a uno un resultado bonito y falso.
const tanda = (ips, n, ipFinal, usuarioFinal = 'pruebafinal') => `
  create or replace function pg_temp.tanda(p_ips text[], p_n int, p_final text, p_usu text)
  returns int language plpgsql as $$
  declare i int; v int;
  begin
    for i in 1..p_n loop
      perform espera_seg from fiscal.entrar_panel(
        'prueba' || i::text, 'loquesea', p_ips[1 + (i % array_length(p_ips, 1))]);
    end loop;
    select espera_seg into v from fiscal.entrar_panel(p_usu, 'loquesea', p_final);
    return v;
  end $$;
  select pg_temp.tanda(array[${ips.map(lit).join(',')}], ${n}, ${lit(ipFinal)}, ${lit(usuarioFinal)}) as espera;`;

try {
  await limpiar();
  await sql(`insert into fiscal.operadores (usuario, clave_hash, nombre, activo)
             values (${lit(USUARIO)}, extensions.crypt(${lit(CLAVE)}, extensions.gen_salt('bf', 10)),
                     'Operador de prueba', true)
             on conflict (usuario) do update
               set clave_hash = excluded.clave_hash, activo = true, nombre = excluded.nombre;`);

  console.log('\n== A. POR LA PUERTA DE VERDAD ==');

  console.log('\n-- 1. Un intento fallido ahora SI queda anotado --');
  const primero = await entrar(USUARIO, 'estanoesladeel');
  check(primero.estado === 401, `rebota (HTTP ${primero.estado})`);
  const anotado = await uno(
    `select quien, motivo, desde, grupo, entro from fiscal.intentos
      where tipo='panel' and quien=${lit(USUARIO)} order by en desc limit 1`,
  );
  check(anotado?.entro === false, `queda el rastro del fallo (${anotado?.motivo})`);
  check(!!anotado?.desde, `con la direccion de donde vino (${anotado?.desde})`);
  check(!!anotado?.grupo, `y con su bloque, que es por lo que se cuenta (${anotado?.grupo})`);

  console.log('\n-- 2. A fuerza de intentos la puerta se traba --');
  let n = 1;
  let r = primero;
  while (r.estado !== 429 && n < 32) {
    r = await entrar(USUARIO, 'estanoesladeel');
    n++;
  }
  check(r.estado === 429, `la puerta se traba sola (HTTP ${r.estado} en el intento ${n})`);
  check(/espera/i.test(r.cuerpo.error ?? ''), `y dice cuanto esperar ("${r.cuerpo.error}")`);

  console.log('\n-- 3. Trabado esta trabado, aunque llegue la clave buena --');
  const conLaBuena = await entrar(USUARIO, CLAVE);
  check(conLaBuena.estado === 429, `la clave correcta tampoco entra mientras dure el freno (HTTP ${conLaBuena.estado})`);

  console.log('\n-- 4. Trabar a uno no traba a los demas --');
  const otro = await entrar('pruebaotro', 'loquesea');
  check(otro.estado === 401, `otro usuario sigue recibiendo su 401 normal (HTTP ${otro.estado})`);

  console.log('\n-- 5. Pasada la ventana se destraba y se entra --');
  // En vez de esperar 15 minutos, se envejecen los intentos de la prueba.
  await sql(`update fiscal.intentos set en = en - interval '${VENTANA_MIN + 1} minutes'
              where tipo='panel' and ${MIOS};`);
  const entrada = await entrar(USUARIO, CLAVE);
  check(entrada.estado === 200, `con la clave buena entra (HTTP ${entrada.estado})`);
  check(entrada.cuerpo.nombre === 'Operador de prueba', `y saluda por su nombre (${entrada.cuerpo.nombre})`);
  const rastroBueno = await uno(
    `select entro, motivo from fiscal.intentos where tipo='panel' and quien=${lit(USUARIO)}
      order by en desc limit 1`,
  );
  check(rastroBueno?.entro === true, `la entrada tambien queda anotada (${rastroBueno?.motivo})`);

  console.log('\n-- 6. El panel muestra los golpes en la puerta --');
  const tablero = (await api('tablero')).cuerpo.datos;
  check(tablero.intentos_panel_hoy > 0, `el resumen los cuenta aparte (${tablero.intentos_panel_hoy} hoy)`);
  check(typeof tablero.intentos_fallidos_hoy === 'number', 'y sigue contando aparte las llaves de maquina');

  console.log('\n== B. POR DENTRO, CON DIRECCIONES FIJAS ==');
  // Desde esta maquina la direccion cambia en cada peticion, asi que el freno
  // por bloque no se puede probar por HTTP. Se prueba llamando la funcion.
  const cuantasIps = await uno(
    `select count(distinct desde)::int as n, count(distinct grupo)::int as g
       from fiscal.intentos where tipo='panel' and ${MIOS} and desde is not null`,
  );
  console.log(`      (los ${n + 2} intentos de arriba llegaron con ${cuantasIps.n} direcciones distintas, ${cuantasIps.g} bloques)`);

  console.log('\n-- 7. El tope por usuario son ' + TOPE_USU + ' fallos exactos --');
  await limpiarIntentos();
  const justoAntes = await uno(tanda(['203.0.113.7'], TOPE_USU - 1, '203.0.113.7', 'prueba' + TOPE_USU));
  check(Number(justoAntes.espera) === 0, `con ${TOPE_USU - 1} fallos de otros, un usuario nuevo entra a probar normal`);
  await limpiarIntentos();
  const justoDespues = await uno(`
    create or replace function pg_temp.mismo(p_n int) returns int language plpgsql as $$
    declare i int; v int;
    begin
      for i in 1..p_n loop
        perform espera_seg from fiscal.entrar_panel(${lit(USUARIO)}, 'loquesea', '203.0.113.7');
      end loop;
      select espera_seg into v from fiscal.entrar_panel(${lit(USUARIO)}, ${lit(CLAVE)}, '203.0.113.7');
      return v;
    end $$;
    select pg_temp.mismo(${TOPE_USU}) as espera;`);
  check(
    Number(justoDespues.espera) > 0,
    `al fallo ${TOPE_USU} se traba, y le dice cuanto falta (${Math.round(Number(justoDespues.espera) / 60)} min)`,
  );

  console.log('\n-- 8. El freno cuenta por BLOQUE, no por direccion exacta --');
  await limpiarIntentos();
  // Dos direcciones distintas del mismo /64: tienen que sumar juntas.
  const mismoBloque = await uno(
    tanda(['2001:db8:1:2::a', '2001:db8:1:2::b'], TOPE_IP, '2001:db8:1:2::c', 'pruebanuevo'),
  );
  check(
    Number(mismoBloque.espera) > 0,
    `${TOPE_IP} fallos repartidos en dos direcciones del mismo bloque traban a la tercera`,
  );
  const otroBloque = await uno(
    `select espera_seg as espera from fiscal.entrar_panel('pruebaotrobloque', 'loquesea', '2001:db8:99:99::c')`,
  );
  check(Number(otroBloque.espera) === 0, 'y un bloque distinto sigue atendiendo, no se traba el mundo entero');

  console.log('\n-- 9. Desde un bloque conocido el tope sube --');
  // Si no fuera asi, cualquiera podria dejar a Gilberto trancado afuera de su
  // propio panel nada mas fallando con su usuario.
  await limpiarIntentos();
  const casa = await uno(`
    create or replace function pg_temp.casa() returns int language plpgsql as $$
    declare i int; v int;
    begin
      perform espera_seg from fiscal.entrar_panel(${lit(USUARIO)}, ${lit(CLAVE)}, '203.0.113.7');
      for i in 1..${TOPE_USU} loop
        perform espera_seg from fiscal.entrar_panel(${lit(USUARIO)}, 'loquesea', '203.0.113.7');
      end loop;
      select espera_seg into v from fiscal.entrar_panel(${lit(USUARIO)}, ${lit(CLAVE)}, '203.0.113.7');
      return v;
    end $$;
    select pg_temp.casa() as espera;`);
  check(Number(casa.espera) === 0, `tras entrar bien una vez, ${TOPE_USU} fallos ya no traban esa puerta`);

  console.log('\n-- 10. El reloj no canta cuales usuarios existen --');
  await limpiarIntentos();
  const t = await uno(`
    create or replace function pg_temp.medir(p_usuario text, p_clave text, p_n int)
    returns numeric language plpgsql as $$
    declare t0 timestamptz; i int;
    begin
      t0 := clock_timestamp();
      for i in 1..p_n loop
        perform entro from fiscal.entrar_panel(p_usuario, p_clave, null);
      end loop;
      return round((extract(epoch from (clock_timestamp() - t0)) * 1000 / p_n)::numeric, 2);
    end $$;
    select pg_temp.medir(${lit(USUARIO)}, 'clavemala', 5) as existe,
           pg_temp.medir('nadieconesenombre', 'clavemala', 5) as noexiste;`);
  const razon = Number(t.noexiste) / Number(t.existe);
  check(
    razon > 0.5 && razon < 2,
    `usuario que existe ${t.existe} ms vs uno que no ${t.noexiste} ms (razon ${razon.toFixed(2)})`,
  );
} catch (e) {
  console.log('  XX  ' + String(e).slice(0, 300));
  fallos++;
} finally {
  await limpiar();
}

console.log(fallos === 0 ? '\nFRENO DEL PANEL: TODO VERDE' : `\n${fallos} FALLO(S)`);
process.exit(fallos === 0 ? 0 : 1);
