<?php
/**
 * Slice 8c-Trigger · sidebar badge counts + per-project alert strip + wire audit.
 *
 * Also serves as the Works-module wire audit: enumerates every Works* controller
 * built during the 8c slices and asserts sidebar entries, perms and cross-links.
 */
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(__DIR__));
require_once ROOT . DS . 'config' . DS . 'config.php';
require_once ROOT . DS . 'core' . DS . 'Application.php';
require_once ROOT . DS . 'core' . DS . 'Model.php';
require_once ROOT . DS . 'core' . DS . 'DB.php';
$_SESSION = $_SESSION ?? [];
foreach (['WorksAlerts.php','WorksProjectReport.php','WorksIpcs.php','WorksBoq.php','WorksRetention.php','WorksCustomerRetention.php'] as $m) {
    require_once ROOT . DS . 'app' . DS . 'models' . DS . $m;
}

function h(string $t): void { echo "\n==== $t ====\n"; }
function p(string $t): void { echo "  $t\n"; }
function ok(bool $c, string $m): void { echo ($c ? "  ✓ " : "  ✗ ") . $m . "\n"; }
function boom(string $why): void { echo "\nFATAL: $why\n"; exit(1); }

$db = new DB();
$wa = new WorksAlerts();

/* ── 1. alertsFor filter ──────────────────────────────────────────── */
h('1. alertsFor($projectId) filters correctly');
$all = $wa->allAlerts();
if (empty($all)) {
    p('SKIP · no live alerts to filter by (all clear)');
} else {
    $someProject = (int)$all[0]['project_id'];
    $filt = $wa->alertsFor($someProject);
    ok(!empty($filt),                                              'alertsFor returns >= 1 for a project with alerts');
    $wrongProject = 0;
    foreach ($filt as $a) if ((int)$a['project_id'] !== $someProject) $wrongProject++;
    ok($wrongProject === 0,                                        'every returned alert matches project_id');
    ok(count($filt) <= count($all),                                'filtered subset ≤ all');
}
$empty = $wa->alertsFor(999999);
ok(is_array($empty) && count($empty) === 0,                        'alertsFor(missing_id) returns []');

/* ── 2. cachedCounts populates + reuses session ──────────────────── */
h('2. cachedCounts session TTL');
WorksAlerts::invalidateCounts();
$c1 = WorksAlerts::cachedCounts();
ok(is_array($c1) && isset($c1['HIGH'], $c1['MEDIUM'], $c1['LOW'], $c1['total']), 'first call populates all 4 keys');
$writtenAt = $_SESSION['works_alerts_counts_at'] ?? 0;
ok($writtenAt > 0,                                                'session cache stamped');
// Second call within TTL — should return cached (same array)
$c2 = WorksAlerts::cachedCounts();
$writtenAt2 = $_SESSION['works_alerts_counts_at'] ?? 0;
ok($writtenAt === $writtenAt2,                                    'second call reuses cache (no rewrite)');
// Invalidate + refresh
WorksAlerts::invalidateCounts();
ok(empty($_SESSION['works_alerts_counts']),                       'invalidateCounts clears the cache key');
$c3 = WorksAlerts::cachedCounts();
ok(!empty($_SESSION['works_alerts_counts']),                       'after invalidate, next call re-populates cache');
ok(is_array($c3) && $c3['total'] === $c1['total'],                 'refreshed counts match (same underlying state)');

/* ── 3. Report view has alert strip surface ────────────────────── */
h('3. Report view exposes the alert strip');
$viewBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksProjectReport' . DS . 'view.php');
ok(strpos($viewBody, "\$this->data['alerts']") !== false,         'view reads alerts from controller data');
ok(strpos($viewBody, 'alert strip')            !== false || strpos($viewBody, 'alerts on this project') !== false, 'view has strip label');
ok(strpos($viewBody, 'alertsFor')              === false,          'view does not call model directly (controller passes data)');
$ctrlBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'WorksProjectReportController.php');
ok(strpos($ctrlBody, "'alerts'")               !== false,          'controller passes alerts');
ok(strpos($ctrlBody, 'alertsFor($projectId)')  !== false,          'controller calls alertsFor');

/* ── 4. Sidebar badges wired on all 5 sidebars ──────────────────── */
h('4. Sidebar badges');
foreach (['projects_coordinator','project_manager','field_supervisor','administrator','executive'] as $role) {
    $body = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'layouts' . DS . 'partials' . DS . "_sidebar_$role.php");
    ok(strpos($body, 'WorksAlerts::cachedCounts')             !== false, "_sidebar_$role calls cachedCounts");
    ok(strpos($body, "PROOT; ?>WorksAlerts")                  !== false, "_sidebar_$role has WorksAlerts link");
    ok(strpos($body, "\$wac['HIGH']")                          !== false, "_sidebar_$role reads HIGH count");
}

/* ── 5. WIRE AUDIT · Every Works* controller has full wire ─────── */
h('5. Wire audit · every Works* controller');
// Controller → sidebars it must appear in (empty = no sidebar entry required, project-scoped only)
$wire = [
    'WorksBoq'               => ['sidebars' => ['projects_coordinator','project_manager','field_supervisor','administrator','executive']],
    'WorksPhases'            => ['sidebars' => ['projects_coordinator','project_manager','field_supervisor','administrator','executive']],
    'WorksCrs'               => ['sidebars' => ['projects_coordinator','project_manager','field_supervisor','administrator','executive']],
    'WorksProjectReport'     => ['sidebars' => ['projects_coordinator','project_manager','field_supervisor','administrator','executive']],
    'WorksIpcs'              => ['sidebars' => ['projects_coordinator','project_manager','field_supervisor','administrator','executive']],
    'WorksAlerts'            => ['sidebars' => ['projects_coordinator','project_manager','field_supervisor','administrator','executive']],
    'WorksRetention'         => ['sidebars' => []],   // project-scoped, reached via report
    'WorksCustomerRetention' => ['sidebars' => []],   // project-scoped, reached via report + IPCs
];
foreach ($wire as $ctrl => $meta) {
    // Controller file exists
    $cFile = ROOT . DS . 'app' . DS . 'controllers' . DS . $ctrl . 'Controller.php';
    ok(is_file($cFile), "controller file: $ctrl");
    // At least one role has perms
    $r = $db->mySelect(
        "SELECT COUNT(*) n FROM user_groups_perms WHERE controller_allowed = :c",
        [':c' => $ctrl]
    );
    ok((int)$r[0]['n'] > 0, "$ctrl has perms rows");
    // Sidebar wiring
    foreach ($meta['sidebars'] as $role) {
        $body = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'layouts' . DS . 'partials' . DS . "_sidebar_$role.php");
        ok(strpos($body, "PROOT; ?>$ctrl") !== false, "  _sidebar_$role links $ctrl");
    }
}

/* ── 6. Cross-link surface: project report is the hub ─────────── */
h('6. Cross-links from project report');
$rpt = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksProjectReport' . DS . 'view.php');
foreach ([
    'WorksBoq/view/'                => 'BOQ',
    'WorksPhases/plan/'             => 'Phases',
    'WorksIpcs/list/'               => 'IPCs',
    'WorksCustomerRetention/pool/'  => 'Customer retention',
    'WorksRetention/pool/'          => 'Supplier retention',
    'WorksAlerts'                   => 'Alerts',
    'WorksProjectReport'            => 'All reports',
] as $frag => $label) {
    ok(strpos($rpt, $frag) !== false, "project report links to $label ($frag)");
}

/* ── 7. Cross-link surface: BOQ view is the operational hub ────── */
h('7. Cross-links from BOQ view');
$boq = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksBoq' . DS . 'view.php');
foreach ([
    'WorksProjectReport/view/' => 'Report',
    'WorksIpcs/list/'          => 'IPCs',
    'WorksPhases/plan/'        => 'Phases',
] as $frag => $label) {
    ok(strpos($boq, $frag) !== false, "BOQ view links to $label ($frag)");
}

/* ── 8. HTTP-level smoke of every Works* URL (302 = ok, 500 = broken) ─ */
h('8. HTTP smoke check');
$anchor = $db->mySelect("SELECT p.c0d FROM c_projects p JOIN works_boqs b ON b.project_id = p.c0d WHERE p.project_type='works' AND p.deleted=0 ORDER BY p.c0d ASC LIMIT 1", []);
$prjId = (int)($anchor[0]['c0d'] ?? 0);
$urls = [
    'WorksBoq', 'WorksBoq/view/' . $prjId,
    'WorksPhases', 'WorksPhases/plan/' . $prjId,
    'WorksCrs', 'WorksCrs/create/' . $prjId,
    'WorksProjectReport', 'WorksProjectReport/view/' . $prjId,
    'WorksRetention/pool/' . $prjId,
    'WorksCustomerRetention/pool/' . $prjId,
    'WorksIpcs', 'WorksIpcs/list/' . $prjId, 'WorksIpcs/create/' . $prjId,
    'WorksAlerts',
];
foreach ($urls as $u) {
    $ch = curl_init('http://localhost/rsflow.dspera/' . $u);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_NOBODY => true]);
    curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    // 302 = login redirect (route + perms gate hit cleanly); 200 = served; 500 = fatal
    ok($code === 302 || $code === 200, "GET /$u → $code");
}

echo "\n=== END ===\n";
