// PRUEBA DE LA ETAPA 2 (salon local-first) contra la BD VIVA:
//  FASE 1 (online): se baja el espejo del menu del demo a disco.
//  FASE 2 (OFFLINE DE VERDAD): con puro disco e impresoras virtuales, el salon
//    toma pedido de mesa (imprime por puesto), cobra la mesa, hace una venta
//    directa, y sobrevive a un "reinicio" (persistencia atomica).
//  FASE 3 (vuelve el internet): el sincronizador sube el buzon a la nube;
//    se verifica cuenta/pedidos/pagos en la BD, el LATIDO del cerebrito, y que
//    resubir la misma venta NO duplica nada (idempotencia).
// Env: SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_KEY.
import { createClient } from '@supabase/supabase-js';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { crearImpresoraVirtual } from './impresora_virtual.mjs';
import { refrescarEspejo, cargarEspejo } from '../dist/espejo.js';
import { ColaImpresion } from '../dist/impresoras.js';
import { crearSalon } from '../dist/salon.js';
import { iniciarSincronizador } from '../dist/sincronizador.js';
import { iniciarPos } from '../dist/pos.js';
import { exigirPuertosLibres } from './puerto_libre.mjs';

// Si hay un cerebrito real corriendo en esta maquina, NO seguir: la suite le
// pegaria al POS de produccion en el 8738 (PIN ajeno, datos ajenos).
await exigirPuertosLibres([8738]);

const URL = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL;
const ANON = process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
const SERVICE = process.env.SUPABASE_SERVICE_KEY;
if (!URL || !ANON || !SERVICE) {
  console.log('Faltan SUPABASE_URL / SUPABASE_ANON_KEY / SUPABASE_SERVICE_KEY');
  process.exit(1);
}

let fallos = 0;
const check = (c, m) => {
  console.log((c ? '  OK  ' : '  XX  ') + m);
  if (!c) fallos++;
};
const espera = (ms) => new Promise((r) => setTimeout(r, ms));
const hasta = async (fn, ms, paso = 500) => {
  const tope = Date.now() + ms;
  while (Date.now() < tope) {
    if (fn()) return true;
    await espera(paso);
  }
  return fn();
};
// Igual que en prueba_local: los tickets van en ASCII puro desde v2.6.5, asi
// que las comparaciones se hacen sin acentos (y '·' sale como '-').
const plano = (t) => t.normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/\s+/g, ' ');

const svc = createClient(URL, SERVICE, { auth: { persistSession: false } });
const { data: rest } = await svc.from('restaurantes').select('id').eq('slug', 'demo-gustito').maybeSingle();
const REST = rest.id;

const EMAIL = 'localfirst.test@gustitoxpress.app';
const CLAVE = 'LocalFirst2026!';
let uid;
let impCocina;
let impBarra;
let sincro;
let idsLimpieza = { pedidos: [], cuenta: null };
const base = mkdtempSync(join(tmpdir(), 'cerebrito-lf-'));

const limpiar = async () => {
  try {
    sincro?.detener();
  } catch {
    /* ya */
  }
  await impCocina?.cerrar();
  await impBarra?.cerrar();
  for (const pid of idsLimpieza.pedidos) await svc.from('pedidos').delete().eq('id', pid);
  if (idsLimpieza.cuenta) await svc.from('cuentas_mesa').delete().eq('id', idsLimpieza.cuenta);
  await svc.from('cerebritos').delete().eq('restaurante_id', REST);
  if (uid) await svc.auth.admin.deleteUser(uid).catch(() => {});
  rmSync(base, { recursive: true, force: true });
};

try {
  // ===== Preparacion: puestos demo + encargado temporal =====
  await svc.from('puestos_impresion').upsert(
    [
      { restaurante_id: REST, nombre: 'Cocina', orden: 1, es_defecto: true },
      { restaurante_id: REST, nombre: 'Barra', orden: 2, es_defecto: false },
    ],
    { onConflict: 'restaurante_id,nombre' },
  );
  const { data: lista } = await svc.auth.admin.listUsers();
  for (const u of lista?.users ?? []) if (u.email === EMAIL) await svc.auth.admin.deleteUser(u.id);
  const { data: u } = await svc.auth.admin.createUser({ email: EMAIL, password: CLAVE, email_confirm: true });
  uid = u.user.id;
  await svc.from('perfiles').upsert({ id: uid, rol: 'encargado', restaurante_id: REST, nombre: 'LocalFirst Test' });

  // ===== FASE 1: bajar el espejo (con internet) =====
  const cli = createClient(URL, ANON, { auth: { persistSession: false } });
  await cli.auth.signInWithPassword({ email: EMAIL, password: CLAVE });
  const espejoBajado = await refrescarEspejo(cli, REST, base);
  check(espejoBajado.productos.length > 0 && espejoBajado.puestos.length >= 2, 'FASE 1: espejo del menu bajado a disco');
  await cli.auth.signOut();

  // ===== FASE 2: OFFLINE (puro disco + LAN) =====
  let espejo = cargarEspejo(base);
  check(!!espejo, 'FASE 2: el espejo carga desde disco (sin nube)');
  const comida = espejo.productos.find((p) => {
    const cat = espejo.categorias.find((c) => c.id === p.categoria_id);
    return p.disponible && cat && !/bebida/i.test(cat.nombre);
  });
  const bebida = espejo.productos.find((p) => {
    const cat = espejo.categorias.find((c) => c.id === p.categoria_id);
    return p.disponible && cat && /bebida/i.test(cat.nombre);
  });

  impCocina = await crearImpresoraVirtual(9440);
  impBarra = await crearImpresoraVirtual(9441);
  const logs = [];
  const cola = new ColaImpresion(
    [
      { puesto: 'Cocina', tipo: 'tcp', host: '127.0.0.1', puerto: 9440 },
      { puesto: 'Barra', tipo: 'tcp', host: '127.0.0.1', puerto: 9441 },
    ],
    (m) => logs.push(m),
  );
  let salon = crearSalon({ base, espejo: () => espejo, cola, log: (m) => logs.push(m), hayDespacho: false });

  const p1 = salon.tomarPedidoMesa({
    mesa: '7',
    items: [
      { producto_id: comida.id, cantidad: 2, nota: 'Salsa: Tártara · Sin cebolla', extra_usd: 0 },
      { producto_id: bebida.id, cantidad: 1 },
    ],
    mesero: 'María',
  });
  check(/^.+-L0001$/.test(p1.codigo), 'pedido local con serie propia: ' + p1.codigo);
  const impreso = await hasta(() => impCocina.tickets.length >= 1 && impBarra.tickets.length >= 1, 15_000);
  check(impreso, 'OFFLINE: la comanda salio por puesto (cocina y barra) sin tocar la nube');
  const tC = plano(impCocina.tickets.join(' '));
  check(tC.includes('MESA 7') && tC.includes(plano('2x ' + comida.nombre)) && tC.includes('Maria'), 'la comanda trae mesa, renglon y quien atiende');
  check(!tC.includes(plano(bebida.nombre)) && plano(impBarra.tickets.join(' ')).includes(plano('1x ' + bebida.nombre)), 'el ruteo por puesto funciona igual offline');

  // PRE-CUENTA antes de cobrar (no fiscal, sale por la impresora de cuenta).
  salon.imprimirCuenta('7');
  const preOk = await hasta(() => plano(impCocina.tickets.join(' ')).includes('CUENTA - MESA 7'), 10_000);
  const tPre = plano(impCocina.tickets.join(' '));
  check(preOk && tPre.includes('ESTE TICKET NO ES FACTURA') && tPre.includes('TOTAL'), 'la PRE-CUENTA imprime con total y leyenda no-fiscal');

  salon.cobrarMesa({ mesa: '7', metodo: 'pago_movil', referencia: '4321', propina_usd: 1, mesero: 'María' });
  const pd = salon.ventaDirecta({ items: [{ producto_id: comida.id, cantidad: 1 }], metodo: 'efectivo', mesero: 'Carlos' });
  check(salon.resumen().ventasPorSubir === 2 && salon.resumen().mesasAbiertas === 0, 'mesa cobrada + venta directa: 2 ventas en el buzon, mesa libre');

  // "Reinicio" de la laptop: otro salon desde el mismo disco.
  salon = crearSalon({ base, espejo: () => espejo, cola, log: (m) => logs.push(m), hayDespacho: false });
  check(salon.resumen().ventasPorSubir === 2, 'tras un reinicio, el buzon sobrevive en disco (persistencia atomica)');
  const ventaMesa = salon.ventasPendientes().find((v) => v.tipo === 'mesa');
  idsLimpieza.cuenta = ventaMesa.cuenta.id;
  idsLimpieza.pedidos = salon.ventasPendientes().flatMap((v) => v.pedidos.map((p) => p.id));

  // El POS de las tablets tambien vive sin nube.
  iniciarPos({
    pin: () => '2468',
    espejo: () => espejo,
    salon: () => salon,
    enReserva: () => false,
    nubeViva: () => false,
    log: (m) => logs.push(m),
  });
  await espera(400);
  const mal = await fetch('http://127.0.0.1:8738/pos/entrar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"pin":"0000"}' });
  check(mal.status === 400, 'POS: PIN malo rebota');
  const okE = await (await fetch('http://127.0.0.1:8738/pos/entrar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"pin":"2468"}' })).json();
  const datos = await (await fetch('http://127.0.0.1:8738/pos/datos', { headers: { 'x-pos': okE.token } })).json();
  check(datos.productos.length > 0 && datos.mesas.length > 0 && datos.nubeViva === false, 'POS: entrega el menu y las mesas del espejo, marcando "sin internet"');

  // ===== FASE 3: vuelve el internet -> el sincronizador sube el buzon =====
  const cargaIdem = JSON.parse(JSON.stringify({ cuenta: ventaMesa.cuenta, pedidos: ventaMesa.pedidos, pagos: ventaMesa.pagos }));
  sincro = iniciarSincronizador({
    cfg: { supabaseUrl: URL, anonKey: ANON, correo: EMAIL, clave: CLAVE, impresoras: [] },
    base,
    salon,
    espejoSet: (e) => {
      espejo = e;
    },
    log: (m) => console.log('   [sincro] ' + m),
  });
  const subio = await hasta(() => salon.resumen().ventasPorSubir === 0, 40_000);
  check(subio, 'FASE 3: el buzon completo subio a la nube al volver el internet');

  const { data: cuentaNube } = await svc.from('cuentas_mesa').select('estado,mesa,mesero_nombre').eq('id', ventaMesa.cuenta.id).maybeSingle();
  check(cuentaNube?.estado === 'cerrada' && cuentaNube?.mesa === '7', 'la cuenta de mesa quedo en la nube, cerrada');
  const { data: pedsNube } = await svc.from('pedidos').select('id,codigo,estado,tipo_entrega,pedido_items(id)').in('id', idsLimpieza.pedidos);
  check(pedsNube?.length === 2 && pedsNube.every((p) => p.estado === 'entregado'), 'los 2 pedidos locales quedaron en la nube, entregados');
  check(pedsNube.some((p) => /-L\d+$/.test(p.codigo)), 'con su serie local -L en el codigo');
  const { data: pagosMesaNube } = await svc.from('pagos_mesa').select('metodo,verificado,propina_usd').eq('cuenta_id', ventaMesa.cuenta.id);
  check(pagosMesaNube?.length === 1 && pagosMesaNube[0].metodo === 'pago_movil' && pagosMesaNube[0].verificado === false && Number(pagosMesaNube[0].propina_usd) === 1, 'el pago de mesa subio con propina y queda por verificar (pago movil)');
  const { data: pagoDirecto } = await svc.from('pagos').select('estado,metodo').eq('pedido_id', pd.id);
  check(pagoDirecto?.length === 1 && pagoDirecto[0].estado === 'confirmado' && pagoDirecto[0].metodo === 'efectivo', 'el pago de la venta directa subio confirmado (efectivo)');
  const { data: latido } = await svc.from('cerebritos').select('visto_en,version').eq('restaurante_id', REST).maybeSingle();
  check(!!latido && Date.now() - new Date(latido.visto_en).getTime() < 120_000, 'el LATIDO del cerebrito quedo registrado (tablero central)');

  // Idempotencia: resubir la MISMA venta no duplica nada.
  const cli2 = createClient(URL, ANON, { auth: { persistSession: false } });
  await cli2.auth.signInWithPassword({ email: EMAIL, password: CLAVE });
  const { data: re } = await cli2.rpc('sincronizar_venta_local', { p_venta: cargaIdem });
  const { data: pedsDespues } = await svc.from('pedidos').select('id').in('id', idsLimpieza.pedidos);
  check((re?.pedidos_nuevos ?? -1) === 0 && pedsDespues?.length === 2, 'resubir la misma venta NO duplica (idempotencia real)');
  await cli2.auth.signOut();

  // Un anonimo no puede usar la RPC de sincronizacion.
  const anon = createClient(URL, ANON, { auth: { persistSession: false } });
  const { error: eAnon } = await anon.rpc('sincronizar_venta_local', { p_venta: {} });
  check(!!eAnon, 'un anonimo NO puede sincronizar ventas (candado)');

  // ===== 2.B: semaforo publico del local (aviso suave del QR) =====
  const { data: lineaViva } = await anon.rpc('fn_local_en_linea', { p_restaurante: REST });
  check(lineaViva === 'en_linea', 'con latido fresco, la tienda ve el local EN LINEA');
  await svc.from('cerebritos').update({ visto_en: new Date(Date.now() - 10 * 60_000).toISOString() }).eq('restaurante_id', REST);
  const { data: lineaCaida } = await anon.rpc('fn_local_en_linea', { p_restaurante: REST });
  check(lineaCaida === 'sin_conexion', 'con latido viejo, la tienda ve SIN CONEXION (dispara el aviso suave)');
  const { data: lineaNada } = await anon.rpc('fn_local_en_linea', { p_restaurante: '00000000-0000-0000-0000-000000000000' });
  check(lineaNada === 'sin_cerebrito', 'un negocio sin cerebrito ni se entera del aviso');

  // ===== 2.B: tablero central (solo fundador/admin) =====
  const EMAIL_F = 'fundador.tablero.test@gustitoxpress.app';
  const { data: listaF } = await svc.auth.admin.listUsers();
  for (const x of listaF?.users ?? []) if (x.email === EMAIL_F) await svc.auth.admin.deleteUser(x.id);
  const { data: uf } = await svc.auth.admin.createUser({ email: EMAIL_F, password: CLAVE, email_confirm: true });
  await svc.from('perfiles').upsert({ id: uf.user.id, rol: 'comensal', es_fundador: true, nombre: 'Fundador Test' });
  const fundador = createClient(URL, ANON, { auth: { persistSession: false } });
  await fundador.auth.signInWithPassword({ email: EMAIL_F, password: CLAVE });
  const { data: tablero } = await fundador.rpc('fn_cerebritos_tablero');
  check(
    Array.isArray(tablero) && tablero.some((f) => /Do.a Carmen/.test(f.restaurante) && f.detalle?.ip),
    'el tablero del fundador ve el cerebrito del demo CON su IP (para el boton del panel)',
  );
  const encTab = createClient(URL, ANON, { auth: { persistSession: false } });
  await encTab.auth.signInWithPassword({ email: EMAIL, password: CLAVE });
  const { data: tableroEnc } = await encTab.rpc('fn_cerebritos_tablero');
  check(Array.isArray(tableroEnc) && tableroEnc.length === 0, 'un encargado comun NO ve el tablero (vacio, sin fuga)');
  await svc.auth.admin.deleteUser(uf.user.id).catch(() => {});
} catch (e) {
  console.log('  XX  Excepcion: ' + e.message);
  fallos++;
} finally {
  await limpiar();
  console.log('\nLimpieza hecha (nube, usuario, impresoras y carpeta fuera)');
}
console.log(fallos === 0 ? '\nETAPA 2 LOCAL-FIRST OK' : `\nETAPA 2: ${fallos} FALLO(S)`);
process.exit(fallos === 0 ? 0 : 1);
