import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { dirname, join } from 'path';
import { construirIndice } from './construir';
import { ManualService, nombreArchivo } from './manual.service';
import { tokens } from './normalizar';

/** Un panel mínimo con la misma forma que el real: menú, rutas, Configuración, componentes, backend. */
function panelDeEjemplo(): string {
  const raiz = mkdtempSync(join(tmpdir(), 'manual-'));
  const archivos: Record<string, string> = {
    'frontend/src/hooks/useMenuAcceso.js': `export const MODULO_RAIZ_META = { VENTAS: { label: "Ventas" } };`,
    'frontend/src/utils/dataEstatica.jsx': `export const LinksArray = [
      { label: "Facturación", to: "/ventas", modulo: "VENTAS" },
      { label: "POS", to: "/pos", modulo: "VENTAS" },
    ];
    export const SecondarylinksArray = [{ label: "Configuración", to: "/configuracion" }];`,
    'frontend/src/routers/routes.jsx': `
      const Ventas = lazyNamed(() => import("../pages/Ventas"), "Ventas");
      const POS = lazyNamed(() => import("../pages/POS"), "POS");
      const ConfiguracionNew = lazyNamed(() => import("../pages/ConfiguracionNew"), "ConfiguracionNew");
      export const R = () => (<Routes>
        <Route path="/ventas" element={<Layout><ProtectedRoute><Ventas /></ProtectedRoute></Layout>} />
        <Route path="/pos" element={<Layout><POS /></Layout>} />
        <Route path="/configuracion" element={<Layout><ConfiguracionNew /></Layout>} />
        <Route path="/login" element={<Login />} />
      </Routes>);`,
    'frontend/src/pages/Ventas.jsx': `import { useNavigate } from "react-router-dom";
      export function Ventas() { const n = useNavigate();
        return <div><h1>Facturas</h1><Button onClick={() => n("/pos")} title="Ir al POS para crear una nueva factura">Nueva Factura</Button></div>; }`,
    'frontend/src/pages/POS.jsx': `import { Cobro } from "../components/pos/Cobro";
      export function POS() { return <Cobro />; }`,
    'frontend/src/components/pos/Cobro.jsx': `export function Cobro() {
      const avisar = () => toast.error("Este cliente no tiene habilitada la venta a crédito");
      return <Dialog title="Cobrar Venta"><span>Condición de pago</span><Button>Contado</Button><Button>Crédito</Button>
        <TextField label="Condición" helperText="Elegí A plazo o En cuotas" /><p>Venta a crédito — no se cobra en caja</p></Dialog>; }`,
    'frontend/src/pages/ConfiguracionNew.jsx': `import { PagosConfigTab } from "../components/configuracion/PagosConfigTab";
      const menuCategories = [{ id: "facturacion", label: "Facturación", items: [{ id: "metodos-pago", label: "Métodos de Pago" }] }];
      export function ConfiguracionNew({ s }) { switch (s) { case "metodos-pago": return <PagosConfigTab />; default: return null; } }`,
    'frontend/src/components/configuracion/PagosConfigTab.jsx': `export function PagosConfigTab() {
      return <Card title="Condiciones de Pago"><Button>{isMobile ? "Nueva" : "Nueva Condición"}</Button>
        <TextField label="Tipo de Crédito" /><TextField label="Días de Plazo" helperText={error?.message || "Entre 1 y 365"} /></Card>; }`,
    'backend/src/clientes/clientes.service.ts': `export class ClientesService {
      verificar() { throw new BadRequestException('El cliente no tiene habilitada la venta a crédito'); } }`,
  };
  for (const [ruta, contenido] of Object.entries(archivos)) {
    mkdirSync(dirname(join(raiz, 'codigo', ruta)), { recursive: true });
    writeFileSync(join(raiz, 'codigo', ruta), contenido);
  }
  mkdirSync(join(raiz, 'guias'));
  const paso = (n: number, texto: string) => ({
    n,
    texto,
    imagen: `condicion-pago-plazo-0${n}.png`,
    ancho: 100,
    alto: 100,
    marcas: [{ x: 10, y: 10, w: 20, h: 5 }],
  });
  const guia = {
    id: 'condicion-pago-plazo',
    titulo: 'Configurar una condición de pago a plazo',
    descripcion: 'Crear una condición de crédito con días de plazo.',
    palabras: ['condición de pago', 'crédito', 'plazo'],
    generada: '2026-10-26T00:00:00Z',
    pasos: [paso(1, 'Tocá **Nueva Condición**.'), paso(2, 'Elegí **Plazo**.')],
  };
  writeFileSync(join(raiz, 'guias', 'condicion-pago-plazo.json'), JSON.stringify(guia));
  for (const p of guia.pasos) writeFileSync(join(raiz, 'guias', p.imagen), 'png');
  // Una guía a la que le falta una imagen no se carga: el visor quedaría roto.
  writeFileSync(join(raiz, 'guias', 'rota.json'), JSON.stringify({ ...guia, id: 'rota', pasos: [paso(9, 'x')] }));
  return raiz;
}

describe('tokens', () => {
  it('factura, facturas, facturación y facturar tienen la misma raíz', () => {
    expect(new Set(tokens('factura facturas Facturación facturar'))).toEqual(new Set(['factur']));
    expect(tokens('¿Puedo emitir una factura a crédito con plazo?')).toEqual(['emit', 'factur', 'credit', 'plaz']);
  });
});

describe('manual del sistema', () => {
  let raiz: string;
  let manual: ManualService;

  beforeAll(() => {
    raiz = panelDeEjemplo();
    const indice = construirIndice(join(raiz, 'codigo'), { frontend: 'x', backend: 'y' });
    writeFileSync(join(raiz, 'indice.json'), JSON.stringify(indice));
    manual = new ManualService();
    manual.cargar(raiz);
  });

  afterAll(() => rmSync(raiz, { recursive: true, force: true }));

  it('arma las fichas: menú, pantallas fuera del menú, Configuración y reglas del backend', () => {
    const indice = manual.indiceDePantallas();
    expect(indice).toContain('ruta:/ventas | Ventas › Facturación');
    expect(indice).toContain('config:metodos-pago | Configuración › Facturación › Métodos de Pago');
    expect(indice).toContain('reglas:clientes');
    // La página contenedora de Configuración y el login no son pantallas del manual.
    expect(indice).not.toMatch(/ruta:\/configuracion \|/);
    expect(indice).not.toContain('ruta:/login');
  });

  it('sigue los imports: el POS trae los textos del diálogo de cobro, y sabe desde dónde se abre', () => {
    const [pos] = manual.detalle(['/pos'], 'condición de pago crédito plazo');
    expect(pos.pantalla).toBe('Ventas › POS');
    expect(pos.textos).toEqual(expect.arrayContaining(['Cobrar Venta', 'Condición de pago', 'Crédito']));
    expect(pos.textos).toContain('Este cliente no tiene habilitada la venta a crédito');
    expect(pos.se_abre_desde).toContain('Ventas › Facturación');
  });

  it('toma los textos de las dos ramas de una condición y la alternativa de un ||', () => {
    const [pagos] = manual.detalle(['config:metodos-pago'], 'nueva condición plazo');
    expect(pagos.textos).toEqual(expect.arrayContaining(['Nueva Condición', 'Entre 1 y 365']));
  });

  it('carga las guías paso a paso completas y las ofrece al enrutador', () => {
    expect(manual.guia('condicion-pago-plazo')?.pasos).toHaveLength(2);
    expect(manual.guia('rota')).toBeNull();
    expect(manual.indiceDePantallas()).toContain('guia:condicion-pago-plazo | Guía paso a paso: Configurar');
    expect(manual.resolverId('guia:condicion-pago-plazo')).toBe('guia:condicion-pago-plazo');
    expect(manual.resolverId('guia:rota')).toBeNull();
  });

  it('da la guía que eligió el enrutador, o la que coincide por palabras', () => {
    const esperada = [
      {
        marca: '[[guia:condicion-pago-plazo]]',
        titulo: 'Configurar una condición de pago a plazo',
        pasos: ['Tocá **Nueva Condición**.', 'Elegí **Plazo**.'],
      },
    ];
    expect(manual.guiasPara(['guia:condicion-pago-plazo', 'ruta:/pos'], 'x')).toEqual(esperada);
    expect(manual.guiasPara([], '¿cómo creo una condición de crédito a plazo?')).toEqual(esperada);
    // Una sola palabra en común no alcanza.
    expect(manual.guiasPara([], '¿cómo emito una factura a crédito?')).toEqual([]);
  });

  it('sirve las imágenes de las guías con el mismo control de nombres', () => {
    expect(manual.rutaCaptura('condicion-pago-plazo-01.png')).toMatch(/guias\/condicion-pago-plazo-01\.png$/);
    expect(manual.rutaCaptura('../guias/condicion-pago-plazo-01.png')).toBeNull();
  });

  it('acepta los ids como los devuelve un modelo', () => {
    expect(manual.resolverId('ruta:/pos')).toBe('ruta:/pos');
    expect(manual.resolverId('/pos')).toBe('ruta:/pos');
    expect(manual.resolverId('metodos-pago')).toBe('config:metodos-pago');
    expect(manual.resolverId('Ventas › Facturación')).toBe('ruta:/ventas');
    expect(manual.resolverId('/no-existe')).toBeNull();
  });

  it('la búsqueda por palabras encuentra dónde se configura el plazo', () => {
    const r = manual.buscar('donde configuro los dias de plazo del credito');
    expect(r.map((x) => x.pantalla)).toContain('Configuración › Facturación › Métodos de Pago');
  });

  it('busca un texto en toda la foto del código, como un grep', () => {
    const r = manual.buscarCodigo('venta a credito');
    expect(r.coincidencias.map((c) => c.archivo)).toEqual(
      expect.arrayContaining(['frontend/src/components/pos/Cobro.jsx', 'backend/src/clientes/clientes.service.ts']),
    );
    expect(r.coincidencias[0].linea).toBeGreaterThan(0);
    expect(manual.buscarCodigo('venta a credito', 'backend').coincidencias.every((c) => c.archivo.startsWith('backend/'))).toBe(true);
    expect(manual.buscarCodigo('no-aparece-en-ningun-lado').coincidencias).toEqual([]);
    expect(manual.buscarCodigo('ab').coincidencias).toEqual([]); // muy corto
  });

  it('lee un archivo de la foto del código, solo las zonas pedidas', () => {
    const r = manual.leerCodigo('frontend/src/components/pos/Cobro.jsx', 'Crédito');
    expect('lineas' in r && r.lineas).toMatch(/Crédito/);
  });

  it.each(['../indice.json', '/etc/passwd', 'frontend/../../indice.json', '', 'frontend/src/no-existe.jsx'])(
    'no sale de la foto del código: %s',
    (archivo) => {
      expect(manual.leerCodigo(archivo, 'x')).toHaveProperty('error');
    },
  );

  it('las capturas solo con nombres válidos', () => {
    expect(nombreArchivo('ruta:/ventas')).toBe('ruta-ventas');
    expect(nombreArchivo('config:metodos-pago')).toBe('config-metodos-pago');
    expect(manual.rutaCaptura('../indice.json')).toBeNull();
    expect(manual.rutaCaptura('ruta-ventas.png')).toBeNull(); // no existe todavía
  });
});
