<?php
/**
 * NIS2 Agile — Test Runner v2
 * Accesso: https://nis2.agile.software/test-runner.php?t=Nis2Test2026
 *
 * Livelli di test:
 *   L1  Auth & JWT
 *   L2  Multi-Tenant Isolation
 *   L3  Compliance Core
 *   L4  B2B & Services API
 *   L5  Export & Reports
 *   L6  AI Features (Cross-Analysis, Workflow, Normative)
 *   SIM Simulazioni scenari reali
 */

define('ACCESS_TOKEN',  'Nis2Test2026');
define('PROJECT_ROOT',  '/var/www/nis2-agile');
define('API_BASE',      'https://nis2.agile.software/api');
define('SIMULATE_PHP',  PROJECT_ROOT . '/simulate-nis2.php');
define('RESET_SQL',     PROJECT_ROOT . '/docs/sql/reset-demo.sql');

// ── Auth ──────────────────────────────────────────────────────────────────────

session_start();

$token = $_GET['t'] ?? $_POST['t'] ?? '';
if ($token === ACCESS_TOKEN) {
    $_SESSION['auth'] = true;
}

if (empty($_SESSION['auth'])) {
    http_response_code(401);
    echo '<!doctype html><html><body style="font-family:monospace;background:#0f172a;color:#f87171;padding:2rem">'
       . '<h2>401 — Token richiesto</h2><p>Accedi con <code>?t=Nis2Test2026</code></p></body></html>';
    exit;
}

// ── Router ────────────────────────────────────────────────────────────────────

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$base = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/');
$slug = ltrim(substr($path, strlen($base)), '/');

if ($slug === 'events') {
    serveEvents();
    exit;
}
if ($slug === 'status') {
    serveStatus();
    exit;
}
if ($slug === 'db-stats') {
    serveDbStats();
    exit;
}

serveUI();
exit;

// ── Commands ──────────────────────────────────────────────────────────────────

function getCommands(): array
{
    $php  = PHP_BINARY ?: 'php';
    $root = PROJECT_ROOT;
    $api  = API_BASE;

    $loginAdmin   = "curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore-srl.demo\",\"password\":\"NIS2Demo2026!\"}' 2>/dev/null";
    $getToken     = "\$({$loginAdmin} | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null)";

    return [
        // ── INFRASTRUCTURE ──────────────────────────────────────────────────
        'health' => [
            'label'   => 'Health Check — API Status',
            'level'   => 'infra',
            'bash'    => "curl -sf {$api}/../api-status.php | python3 -m json.tool",
            'cwd'     => $root, 'timeout' => 15, 'continue_on_fail' => false,
        ],

        // ── L1: AUTH & JWT ──────────────────────────────────────────────────
        'l1-auth' => [
            'label'   => 'L1 — Auth & JWT',
            'level'   => 'l1',
            'bash'    => implode(' && ', [
                "echo '━━━ L1.1 Login valido ━━━'",
                "curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore-srl.demo\",\"password\":\"NIS2Demo2026!\"}' | python3 -m json.tool || echo '[SKIP] utente non trovato'",
                "echo ''",
                "echo '━━━ L1.2 Login password errata (401) ━━━'",
                "curl -sf -o /dev/null -w 'HTTP %{http_code}' -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore-srl.demo\",\"password\":\"WRONG\"}'",
                "echo ''",
                "echo '━━━ L1.3 /auth/me senza token (401) ━━━'",
                "curl -sf -o /dev/null -w 'HTTP %{http_code}' {$api}/auth/me",
                "echo ''",
                "echo '━━━ L1.4 /auth/me con token valido ━━━'",
                "TOKEN={$getToken} && [ -n \"\$TOKEN\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/auth/me | python3 -m json.tool || echo '[SKIP] token non disponibile'",
                "echo ''",
                "echo '━━━ L1.5 Rate limiting (5 login rapidi) ━━━'",
                "for i in 1 2 3 4 5; do curl -sf -o /dev/null -w \"Tentativo \$i → HTTP %{http_code}\\n\" -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"ratelimit-test@test.com\",\"password\":\"wrong\"}'; done",
                "echo '[OK] L1 Auth & JWT completato'",
            ]),
            'cwd' => $root, 'timeout' => 45, 'continue_on_fail' => true,
        ],

        // ── L2: MULTI-TENANT ────────────────────────────────────────────────
        'l2-tenant' => [
            'label'   => 'L2 — Multi-Tenant Isolation',
            'level'   => 'l2',
            'bash'    => implode(' && ', [
                "echo '━━━ L2.1 Login utente DataCore ━━━'",
                "TOKEN1={$getToken} && echo \"Token ottenuto: \${TOKEN1:0:20}...\"",
                "echo ''",
                "echo '━━━ L2.2 Organizations dell\'utente ━━━'",
                "TOKEN={$getToken} && curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list | python3 -m json.tool",
                "echo ''",
                "echo '━━━ L2.3 Accesso org corrente ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/organizations/current | python3 -m json.tool || echo '[SKIP] nessuna org trovata'",
                "echo ''",
                "echo '━━━ L2.4 Dashboard overview con org ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/dashboard/overview | python3 -m json.tool || echo '[SKIP]'",
                "echo '[OK] L2 Multi-Tenant completato'",
            ]),
            'cwd' => $root, 'timeout' => 60, 'continue_on_fail' => true,
        ],

        // ── L3: COMPLIANCE CORE ─────────────────────────────────────────────
        'l3-compliance' => [
            'label'   => 'L3 — Compliance Core',
            'level'   => 'l3',
            'bash'    => implode(' && ', [
                "echo '━━━ L3.1 Compliance score ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/dashboard/compliance-score | python3 -m json.tool || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L3.2 Risk matrix ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/risks/matrix | python3 -m json.tool || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L3.3 Audit controls (Art.21) ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/audit/controls | python3 -m json.tool || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L3.4 Incidents list ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/incidents/list | python3 -c \"import sys,json; d=json.load(sys.stdin); items=d.get('data',[]); print(f'Incidenti trovati: {len(items)}')\" || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L3.5 Upcoming deadlines ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/dashboard/upcoming-deadlines | python3 -m json.tool || echo '[SKIP]'",
                "echo '[OK] L3 Compliance Core completato'",
            ]),
            'cwd' => $root, 'timeout' => 90, 'continue_on_fail' => true,
        ],

        // ── L4: B2B & SERVICES ──────────────────────────────────────────────
        'l4-b2b' => [
            'label'   => 'L4 — B2B & Services API',
            'level'   => 'l4',
            'bash'    => implode(' && ', [
                "echo '━━━ L4.1 Validate invite (endpoint esistenza) ━━━'",
                "curl -sf -o /dev/null -w 'POST /auth/validate-invite → HTTP %{http_code}\\n' -X POST {$api}/auth/validate-invite -H 'Content-Type: application/json' -d '{\"invite_token\":\"invalid-test-token\"}'",
                "echo ''",
                "echo '━━━ L4.2 Supply chain risk overview ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/supply-chain/risk-overview | python3 -m json.tool || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L4.3 NCR stats ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/ncr/stats | python3 -m json.tool || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L4.4 Training compliance status ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/training/compliance-status | python3 -m json.tool || echo '[SKIP]'",
                "echo '[OK] L4 B2B & Services completato'",
            ]),
            'cwd' => $root, 'timeout' => 60, 'continue_on_fail' => true,
        ],

        // ── L5: EXPORT & REPORTS ────────────────────────────────────────────
        'l5-export' => [
            'label'   => 'L5 — Export & Reports',
            'level'   => 'l5',
            'bash'    => implode(' && ', [
                "echo '━━━ L5.1 Export CSV rischi ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -o /tmp/risks_export.csv -w 'CSV rischi: %{size_download} bytes (HTTP %{http_code})' -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" \"{$api}/audit/export?type=risks\" && head -3 /tmp/risks_export.csv || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L5.2 Export CSV incidenti ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -o /tmp/incidents_export.csv -w 'CSV incidenti: %{size_download} bytes (HTTP %{http_code})' -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" \"{$api}/audit/export?type=incidents\" || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L5.3 ISO 27001 mapping ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/audit/iso27001-mapping | python3 -c \"import sys,json; d=json.load(sys.stdin); items=d.get('data',[]); print(f'Controlli ISO 27001: {len(items)}')\" || echo '[SKIP]'",
                "echo ''",
                "echo '━━━ L5.4 Report audit log (ultimi 10) ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/audit/logs | python3 -c \"import sys,json; d=json.load(sys.stdin); logs=d.get('data',[]); print(f'Log audit: {len(logs)} entries')\" || echo '[SKIP]'",
                "echo '[OK] L5 Export & Reports completato'",
            ]),
            'cwd' => $root, 'timeout' => 60, 'continue_on_fail' => true,
        ],

        // ── L6: AI FEATURES ─────────────────────────────────────────────────
        'l6-ai' => [
            'label'   => 'L6 — AI Features (Cross-Analysis)',
            'level'   => 'l6',
            'bash'    => implode(' && ', [
                // Login come consultant (ha accesso a cross-analysis)
                "echo '━━━ L6.0 Setup: login consultant ━━━'",
                "TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"NIS2Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && echo \"Token consultant: \${TOKEN_CONS:0:25}...\" || echo '[SKIP] consultant non trovato — eseguire SIM-01 prima'",
                "echo ''",

                // L6.1: portfolio (senza AI, solo dati aggregati)
                "echo '━━━ L6.1 Cross-Analysis Portfolio (no AI) ━━━'",
                "TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"NIS2Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN_CONS\" {$api}/cross-analysis/portfolio | python3 -m json.tool || echo '[SKIP] token non disponibile'",
                "echo ''",

                // L6.2: history (vuota, ma endpoint deve rispondere 200)
                "echo '━━━ L6.2 Cross-Analysis History ━━━'",
                "TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"NIS2Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN_CONS\" {$api}/cross-analysis/history | python3 -c \"import sys,json; d=json.load(sys.stdin); h=d.get('data',{}).get('history',[]); print(f'History entries: {len(h)} — HTTP OK')\" || echo '[SKIP]'",
                "echo ''",

                // L6.3: analyze con domanda breve (chiama AI Anthropic)
                "echo '━━━ L6.3 Cross-Analysis AI Analyze ━━━'",
                "TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"NIS2Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -X POST -H \"Authorization: Bearer \$TOKEN_CONS\" -H 'Content-Type: application/json' -d '{\"question\":\"Qual e il livello medio di compliance e quali sono le categorie NIS2 piu deboli nel portfolio?\"}' {$api}/cross-analysis/analyze | python3 -c \"import sys,json; d=json.load(sys.stdin); r=d.get('data',{}); ans=r.get('result',{}).get('answer',''); orgs=r.get('org_count',0); print(f'Org analizzate: {orgs}'); print(f'Risposta AI ({len(ans)} chars): {ans[:300]}...' if len(ans)>300 else f'Risposta: {ans}')\" || echo '[SKIP/ERRORE]'",
                "echo ''",

                // L6.4: accesso negato a utente normale (403)
                "echo '━━━ L6.4 Cross-Analysis 403 per utente non-consultant ━━━'",
                "TOKEN_EMP=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore-srl.demo\",\"password\":\"NIS2Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_EMP\" ] && curl -sf -o /dev/null -w 'GET /cross-analysis/portfolio (org_admin) → HTTP %{http_code}\\n' -H \"Authorization: Bearer \$TOKEN_EMP\" {$api}/cross-analysis/portfolio || echo '[SKIP]'",
                "echo ''",

                // L6.5: normative feed
                "echo '━━━ L6.5 Normative Feed ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/normative/list | python3 -c \"import sys,json; d=json.load(sys.stdin); items=d.get('data',[]); print(f'Aggiornamenti normativi: {len(items)}')\" || echo '[SKIP]'",
                "echo ''",

                // L6.6: whistleblowing stats
                "echo '━━━ L6.6 Whistleblowing Stats ━━━'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/whistleblowing/stats | python3 -m json.tool || echo '[SKIP]'",

                "echo '[OK] L6 AI Features completato'",
            ]),
            'cwd' => $root, 'timeout' => 120, 'continue_on_fail' => true,
        ],

        // ── SMOKE ───────────────────────────────────────────────────────────
        'smoke' => [
            'label'   => 'Smoke Tests (curl rapido)',
            'level'   => 'infra',
            'bash'    => implode(' && ', [
                "echo '=== Login demo ===' && curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore-srl.demo\",\"password\":\"NIS2Demo2026!\"}' | python3 -m json.tool || echo '[SKIP]'",
                "echo '=== /auth/me senza token (401) ===' && curl -sf -o /dev/null -w 'HTTP %{http_code}' {$api}/auth/me",
                "echo '=== /dashboard/overview (no token → 401) ===' && curl -sf -o /dev/null -w 'HTTP %{http_code}' {$api}/dashboard/overview",
                "echo '=== API status ===' && curl -sf {$api}/../api-status.php | python3 -m json.tool",
            ]),
            'cwd' => $root, 'timeout' => 30, 'continue_on_fail' => true,
        ],

        // ── SIMULAZIONI ─────────────────────────────────────────────────────
        'sim01' => [
            'label'   => 'SIM-01 Onboarding + Assessment',
            'level'   => 'sim',
            'bash'    => "NIS2_SIM=SIM01 {$php} {$root}/simulate-nis2.php",
            'cwd' => $root, 'timeout' => 180, 'continue_on_fail' => false,
        ],
        'sim02' => [
            'label'   => 'SIM-02 Ransomware Art.23',
            'level'   => 'sim',
            'bash'    => "NIS2_SIM=SIM02 {$php} {$root}/simulate-nis2.php",
            'cwd' => $root, 'timeout' => 120, 'continue_on_fail' => false,
        ],
        'sim03' => [
            'label'   => 'SIM-03 Data Breach Supply Chain',
            'level'   => 'sim',
            'bash'    => "NIS2_SIM=SIM03 {$php} {$root}/simulate-nis2.php",
            'cwd' => $root, 'timeout' => 120, 'continue_on_fail' => false,
        ],
        'sim04' => [
            'label'   => 'SIM-04 Whistleblowing SCADA',
            'level'   => 'sim',
            'bash'    => "NIS2_SIM=SIM04 {$php} {$root}/simulate-nis2.php",
            'cwd' => $root, 'timeout' => 90, 'continue_on_fail' => false,
        ],
        'sim05' => [
            'label'   => 'SIM-05 Audit Chain Verify',
            'level'   => 'sim',
            'bash'    => "NIS2_SIM=SIM05 {$php} {$root}/simulate-nis2.php",
            'cwd' => $root, 'timeout' => 60, 'continue_on_fail' => false,
        ],
        'sim06' => [
            'label'   => 'SIM-06 B2B License Provisioning',
            'level'   => 'sim',
            'bash'    => "NIS2_SIM=SIM06 {$php} {$root}/simulate-nis2.php",
            'cwd' => $root, 'timeout' => 90, 'continue_on_fail' => false,
        ],
        'simulate' => [
            'label'   => 'Tutte le Simulazioni (SIM-01→06)',
            'level'   => 'sim',
            'bash'    => "{$php} {$root}/simulate-nis2.php",
            'cwd' => $root, 'timeout' => 720, 'continue_on_fail' => false,
        ],
        'chain-verify' => [
            'label'   => 'Verifica Hash Chain Audit',
            'level'   => 'infra',
            'bash'    => implode(' && ', [
                "echo '=== Verifica chain org demo ==='",
                "curl -sf -H 'Authorization: Bearer \$(cat /tmp/nis2_chain_token 2>/dev/null || echo \"\")' {$api}/audit/chain-verify | python3 -m json.tool || echo '[INFO] Autenticarsi prima con simulate'",
            ]),
            'cwd' => $root, 'timeout' => 30, 'continue_on_fail' => true,
        ],
        'reset' => [
            'label'   => 'Reset Dati Demo',
            'level'   => 'infra',
            'bash'    => "mysql -u nis2_agile_user -p\$(grep DB_PASSWORD {$root}/.env | cut -d= -f2) nis2_agile_db < {$root}/docs/sql/reset-demo.sql",
            'cwd' => $root, 'timeout' => 30, 'continue_on_fail' => false,
        ],
        'full-suite' => [
            'label'   => 'Full Suite L1+L2+L3+L4+L5+L6',
            'level'   => 'infra',
            'bash'    => implode(' && ', [
                "echo '════════════ L1 AUTH ════════════'",
                "curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore-srl.demo\",\"password\":\"NIS2Demo2026!\"}' | python3 -c \"import sys,json; d=json.load(sys.stdin); print('Login:', 'OK' if d.get('success') else 'FAIL')\" || echo 'L1 SKIP'",
                "echo '════════════ L2 TENANT ════════════'",
                "TOKEN={$getToken} && curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(f'Orgs: {len(orgs)}')\" || echo 'L2 SKIP'",
                "echo '════════════ L3 COMPLIANCE ════════════'",
                "TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/dashboard/compliance-score | python3 -c \"import sys,json; d=json.load(sys.stdin); print('Score:', d.get('data',{}).get('score','?'))\" || echo 'L3 SKIP'",
                "echo '════════════ L5 EXPORT ════════════'",
                "curl -sf {$api}/../api-status.php | python3 -c \"import sys,json; d=json.load(sys.stdin); print('API:', d.get('status','?'))\"",
                "echo '════════════ L6 AI CROSS ════════════'",
                "TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"NIS2Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN_CONS\" {$api}/cross-analysis/portfolio | python3 -c \"import sys,json; d=json.load(sys.stdin); print('Portfolio orgs:', d.get('data',{}).get('org_count','?'))\" || echo 'L6 SKIP (eseguire SIM-01 prima)'",
                "echo '[OK] Full Suite L1→L6 completata'",
            ]),
            'cwd' => $root, 'timeout' => 300, 'continue_on_fail' => true,
        ],
        'full-reset-sim' => [
            'label'   => 'Reset + Simula + Testa Tutto',
            'level'   => 'infra',
            'bash'    => implode(' && ', [
                "echo '════════════════════════════════════════'",
                "echo '  FASE 1 — Reset database demo'",
                "echo '════════════════════════════════════════'",
                "mysql -u nis2_agile_user -p\$(grep DB_PASSWORD {$root}/.env | cut -d= -f2) nis2_agile_db < {$root}/docs/sql/reset-demo.sql",
                "echo '[OK] Reset completato.'",
                "echo ''",
                "echo '════════════════════════════════════════'",
                "echo '  FASE 2 — Simulazioni demo (SIM-01→06)'",
                "echo '════════════════════════════════════════'",
                PHP_BINARY . " {$root}/simulate-nis2.php",
                "echo ''",
                "echo '════════════════════════════════════════'",
                "echo '  FASE 3 — Smoke tests API'",
                "echo '════════════════════════════════════════'",
                "curl -sf " . API_BASE . "/../api-status.php | python3 -m json.tool",
                "echo '[OK] Suite completa terminata.'",
            ]),
            'cwd' => $root, 'timeout' => 900, 'continue_on_fail' => false,
        ],
    ];
}

// ── SSE Events handler ────────────────────────────────────────────────────────

function serveEvents(): void
{
    $cmd = $_GET['cmd'] ?? '';
    $commands = getCommands();

    if (!isset($commands[$cmd])) {
        http_response_code(400);
        echo "Comando sconosciuto: {$cmd}";
        return;
    }

    $def = $commands[$cmd];

    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    header('X-Accel-Buffering: no');

    if (ob_get_level()) ob_end_clean();

    sseWrite('start', json_encode(['cmd' => $cmd, 'label' => $def['label'], 'ts' => date('c')]));

    $t0 = microtime(true);
    runCommand($def, function (string $type, string $line) {
        sseWrite($type, $line);
    });

    $elapsed = round(microtime(true) - $t0, 2);
    sseWrite('done', json_encode(['cmd' => $cmd, 'elapsed' => $elapsed]));
}

function runCommand(array $def, callable $emit): void
{
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ];

    $env = array_merge($_ENV, getenv() ?: [], ['NIS2_API_BASE' => API_BASE]);

    $proc = proc_open(
        $def['bash'],
        $descriptors,
        $pipes,
        $def['cwd'] ?? PROJECT_ROOT,
        $env
    );

    if (!is_resource($proc)) {
        $emit('stderr', '[ERRORE] Impossibile avviare il processo');
        return;
    }

    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    $timeout  = $def['timeout'] ?? 120;
    $deadline = microtime(true) + $timeout;
    $buf1 = '';
    $buf2 = '';

    while (true) {
        if (microtime(true) > $deadline) {
            $emit('stderr', "[TIMEOUT] Processo terminato dopo {$timeout}s");
            proc_terminate($proc, 9);
            break;
        }

        $r = [$pipes[1], $pipes[2]];
        $w = null; $e = null;

        $changed = @stream_select($r, $w, $e, 0, 50000);
        if ($changed === false) break;

        foreach ($r as $s) {
            $chunk = fread($s, 4096);
            if ($chunk === false || $chunk === '') continue;

            if ($s === $pipes[1]) {
                $buf1 .= $chunk;
                while (($nl = strpos($buf1, "\n")) !== false) {
                    $emit('stdout', rtrim(substr($buf1, 0, $nl)));
                    $buf1 = substr($buf1, $nl + 1);
                }
            } else {
                $buf2 .= $chunk;
                while (($nl = strpos($buf2, "\n")) !== false) {
                    $emit('stderr', rtrim(substr($buf2, 0, $nl)));
                    $buf2 = substr($buf2, $nl + 1);
                }
            }
        }

        if (feof($pipes[1]) && feof($pipes[2])) break;
        if (connection_aborted()) { proc_terminate($proc, 9); break; }

        if (ob_get_level()) ob_flush();
        flush();
    }

    if ($buf1 !== '') $emit('stdout', rtrim($buf1));
    if ($buf2 !== '') $emit('stderr', rtrim($buf2));

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);

    if (ob_get_level()) ob_flush();
    flush();
}

function sseWrite(string $event, string $data): void
{
    echo "event: {$event}\ndata: " . $data . "\n\n";
    if (ob_get_level()) ob_flush();
    flush();
}

// ── Status JSON ───────────────────────────────────────────────────────────────

function serveStatus(): void
{
    header('Content-Type: application/json');

    $api = API_BASE;
    $up  = false;
    $info = [];

    $ch = curl_init("{$api}/../api-status.php");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_SSL_VERIFYPEER => false,
    ]);
    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code === 200 && $resp) {
        $up   = true;
        $info = json_decode($resp, true) ?: [];
    }

    echo json_encode([
        'api_up'      => $up,
        'api_url'     => $api,
        'api_info'    => $info,
        'php_version' => PHP_VERSION,
        'ts'          => date('c'),
    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}

// ── DB Stats JSON ─────────────────────────────────────────────────────────────

function serveDbStats(): void
{
    header('Content-Type: application/json');

    $envFile = PROJECT_ROOT . '/.env';
    if (!file_exists($envFile)) {
        echo json_encode(['error' => '.env non trovato'], JSON_PRETTY_PRINT);
        return;
    }

    $env = [];
    foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue;
        [$k, $v] = explode('=', $line, 2);
        $env[trim($k)] = trim($v);
    }

    try {
        $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4',
            $env['DB_HOST'] ?? '127.0.0.1',
            $env['DB_NAME'] ?? 'nis2_agile_db'
        );
        $pdo = new PDO($dsn, $env['DB_USER'] ?? '', $env['DB_PASSWORD'] ?? '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 3,
        ]);

        $tables = [
            'organizations', 'users', 'user_organizations',
            'assessments', 'assessment_responses',
            'risks', 'risk_treatments',
            'incidents', 'incident_timeline',
            'policies', 'suppliers',
            'training_courses', 'training_assignments',
            'assets', 'compliance_controls', 'evidence_files',
            'audit_logs', 'ai_interactions', 'email_log',
            'non_conformities', 'corrective_actions',
        ];

        $stats = [];
        foreach ($tables as $t) {
            try {
                $stmt = $pdo->query("SELECT COUNT(*) FROM `{$t}`");
                $stats[$t] = (int)$stmt->fetchColumn();
            } catch (\Throwable) {
                $stats[$t] = null; // tabella non esiste
            }
        }

        echo json_encode([
            'stats' => $stats,
            'total' => array_sum(array_filter($stats, fn($v) => $v !== null)),
            'ts'    => date('c'),
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

    } catch (\Throwable $e) {
        echo json_encode(['error' => $e->getMessage()], JSON_PRETTY_PRINT);
    }
}

// ── UI ────────────────────────────────────────────────────────────────────────

function serveUI(): void
{
    $commands = getCommands();
    $cmdsJson = json_encode(array_map(fn($k, $v) => [
        'id'    => $k,
        'label' => $v['label'],
        'level' => $v['level'] ?? 'infra',
    ], array_keys($commands), $commands), JSON_UNESCAPED_UNICODE);

    $token = ACCESS_TOKEN;
    $api   = API_BASE;
    $url   = "https://nis2.agile.software/test-runner.php?t={$token}";

    $demoCredentials = [
        ['role' => '★ Super Admin', 'email' => 'cristiano.benassati@gmail.com', 'password' => 'Silvia1978!@', 'org' => 'Tutte'],
        ['role' => 'Admin (DataCore)', 'email' => 'admin@datacore-srl.demo', 'password' => 'NIS2Demo2026!', 'org' => 'DataCore S.r.l.'],
        ['role' => 'Compliance (MedClinic)', 'email' => 'admin@medclinic-spa.demo', 'password' => 'NIS2Demo2026!', 'org' => 'MedClinic Italia'],
        ['role' => 'CISO (EnerNet)', 'email' => 'admin@enernet-srl.demo', 'password' => 'NIS2Demo2026!', 'org' => 'EnerNet S.r.l.'],
        ['role' => 'Consultant', 'email' => 'consultant@nis2agile.demo', 'password' => 'NIS2Demo2026!', 'org' => 'Multi-azienda'],
    ];
    $credsRows = '';
    foreach ($demoCredentials as $c) {
        $credsRows .= "<tr><td>{$c['role']}</td><td><code>{$c['email']}</code></td><td><code>{$c['password']}</code></td><td>{$c['org']}</td></tr>";
    }

    // SIM cards
    $simCards = '';
    $simDefs = [
        'sim01' => ['SIM-01', 'Onboarding + Assessment',   '3 aziende, gap analysis 80 domande',          'cyan'],
        'sim02' => ['SIM-02', 'Ransomware Art.23',          'Incidente critico, 24h/72h/30d timeline',     'orange'],
        'sim03' => ['SIM-03', 'Data Breach Supply Chain',   'Fornitore compromesso, Art.23 parallelo',     'red'],
        'sim04' => ['SIM-04', 'Whistleblowing SCADA',       'Segnalazione anonima tracciata a chiusura',   'purple'],
        'sim05' => ['SIM-05', 'Audit Chain Verify',         'Verifica integrità SHA-256 audit trail',      'green'],
        'sim06' => ['SIM-06', 'B2B License Provisioning',   'Invite token → org creata + SSO + JWT',       'yellow'],
    ];
    foreach ($simDefs as $id => [$code, $name, $desc, $col]) {
        $simCards .= <<<HTML
        <div class="sim-card" data-cmd="{$id}" onclick="runCmd('{$id}')">
            <span class="sim-badge badge-{$col}">{$code}</span>
            <div class="sim-info">
                <strong>{$name}</strong>
                <small>{$desc}</small>
            </div>
        </div>
        HTML;
    }
    $simCards .= '<hr class="btn-sep">';
    $simCards .= "<button class='btn' style='text-align:center' onclick=\"runCmd('simulate')\">Tutte le Simulazioni →</button>";

    // Level buttons
    $levelBtns = "<button class='btn btn-fullreset' onclick=\"runCmd('full-reset-sim')\">⚡ Reset + Simula + Testa Tutto</button>\n<hr class='btn-sep'>\n";
    $levelDefs = [
        'health'     => ['L0', 'Health Check', 'infra'],
        'smoke'      => ['L0', 'Smoke Tests',  'infra'],
        'l1-auth'    => ['L1', 'Auth & JWT',   'l1'],
        'l2-tenant'  => ['L2', 'Multi-Tenant', 'l2'],
        'l3-compliance' => ['L3', 'Compliance Core', 'l3'],
        'l4-b2b'     => ['L4', 'B2B & Services', 'l4'],
        'l5-export'  => ['L5', 'Export & Reports', 'l5'],
        'l6-ai'      => ['L6', 'AI Features', 'l6'],
        'chain-verify' => ['—', 'Hash Chain Verify', 'infra'],
        'full-suite' => ['ALL', 'Full Suite L1→L6', 'infra'],
        'reset'      => ['⚠', 'Reset Dati Demo', 'danger'],
    ];
    foreach ($levelDefs as $id => [$badge, $label, $cls]) {
        $danger = $cls === 'danger' ? ' btn-danger' : '';
        $levelBtns .= "<button class='btn lvl-btn{$danger}' data-level='{$cls}' onclick=\"runCmd('{$id}')\">"
            . "<span class='lvl-badge lvl-{$cls}'>{$badge}</span> {$label}</button>\n";
    }

    // Coverage endpoint map
    $coverageEndpoints = [
        ['Auth',     'POST /api/auth/login',            'L1', 'ok'],
        ['Auth',     'POST /api/auth/register',         'L1', 'ok'],
        ['Auth',     'GET  /api/auth/me',               'L1', 'ok'],
        ['Auth',     'POST /api/auth/refresh',          'L1', 'partial'],
        ['Auth',     'POST /api/auth/validate-invite',  'L4', 'ok'],
        ['Org',      'GET  /api/organizations/list',    'L2', 'ok'],
        ['Org',      'GET  /api/organizations/current', 'L2', 'ok'],
        ['Org',      'POST /api/organizations/create',  'L2', 'sim'],
        ['Dashboard','GET  /api/dashboard/overview',    'L2', 'ok'],
        ['Dashboard','GET  /api/dashboard/compliance-score', 'L3', 'ok'],
        ['Dashboard','GET  /api/dashboard/upcoming-deadlines', 'L3', 'ok'],
        ['Dashboard','GET  /api/dashboard/risk-heatmap', 'L3', 'partial'],
        ['Risk',     'GET  /api/risks/list',            'L3', 'ok'],
        ['Risk',     'GET  /api/risks/matrix',          'L3', 'ok'],
        ['Risk',     'POST /api/risks/create',          'sim','ok'],
        ['Incident', 'GET  /api/incidents/list',        'L3', 'ok'],
        ['Incident', 'POST /api/incidents/create',      'sim','ok'],
        ['Policy',   'GET  /api/policies/list',         'L3', 'partial'],
        ['Audit',    'GET  /api/audit/controls',        'L3', 'ok'],
        ['Audit',    'GET  /api/audit/export',          'L5', 'ok'],
        ['Audit',    'GET  /api/audit/iso27001-mapping','L5', 'ok'],
        ['Audit',    'GET  /api/audit/logs',            'L5', 'ok'],
        ['Supply',   'GET  /api/supply-chain/risk-overview', 'L4', 'ok'],
        ['Training', 'GET  /api/training/compliance-status', 'L4', 'ok'],
        ['NCR',      'GET  /api/ncr/stats',             'L4', 'ok'],
        ['Onboard',  'POST /api/onboarding/complete',   'sim','ok'],
        ['Admin',    'GET  /api/admin/stats',           'admin','skip'],
        ['CrossAI',  'GET  /api/cross-analysis/portfolio', 'L6', 'ok'],
        ['CrossAI',  'GET  /api/cross-analysis/history',   'L6', 'ok'],
        ['CrossAI',  'POST /api/cross-analysis/analyze',   'L6', 'ok'],
        ['Normative','GET  /api/normative/list',            'L6', 'ok'],
        ['Normative','POST /api/normative/{id}/ack',        'L6', 'sim'],
        ['Whistle',  'GET  /api/whistleblowing/stats',      'L6', 'ok'],
        ['Whistle',  'POST /api/whistleblowing/submit',     'L6', 'sim'],
    ];
    $covRows = '';
    foreach ($coverageEndpoints as [$module, $ep, $level, $status]) {
        $statusColor = match($status) {
            'ok'      => 'var(--green)',
            'partial' => 'var(--yellow)',
            'sim'     => 'var(--cyan)',
            'skip'    => 'var(--muted)',
            default   => 'var(--red)',
        };
        $statusLabel = match($status) {
            'ok'      => '✓ Testato',
            'partial' => '~ Parziale',
            'sim'     => '⚙ Solo SIM',
            'skip'    => '— Skip',
            default   => '✗ Mancante',
        };
        $covRows .= "<tr><td style='color:var(--muted)'>{$module}</td><td><code>{$ep}</code></td>"
            . "<td><span class='lvl-badge lvl-{$level}' style='font-size:.6rem'>{$level}</span></td>"
            . "<td style='color:{$statusColor};font-size:.72rem'>{$statusLabel}</td></tr>";
    }

    echo <<<HTML
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NIS2 Agile — Test Runner v2</title>
<style>
:root {
    --navy:   #0F172A;
    --slate:  #1E293B;
    --border: #334155;
    --cyan:   #06B6D4;
    --green:  #22C55E;
    --red:    #EF4444;
    --orange: #F97316;
    --yellow: #EAB308;
    --purple: #A855F7;
    --muted:  #64748B;
    --text:   #E2E8F0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { display: flex; height: 100vh; background: var(--navy); color: var(--text); font-family: 'Inter', system-ui, sans-serif; overflow: hidden; }

/* Sidebar */
.sidebar {
    width: 320px; min-width: 320px;
    background: var(--slate);
    border-right: 1px solid var(--border);
    display: flex; flex-direction: column;
    overflow: hidden;
}
.sidebar-header {
    padding: 1rem;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}
.sidebar-header h1 { font-size: .95rem; font-weight: 700; color: var(--cyan); }
.sidebar-header p  { font-size: .7rem; color: var(--muted); margin-top: .15rem; }

.badge-status {
    display: inline-flex; align-items: center; gap: .35rem;
    font-size: .68rem; font-weight: 600;
    padding: .18rem .5rem; border-radius: 999px; margin-top: .5rem;
}
.badge-status.up   { background: rgba(34,197,94,.15);  color: var(--green); }
.badge-status.down { background: rgba(239,68,68,.15);  color: var(--red);   }
.badge-status .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

/* Tabs */
.tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.tab  {
    flex: 1; padding: .5rem .25rem; font-size: .68rem; font-weight: 600;
    text-align: center; cursor: pointer; color: var(--muted);
    border-bottom: 2px solid transparent; transition: .15s;
}
.tab.active { color: var(--cyan); border-color: var(--cyan); }

.tab-panel { display: none; overflow-y: auto; flex: 1; }
.tab-panel.active { display: flex; flex-direction: column; }
.tab-inner { padding: .65rem; display: flex; flex-direction: column; gap: .45rem; }

/* Buttons */
.btn {
    width: 100%; padding: .5rem .65rem; border-radius: 6px; border: none;
    background: rgba(6,182,212,.1); color: var(--cyan); font-size: .78rem;
    font-weight: 600; cursor: pointer; text-align: left; transition: .15s;
    display: flex; align-items: center; gap: .5rem;
}
.btn:hover { background: rgba(6,182,212,.2); }
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn-danger { background: rgba(239,68,68,.1); color: var(--red); }
.btn-danger:hover { background: rgba(239,68,68,.2); }
.btn-fullreset {
    background: rgba(21,128,61,.2); color: #4ade80;
    border: 1px solid rgba(21,128,61,.4);
    font-size: .82rem; padding: .6rem .65rem; justify-content: center;
}
.btn-fullreset:hover { background: rgba(21,128,61,.4); border-color: #4ade80; }
.btn-sep { border: none; border-top: 1px solid var(--border); margin: .15rem 0; }

/* Level badges */
.lvl-badge {
    font-size: .62rem; font-weight: 700; padding: .15rem .4rem;
    border-radius: 3px; white-space: nowrap; flex-shrink: 0;
}
.lvl-l1    { background: rgba(6,182,212,.15);  color: var(--cyan);   }
.lvl-l2    { background: rgba(34,197,94,.15);  color: var(--green);  }
.lvl-l3    { background: rgba(249,115,22,.15); color: var(--orange); }
.lvl-l4    { background: rgba(168,85,247,.15); color: var(--purple); }
.lvl-l5    { background: rgba(234,179,8,.15);  color: var(--yellow); }
.lvl-l6    { background: rgba(16,185,129,.15); color: #10b981;       }
.lvl-infra { background: rgba(100,116,139,.15);color: var(--muted);  }
.lvl-sim   { background: rgba(6,182,212,.1);   color: var(--cyan);   }
.lvl-admin { background: rgba(239,68,68,.1);   color: var(--red);    }
.lvl-danger{ background: rgba(239,68,68,.1);   color: var(--red);    }
.lvl-ALL   { background: rgba(6,182,212,.2);   color: var(--cyan);   }

/* Sim cards */
.sim-card {
    display: flex; align-items: flex-start; gap: .6rem;
    padding: .6rem .7rem; border-radius: 8px;
    background: rgba(255,255,255,.03); border: 1px solid var(--border);
    cursor: pointer; transition: .15s;
}
.sim-card:hover { background: rgba(6,182,212,.06); border-color: var(--cyan); }
.sim-badge {
    font-size: .62rem; font-weight: 700; padding: .18rem .42rem;
    border-radius: 4px; white-space: nowrap; flex-shrink: 0; margin-top: .1rem;
}
.badge-cyan   { background: rgba(6,182,212,.15);  color: var(--cyan);   }
.badge-orange { background: rgba(249,115,22,.15); color: var(--orange); }
.badge-red    { background: rgba(239,68,68,.15);  color: var(--red);    }
.badge-purple { background: rgba(168,85,247,.15); color: var(--purple); }
.badge-green  { background: rgba(34,197,94,.15);  color: var(--green);  }
.badge-yellow { background: rgba(234,179,8,.15);  color: var(--yellow); }
.sim-info { display: flex; flex-direction: column; gap: .12rem; }
.sim-info strong { font-size: .8rem; color: var(--text); }
.sim-info small  { font-size: .7rem; color: var(--muted); }

/* Coverage table */
.cov-table, .cred-table, .stats-table {
    width: 100%; border-collapse: collapse; font-size: .7rem;
}
.cov-table th, .cred-table th, .stats-table th {
    background: rgba(255,255,255,.04); color: var(--muted);
    padding: .38rem .45rem; text-align: left; font-weight: 600;
    border-bottom: 1px solid var(--border); white-space: nowrap;
}
.cov-table td, .cred-table td, .stats-table td {
    padding: .35rem .45rem; border-bottom: 1px solid rgba(255,255,255,.03);
}
.cov-table code, .cred-table code {
    background: rgba(255,255,255,.06); padding: .1rem .28rem;
    border-radius: 3px; font-family: monospace; font-size: .67rem;
}

/* Stats grid */
.stats-grid {
    display: grid; grid-template-columns: 1fr 1fr; gap: .5rem;
    padding: .65rem;
}
.stat-card {
    background: rgba(255,255,255,.03); border: 1px solid var(--border);
    border-radius: 8px; padding: .65rem .75rem;
}
.stat-card .stat-val { font-size: 1.3rem; font-weight: 800; color: var(--cyan); }
.stat-card .stat-lbl { font-size: .68rem; color: var(--muted); margin-top: .15rem; }
.stat-total {
    font-size: .78rem; padding: .5rem .65rem;
    color: var(--muted); border-top: 1px solid var(--border);
}

/* History */
.history-panel {
    flex-shrink: 0; max-height: 160px; overflow-y: auto;
    border-top: 1px solid var(--border); padding: .5rem .65rem;
}
.history-panel h4 { font-size: .68rem; color: var(--muted); font-weight: 600; margin-bottom: .35rem; }
.history-item {
    display: flex; align-items: center; gap: .5rem;
    padding: .2rem 0; font-size: .7rem; cursor: pointer;
    border-bottom: 1px solid rgba(255,255,255,.03);
}
.history-item:hover .h-label { color: var(--cyan); }
.h-icon  { font-size: .65rem; flex-shrink: 0; }
.h-label { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.h-time  { font-size: .65rem; color: var(--muted); white-space: nowrap; }
.h-ok    { color: var(--green); }
.h-err   { color: var(--red); }
.h-empty { font-size: .7rem; color: var(--muted); font-style: italic; }

/* Running bar */
.running-bar {
    height: 3px; background: linear-gradient(90deg, var(--cyan), var(--purple), var(--cyan));
    background-size: 200% 100%; animation: slide 1.5s linear infinite; display: none;
}
.running-bar.active { display: block; }
@keyframes slide { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.elapsed { font-size: .68rem; color: var(--muted); padding: .35rem .75rem; min-height: 1.5rem; }

/* Terminal area */
.terminal-wrap { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.term-header {
    padding: .55rem 1rem; background: var(--slate);
    border-bottom: 1px solid var(--border);
    display: flex; align-items: center; gap: .65rem; flex-shrink: 0;
}
.term-dots span { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.term-dots .d1 { background: #EF4444; }
.term-dots .d2 { background: #EAB308; }
.term-dots .d3 { background: #22C55E; }
.term-title { font-size: .76rem; color: var(--muted); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.btn-clear { background: none; border: 1px solid var(--border); color: var(--muted); font-size: .7rem; padding: .18rem .55rem; border-radius: 4px; cursor: pointer; }
.btn-clear:hover { color: var(--text); border-color: var(--text); }
.btn-stop { background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.3); color: var(--red); font-size: .7rem; padding: .18rem .55rem; border-radius: 4px; cursor: pointer; display: none; }
.btn-stop:hover { background: rgba(239,68,68,.3); }
.btn-stop.visible { display: inline-block; }

#terminal {
    flex: 1; overflow-y: auto; overflow-x: hidden;
    padding: .7rem 1rem; background: var(--navy);
    font-family: 'JetBrains Mono', 'Fira Code', monospace;
    font-size: .76rem; line-height: 1.55;
    white-space: pre-wrap; word-break: break-all;
}
#terminal .ln { display: block; }
#terminal .ln-out  { color: #CBD5E1; }
#terminal .ln-err  { color: #FCA5A5; }
#terminal .ln-info { color: var(--cyan); }
#terminal .ln-ok   { color: var(--green); }
#terminal .ln-warn { color: var(--yellow); }
#terminal .ln-sep  { color: #2D3748; user-select: none; }
#terminal .ln-ts   { color: var(--muted); font-size: .68rem; }
</style>
</head>
<body>

<aside class="sidebar">
    <div class="sidebar-header">
        <h1>NIS2 Agile — Test Runner v2</h1>
        <p>Production · {$api}</p>
        <div id="api-status" class="badge-status down">
            <span class="dot"></span><span id="api-label">Controllo API...</span>
        </div>
    </div>

    <div class="tabs">
        <div class="tab active"  onclick="showTab('test')">Test</div>
        <div class="tab" onclick="showTab('sim')">Scenari</div>
        <div class="tab" onclick="showTab('cov')">Coverage</div>
        <div class="tab" onclick="showTab('stats')">DB Stats</div>
        <div class="tab" onclick="showTab('cred')">Credenziali</div>
    </div>

    <!-- Tab Test -->
    <div id="tab-test" class="tab-panel active">
        <div class="tab-inner">{$levelBtns}</div>
    </div>

    <!-- Tab Scenari -->
    <div id="tab-sim" class="tab-panel">
        <div class="tab-inner">{$simCards}</div>
    </div>

    <!-- Tab Coverage -->
    <div id="tab-cov" class="tab-panel">
        <div style="padding:.5rem .65rem; overflow-x:auto;">
            <p style="font-size:.68rem;color:var(--muted);margin-bottom:.5rem">
                Copertura endpoint API (35 totali)
            </p>
            <table class="cov-table">
                <thead><tr><th>Modulo</th><th>Endpoint</th><th>Lvl</th><th>Status</th></tr></thead>
                <tbody>{$covRows}</tbody>
            </table>
        </div>
    </div>

    <!-- Tab DB Stats -->
    <div id="tab-stats" class="tab-panel">
        <div class="stats-grid" id="stats-grid">
            <div class="stat-card"><div class="stat-val">—</div><div class="stat-lbl">Caricamento...</div></div>
        </div>
        <div class="stat-total" id="stats-total"></div>
        <div style="padding:.5rem .65rem;">
            <button class="btn" onclick="loadDbStats()" style="justify-content:center">
                Aggiorna conteggi DB
            </button>
        </div>
    </div>

    <!-- Tab Credenziali -->
    <div id="tab-cred" class="tab-panel">
        <div style="padding:.5rem .65rem; overflow-x:auto;">
            <p style="font-size:.68rem;color:var(--muted);margin-bottom:.45rem">Utenti demo (creati da simulazione):</p>
            <table class="cred-table">
                <thead><tr><th>Ruolo</th><th>Email</th><th>Password</th><th>Azienda</th></tr></thead>
                <tbody>{$credsRows}</tbody>
            </table>
            <p style="font-size:.65rem;color:var(--muted);margin-top:.65rem">
                URL: <code style="font-size:.62rem">{$url}</code><br>
                App: <a href="https://nis2.agile.software/" target="_blank" style="color:var(--cyan)">nis2.agile.software</a>
            </p>
        </div>
    </div>

    <div style="margin-top:auto; flex-shrink:0;">
        <div class="running-bar" id="running-bar"></div>
        <div class="elapsed" id="elapsed"></div>
    </div>

    <!-- Run History -->
    <div class="history-panel">
        <h4>Cronologia runs</h4>
        <div id="history-list"><p class="h-empty">Nessun run ancora</p></div>
    </div>
</aside>

<!-- Terminal -->
<div class="terminal-wrap">
    <div class="term-header">
        <div class="term-dots"><span class="d1"></span><span class="d2"></span><span class="d3"></span></div>
        <span class="term-title" id="term-title">Pronto · seleziona un test o una simulazione</span>
        <button class="btn-stop" id="btn-stop" onclick="stopCmd()">Interrompi</button>
        <button class="btn-clear" onclick="clearTerm()">Pulisci</button>
    </div>
    <div id="terminal">
        <span class="ln ln-info">NIS2 Agile Test Runner v2 — pronto.</span>
        <span class="ln ln-sep">────────────────────────────────────────────────────────────────────</span>
    </div>
</div>

<script>
const COMMANDS = {$cmdsJson};
let es = null;
let timerInterval = null;
let t0 = null;
let currentRun = null;

const HISTORY_KEY = 'nis2_run_history';
const MAX_HISTORY = 15;

// ── History ──────────────────────────────────────────────────────────────────
function loadHistory() {
    try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); } catch { return []; }
}
function saveHistory(runs) {
    localStorage.setItem(HISTORY_KEY, JSON.stringify(runs.slice(0, MAX_HISTORY)));
}
function addHistory(entry) {
    const runs = loadHistory();
    runs.unshift(entry);
    saveHistory(runs);
    renderHistory();
}
function renderHistory() {
    const list = document.getElementById('history-list');
    const runs = loadHistory();
    if (!runs.length) { list.innerHTML = '<p class="h-empty">Nessun run ancora</p>'; return; }
    list.innerHTML = runs.map(r => {
        const icon = r.status === 'ok' ? '✓' : r.status === 'err' ? '✗' : '~';
        const cls  = r.status === 'ok' ? 'h-ok' : r.status === 'err' ? 'h-err' : '';
        const time = new Date(r.ts).toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'});
        const elapsed = r.elapsed ? ` (\${r.elapsed}s)` : '';
        return `<div class="history-item" onclick="runCmd('\${r.id}')">
            <span class="h-icon \${cls}">\${icon}</span>
            <span class="h-label">\${r.label}</span>
            <span class="h-time">\${time}\${elapsed}</span>
        </div>`;
    }).join('');
}
renderHistory();

// ── API status check ─────────────────────────────────────────────────────────
async function checkApiStatus() {
    try {
        const r = await fetch('status', { signal: AbortSignal.timeout(6000) });
        const d = await r.json();
        const el  = document.getElementById('api-status');
        const lbl = document.getElementById('api-label');
        if (d.api_up) {
            el.className = 'badge-status up';
            lbl.textContent = 'API Online · v' + (d.api_info?.version || '1.x');
        } else {
            el.className = 'badge-status down';
            lbl.textContent = 'API Offline';
        }
    } catch {
        document.getElementById('api-label').textContent = 'API irraggiungibile';
    }
}
checkApiStatus();
setInterval(checkApiStatus, 30000);

// ── DB Stats ─────────────────────────────────────────────────────────────────
async function loadDbStats() {
    const grid = document.getElementById('stats-grid');
    const totalEl = document.getElementById('stats-total');
    grid.innerHTML = '<div style="padding:.65rem;font-size:.72rem;color:var(--muted)">Caricamento...</div>';
    try {
        const r = await fetch('db-stats', { signal: AbortSignal.timeout(8000) });
        const d = await r.json();
        if (d.error) { grid.innerHTML = `<div style="padding:.65rem;color:var(--red);font-size:.72rem">\${d.error}</div>`; return; }
        const stats = d.stats || {};
        const entries = Object.entries(stats).filter(([,v]) => v !== null);
        grid.innerHTML = entries.map(([t, v]) =>
            `<div class="stat-card">
                <div class="stat-val">\${v.toLocaleString('it-IT')}</div>
                <div class="stat-lbl">\${t}</div>
            </div>`
        ).join('');
        totalEl.textContent = `Totale righe DB: \${(d.total || 0).toLocaleString('it-IT')}`;
    } catch (e) {
        grid.innerHTML = `<div style="padding:.65rem;color:var(--red);font-size:.72rem">Errore: \${e.message}</div>`;
    }
}

// ── Tab switch ───────────────────────────────────────────────────────────────
function showTab(id) {
    const tabIds = ['test', 'sim', 'cov', 'stats', 'cred'];
    document.querySelectorAll('.tab').forEach((t, i) => t.classList.toggle('active', tabIds[i] === id));
    document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === 'tab-' + id));
    if (id === 'stats') loadDbStats();
}

// ── Terminal ─────────────────────────────────────────────────────────────────
function appendLine(cls, text) {
    const term = document.getElementById('terminal');
    const span = document.createElement('span');
    span.className = 'ln ln-' + cls;
    span.textContent = text;
    term.appendChild(span);
    term.scrollTop = term.scrollHeight;
}
function clearTerm() {
    document.getElementById('terminal').innerHTML =
        '<span class="ln ln-info">Terminal pulita.</span>';
}

// ── Run command ───────────────────────────────────────────────────────────────
function runCmd(id) {
    if (es) { es.close(); es = null; }
    clearInterval(timerInterval);

    const cmd = COMMANDS.find(c => c.id === id);
    const label = cmd ? cmd.label : id;

    currentRun = { id, label, ts: new Date().toISOString(), status: 'running', elapsed: null };

    document.getElementById('term-title').textContent = '▶ ' + label;
    document.getElementById('running-bar').classList.add('active');
    document.getElementById('btn-stop').classList.add('visible');
    document.querySelectorAll('.btn').forEach(b => b.disabled = true);

    appendLine('sep', '────────────────────────────────────────────────────────────────────');
    appendLine('ts',  new Date().toLocaleTimeString('it-IT') + '  ▶ ' + label);
    appendLine('sep', '────────────────────────────────────────────────────────────────────');

    t0 = Date.now();
    timerInterval = setInterval(() => {
        const s = ((Date.now() - t0) / 1000).toFixed(1);
        document.getElementById('elapsed').textContent = 'Elapsed: ' + s + 's';
    }, 300);

    es = new EventSource('events?cmd=' + encodeURIComponent(id));

    es.addEventListener('stdout', e => {
        const d = e.data;
        if (d.includes('[OK]') || d.startsWith('✓'))   appendLine('ok',   d);
        else if (d.includes('[WARN]'))                  appendLine('warn', d);
        else if (d.startsWith('━━━') || d.startsWith('════')) appendLine('info', d);
        else                                            appendLine('out',  d);
    });
    es.addEventListener('stderr', e => {
        const d = e.data;
        if (d.includes('[OK]') || d.startsWith('✓'))   appendLine('ok',   d);
        else if (d.includes('[WARN]') || d.includes('[INFO]')) appendLine('warn', d);
        else if (d.includes('[SKIP]'))                  appendLine('warn', d);
        else                                            appendLine('err',  d);
    });
    es.addEventListener('done', e => {
        let elapsed = null;
        try {
            const d = JSON.parse(e.data);
            elapsed = d.elapsed;
            appendLine('ok', '✓ Completato in ' + d.elapsed + 's');
        } catch { appendLine('ok', '✓ Completato'); }
        if (currentRun) { currentRun.status = 'ok'; currentRun.elapsed = elapsed; addHistory(currentRun); }
        finish();
    });
    es.onerror = () => {
        appendLine('err', '[ERRORE] Connessione SSE interrotta');
        if (currentRun) { currentRun.status = 'err'; addHistory(currentRun); }
        finish();
    };
}

function stopCmd() {
    if (es) { es.close(); es = null; }
    appendLine('warn', '[STOP] Interrotto dall\'utente');
    if (currentRun) { currentRun.status = 'err'; addHistory(currentRun); }
    finish();
}

function finish() {
    if (es) { es.close(); es = null; }
    clearInterval(timerInterval);
    document.getElementById('running-bar').classList.remove('active');
    document.getElementById('btn-stop').classList.remove('visible');
    document.getElementById('term-title').textContent = 'Completato · ' + new Date().toLocaleTimeString('it-IT');
    document.querySelectorAll('.btn').forEach(b => b.disabled = false);
    currentRun = null;
}
</script>
</body>
</html>
HTML;
}
