<?php
/**
 * Slice 8c-Surfaces · Works UI surfaces end-to-end verify.
 *
 * Goal: verify the plumbing that lets a user reach and use the WorksBoq /
 * WorksPhases / WorksCrs pages from the sidebars — assuming the model layer
 * (Slice 8c-Spine verify covers that) is already sound.
 *
 *   1) ProjectStarter Works pack points at real routes (WorksBoq/view,
 *      WorksPhases/plan, WorksCrs/create) — not the old placeholders.
 *   2) Perms granted for every action a role touches: index / plan / view /
 *      create / *Aj across the 3 controllers, per the target role matrix.
 *   3) Every role sidebar that should surface Works has the section wired
 *      and the section will actually render for that role's perms.
 *   4) Every rendered view file exists on disk (no dead links).
 *   5) The 3 index actions' queries return well-formed rows on a project
 *      that has data from the Slice 8c-Spine verify.
 *   6) The CR viewAction data hydrations (7 pillars + activities + phase
 *      pickers + chain history if submitted) load without error.
 */
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 (['UserGroups.php','ProjectStarter.php','WorksBoq.php','WorksPhases.php','WorksCrs.php','ApprovalChain.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) { echo "\nFATAL: $why\n"; exit(1); }

$db = new DB();

/* ── 1. ProjectStarter Works pack URLs ─────────────────────────────── */
h('1. ProjectStarter Works pack routes updated');
$ps = new ProjectStarter();
$ref = new ReflectionMethod($ps, 'worksPack');
$ref->setAccessible(true);
$pack = $ref->invoke($ps);
$byKey = [];
foreach ($pack as $t) $byKey[$t['task_key']] = $t;

$expected = [
    'IMPORT_BOQ'     => 'WorksBoq/view/{project_id}',
    'PHASE_PLANNING' => 'WorksPhases/plan/{project_id}',
    'FIRST_CR'       => 'WorksCrs/create/{project_id}',
];
foreach ($expected as $k => $exp) {
    $got = $byKey[$k]['target_url_template'] ?? '(missing)';
    ok($got === $exp, "task $k → $exp (got: $got)");
}

// Sanity: nothing points at the retired /WorksCr (no plural) or /import
foreach ($pack as $t) {
    $u = $t['target_url_template'];
    ok(strpos($u, 'WorksCr/') === false || strpos($u, 'WorksCrs/') === 0, "no legacy WorksCr URL in {$t['task_key']}: $u");
    ok(strpos($u, 'WorksBoq/import') === false, "no legacy WorksBoq/import URL in {$t['task_key']}: $u");
}

/* ── 2. Perms grid ─────────────────────────────────────────────────── */
h('2. Perms granted per role matrix');
$rolePerms = [
    'Projects Coordinator' => [
        'WorksBoq'    => ['index','view'],
        'WorksPhases' => ['index','plan','view','createAj','attachBoqItemAj','addActivityAj','markClosedAj'],
        'WorksCrs'    => ['index','create','view','createAj','addLineAj','deleteLineAj','addActivityAj','submitAj'],
    ],
    'Project Manager' => [
        'WorksBoq'    => ['index','view'],
        'WorksPhases' => ['index','plan','view','createAj','attachBoqItemAj','addActivityAj','markClosedAj'],
        'WorksCrs'    => ['index','create','view','submitAj'],
    ],
    'Field Supervisor' => [
        'WorksBoq'    => ['index','view'],
        'WorksPhases' => ['index','plan','view'],
        'WorksCrs'    => ['index','create','view','createAj','addLineAj','addActivityAj','submitAj'],
    ],
    'Executive' => [
        'WorksBoq'    => ['index','view'],
        'WorksPhases' => ['index','plan','view'],
        'WorksCrs'    => ['index','create','view'],
    ],
    'Finance Manager' => [
        'WorksBoq'    => ['index','view'],
        'WorksPhases' => ['index','plan','view'],
        'WorksCrs'    => ['index','create','view'],
    ],
    'Accountant' => [
        'WorksBoq'    => ['index','view'],
        'WorksPhases' => ['index','plan','view'],
        'WorksCrs'    => ['index','create','view'],
    ],
    'Administrator' => [
        'WorksBoq'    => ['index','view','import'],
        'WorksPhases' => ['index','plan','view','createAj','attachBoqItemAj','addActivityAj','markClosedAj'],
        'WorksCrs'    => ['index','create','view','createAj','addLineAj','deleteLineAj','addActivityAj','submitAj'],
    ],
];
foreach ($rolePerms as $role => $ctrls) {
    $rr = $db->mySelect("SELECT c0d FROM user_groups WHERE user_group_name = :n AND deleted = 0", [':n' => $role]);
    $gid = (int)($rr[0]['c0d'] ?? 0);
    if ($gid === 0) { p("skip: role '$role' not found"); continue; }
    foreach ($ctrls as $ctrl => $actions) {
        foreach ($actions as $act) {
            $r = $db->mySelect(
                "SELECT 1 FROM user_groups_perms WHERE user_group_id = :g AND controller_allowed = :c AND action_allowed = :a LIMIT 1",
                [':g' => $gid, ':c' => $ctrl, ':a' => $act]
            );
            ok(!empty($r), "$role · $ctrl/$act");
        }
    }
}

/* ── 3. Sidebar files reference the new controllers ────────────────── */
h('3. Sidebars wired');
$sidebars = [
    'projects_coordinator' => ['WorksBoq','WorksPhases','WorksCrs'],
    'project_manager'      => ['WorksBoq','WorksPhases','WorksCrs'],
    'field_supervisor'     => ['WorksBoq','WorksPhases','WorksCrs'],
    'administrator'        => ['WorksBoq','WorksPhases','WorksCrs'],
    'executive'            => ['WorksBoq','WorksPhases','WorksCrs'],
];
foreach ($sidebars as $role => $ctrls) {
    $file = ROOT . DS . 'app' . DS . 'views' . DS . 'layouts' . DS . 'partials' . DS . "_sidebar_$role.php";
    if (!is_file($file)) { ok(false, "$role sidebar file exists at $file"); continue; }
    $body = (string)file_get_contents($file);
    foreach ($ctrls as $c) {
        ok(strpos($body, "PROOT; ?>$c") !== false, "$role sidebar links to $c");
    }
}

/* ── 4. View files exist ───────────────────────────────────────────── */
h('4. View files present');
$views = [
    'WorksBoq/view','WorksBoq/index',
    'WorksPhases/plan','WorksPhases/view','WorksPhases/index',
    'WorksCrs/create','WorksCrs/view','WorksCrs/index',
];
foreach ($views as $v) {
    $file = ROOT . DS . 'app' . DS . 'views' . DS . str_replace('/', DS, $v) . '.php';
    ok(is_file($file), "view exists: $v.php");
}

/* ── 5. Index queries return rows (needs a works project with data) ─ */
h('5. Index queries hydrate');
$row = $db->mySelect(
    "SELECT p.c0d project_id, b.c0d boq_id
       FROM c_projects p
       JOIN works_boqs b ON b.project_id = p.c0d AND b.deleted = 0
      WHERE p.deleted = 0 AND p.project_type = 'works'
      ORDER BY p.c0d DESC LIMIT 1",
    []
);
$pid   = (int)($row[0]['project_id'] ?? 0);
$boqId = (int)($row[0]['boq_id'] ?? 0);
if ($pid === 0) {
    p('SKIP · no works project with a BOQ found — run scripts/slice8cSpine_e2e_verify.php first');
    echo "\n=== END (partial) ===\n";
    exit(0);
}
p("Anchor works project #$pid · BOQ #$boqId");

// WorksBoq/index equivalent
$boqRows = $db->mySelect(
    "SELECT p.c0d, p.project_no, p.project_name, b.c0d boq_id, b.contract_value
       FROM works_boqs b JOIN c_projects p ON p.c0d = b.project_id AND p.deleted = 0
      WHERE b.deleted = 0",
    []
) ?: [];
ok(count($boqRows) >= 1, 'WorksBoq/index returns >= 1 project');

// WorksPhases/index
$wp = new WorksPhases();
$phaseRows = $db->mySelect(
    "SELECT ph.c0d, ph.phase_no, ph.title, ph.status, p.project_no
       FROM works_phases ph JOIN c_projects p ON p.c0d = ph.project_id AND p.deleted = 0
      WHERE ph.deleted = 0",
    []
) ?: [];
ok(count($phaseRows) >= 1, 'WorksPhases/index returns >= 1 phase');

// WorksCrs/index
$crRows = $db->mySelect(
    "SELECT c.c0d, c.cr_no, c.status, c.grand_total, p.project_no, ph.phase_no
       FROM works_crs c JOIN c_projects p ON p.c0d = c.project_id AND p.deleted = 0
       LEFT JOIN works_phases ph ON ph.c0d = c.phase_id
      WHERE c.deleted = 0",
    []
) ?: [];
ok(count($crRows) >= 1, 'WorksCrs/index returns >= 1 CR');

/* ── 6. CR view page hydration (mimic controller viewAction) ───────── */
h('6. CR viewAction data hydrates');
$wc = new WorksCrs();
$anCr = $db->mySelect("SELECT c0d FROM works_crs WHERE project_id = :p AND deleted = 0 ORDER BY c0d DESC LIMIT 1", [':p' => $pid]);
$crId = (int)($anCr[0]['c0d'] ?? 0);
if ($crId === 0) { p('SKIP · no CR on anchor project'); }
else {
    $cr = $wc->findById($crId);
    ok(!empty($cr), "findById($crId) returns a row");
    $lines = $wc->allLines($crId);
    ok(is_array($lines) && isset($lines['PRELIMINARIES']), 'allLines() returns 7 pillar arrays');
    foreach (['PRELIMINARIES','MANPOWER','MATERIALS','EQUIPMENT','TOOLS','SUBCONTRACTS','OVERHEADS'] as $pillar) {
        ok(array_key_exists($pillar, $lines), "  pillar bucket present: $pillar");
    }
    $acts = $wc->activitiesFor($crId);
    ok(is_array($acts), 'activitiesFor() returns array');
    // Chain history — an approved v1 will have one; a draft won't.
    if (!empty($cr['approval_id'])) {
        $ac = new ApprovalChain();
        $hist = $ac->historyFor((int)$cr['approval_id']);
        ok(is_array($hist), 'chain history loads for submitted CR');
    }
    p("CR c0d=$crId status={$cr['status']} version=" . (int)$cr['version'] . " grand_total=" . number_format((float)$cr['grand_total']));
}

/* ── 7. Phase view page hydration ──────────────────────────────────── */
h('7. Phase viewAction data hydrates');
$anPh = $db->mySelect("SELECT c0d FROM works_phases WHERE project_id = :p AND deleted = 0 ORDER BY c0d DESC LIMIT 1", [':p' => $pid]);
$phId = (int)($anPh[0]['c0d'] ?? 0);
if ($phId === 0) { p('SKIP · no phase on anchor project'); }
else {
    $ph = $wp->findById($phId);
    ok(!empty($ph), "findById($phId) returns a row");
    $items = $wp->boqItemsFor($phId);
    ok(is_array($items), 'boqItemsFor() returns array');
    $acts = $wp->activitiesFor($phId);
    ok(is_array($acts), 'activitiesFor() returns array');
    p("Phase c0d=$phId status={$ph['status']} · " . count($items) . ' BOQ items · ' . count($acts) . ' activities');
}

/* ── 8. BOQ view page hydration ────────────────────────────────────── */
h('8. BOQ viewAction data hydrates');
$wb = new WorksBoq();
$boq = $wb->findByProject($pid);
ok(!empty($boq), "findByProject($pid) returns BOQ header");
$boqItems = $wb->itemsFor((int)$boq['c0d']);
ok(is_array($boqItems) && count($boqItems) >= 1, 'itemsFor() returns items');
$tot = $wb->totals((int)$boq['c0d']);
ok(!empty($tot) && isset($tot['grand_total']), 'totals() returns aggregate row');
p("BOQ #{$boq['c0d']} · " . count($boqItems) . ' items · grand_total=' . number_format((float)$tot['grand_total']));

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