<?php
/**
 * Slice 8c-Close · CR + Phase closure with variance snapshot · end-to-end.
 *
 * Uses an isolated fresh phase on an existing works project so we don't disturb the
 * anchor CR that earlier Slice 8c-* verifies depend on.
 *
 *   1) Create a fresh phase on the anchor project. Attach 1 BOQ item.
 *   2) Draft a CR, add ONE MATERIALS line + ONE activity. Force through DRAFT →
 *      SUBMITTED → APPROVED → ACTIVE by direct SQL (chain bypass for verify).
 *   3) canClose rejects because the CR has no LPOs — WAIT no, canClose only checks
 *      *unpaid* LPOs. An ACTIVE CR with no LPOs is closeable (under-spend variance).
 *      Prove: source an LPO first, DON'T pay it → canClose rejects.
 *   4) Force the LPO to PAID directly → canClose passes → close() succeeds.
 *      Verify works_cr_closure_variance rows written for every pillar.
 *   5) Verify CR status flipped to CLOSED and further sourcing rejected.
 *   6) Phase canClose passes (only CR is CLOSED) → close() flips phase to CLOSED.
 *   7) Phase canClose now rejects (already CLOSED).
 *   8) Rerunnable purge at top so `run 1..N` all pass.
 */
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 (['WorksCrs.php','WorksPhases.php','WorksCrFulfilment.php','Lpos.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();
$wc  = new WorksCrs();
$wp  = new WorksPhases();
$wf  = new WorksCrFulfilment();

$pcId = 100; $pcNm = 'Verify PC'; $pcG = 'Projects Coordinator';

/* ── 0. Anchor project ─────────────────────────────────────────────── */
h('0. Pick anchor works project');
$anchor = $db->mySelect(
    "SELECT c0d, project_no FROM c_projects
      WHERE project_type='works' AND deleted=0 ORDER BY c0d DESC LIMIT 1",
    []
);
if (empty($anchor)) boom('No works project. Run slice8cSpine_e2e_verify.php first.');
$prjId = (int)$anchor[0]['c0d'];
p("Project #$prjId · {$anchor[0]['project_no']}");

/* ── 0b. Purge prior close-verify phases so reruns work ────────────── */
h('0b. Purge prior close-verify artefacts');
$oldPhases = $db->mySelect(
    "SELECT c0d FROM works_phases WHERE project_id = :p AND title LIKE 'CloseVerify %'",
    [':p' => $prjId]
) ?: [];
foreach ($oldPhases as $op) {
    $phId = (int)$op['c0d'];
    // CRs + fulfilment on those phases
    $crs = $db->mySelect("SELECT c0d FROM works_crs WHERE phase_id = :p", [':p' => $phId]) ?: [];
    foreach ($crs as $cr) {
        $cid = (int)$cr['c0d'];
        $lpos = $db->mySelect("SELECT c0d FROM lpos WHERE source_cr_id = :c", [':c' => $cid]) ?: [];
        foreach ($lpos as $lp) {
            $lid = (int)$lp['c0d'];
            $db->insertUpdate("DELETE FROM payment_allocations WHERE lpo_id = :l", [':l' => $lid]);
            $db->insertUpdate("DELETE FROM lpo_items           WHERE lpo_id = :l", [':l' => $lid]);
            $db->insertUpdate("DELETE FROM lpos                WHERE c0d   = :l", [':l' => $lid]);
        }
        $db->insertUpdate("DELETE FROM works_phase_items_issued WHERE cr_id = :c", [':c' => $cid]);
        $db->insertUpdate("DELETE FROM works_cr_closure_variance WHERE cr_id = :c", [':c' => $cid]);
        foreach (WorksCrs::PILLARS as $pil => $meta) {
            $db->insertUpdate("DELETE FROM `{$meta['table']}` WHERE cr_id = :c", [':c' => $cid]);
        }
        $db->insertUpdate("DELETE FROM works_cr_activities WHERE cr_id = :c", [':c' => $cid]);
        $db->insertUpdate("DELETE FROM works_crs WHERE c0d = :c", [':c' => $cid]);
    }
    $db->insertUpdate("DELETE FROM works_phase_activities WHERE phase_id = :p", [':p' => $phId]);
    $db->insertUpdate("DELETE FROM works_phase_boq_items  WHERE phase_id = :p", [':p' => $phId]);
    $db->insertUpdate("DELETE FROM works_phases WHERE c0d = :p", [':p' => $phId]);
}
p("Purged " . count($oldPhases) . " prior close-verify phases");

/* ── 1. Create fresh phase + attach a BOQ item ─────────────────────── */
h('1. Create fresh phase');
$phId = $wp->create($prjId, [
    'code' => 'CV-' . date('His'),
    'title' => 'CloseVerify ' . date('YmdHis'),
    'description' => 'Isolated phase for Slice 8c-Close verify',
    'planned_start' => date('Y-m-d'),
    'planned_end'   => date('Y-m-d', strtotime('+30 days')),
], $pcId);
p("Phase c0d=$phId");
$ph = $wp->findById($phId);
ok($ph !== null && $ph['status'] === 'PLANNED', 'phase created · PLANNED');

/* ── 2. Draft + fast-forward CR to ACTIVE ──────────────────────────── */
h('2. Draft CR, add line, fast-forward DRAFT→ACTIVE');
$crId = $wc->draft($prjId, $phId, ['title' => 'CloseVerify CR ' . date('His'), 'notes' => 'auto'], $pcId, $pcNm);
$wc->addLine($crId, 'MATERIALS', ['description' => 'Diesel', 'qty' => 4000, 'unit' => 'L', 'unit_cost' => 3000]);
$wc->addActivity($crId, ['title' => 'Access track clearance', 'unit' => 'm²', 'planned_qty' => 5000]);
// Fast-forward the CR: bypass the approval chain purely for verification.
$db->insertUpdate(
    "UPDATE works_crs SET status = 'APPROVED', edited_on = NOW() WHERE c0d = :id",
    [':id' => $crId]
);
$wc->activate($crId);
$cr = $wc->findById($crId);
p("CR c0d=$crId · status={$cr['status']} · MATERIALS budget=" . number_format((float)$cr['total_materials']));
ok($cr['status'] === 'ACTIVE', 'CR is ACTIVE');
$phAfter = $wp->findById($phId);
ok($phAfter['status'] === 'ACTIVE', 'phase auto-flipped to ACTIVE on CR activate');

/* ── 3. Guard: canClose rejects for read-only role ─────────────────── */
h('3. Guard · role restriction');
$roChk = $wc->canClose($crId, 'Executive');
ok(!$roChk['ok'] && stripos($roChk['msg'], 'PC / PM / Admin') !== false, 'Executive cannot close');

/* ── 4. Guard: canClose rejects when there's an unpaid LPO ─────────── */
h('4. Guard · unpaid LPO blocks close');
$lines = $wf->sourceableLines($crId)['MATERIALS'];
$line  = $lines[0];
$lpoRes = $wf->draftLpoFromCrLines(
    $crId, 'MATERIALS',
    ['vendor_id' => 1, 'vendor_name' => 'Furniture Direct Ltd', 'currency' => 'TZS'],
    [ ['line_id' => (int)$line['line_id'], 'qty' => 4000, 'unit_price' => 3000] ],
    $pcId, $pcNm
);
if (!$lpoRes['ok']) boom('draftLpoFromCrLines failed: ' . ($lpoRes['msg'] ?? '?'));
$lpoId = (int)$lpoRes['id'];
p("LPO {$lpoRes['lpo_no']} · DRAFT · 12M");
$unpaid = $wc->canClose($crId, $pcG);
ok(!$unpaid['ok'] && stripos($unpaid['msg'], 'Unpaid LPOs') !== false, 'unpaid LPO blocks close');

/* ── 5. Fast-forward LPO to PAID via a real payment ─────────────────── */
h('5. Force LPO to PAID (bypass chain + PV)');
$db->insertUpdate(
    "UPDATE lpos SET cash_out_status = 'PAID', edited_on = NOW() WHERE c0d = :id",
    [':id' => $lpoId]
);
// Also record a payment_allocations row so paid measure is truthful.
$payR = $db->insertUpdate(
    "INSERT INTO payments (payment_no, payment_voucher_id, bank_account_id, amount, currency,
        payment_method, reference_no, payment_date, received_by, created_by, edited_by)
     VALUES (:pn, 0, 1, 12000000, 'TZS', 'BANK_TRANSFER', 'CV-REF-' , CURDATE(), 'Vendor', :u, :u)",
    [':pn' => 'PAY-CV-' . date('His'), ':u' => $pcId]
);
$payId = (int)$payR['last_id'];
$db->insertUpdate(
    "INSERT INTO payment_allocations (payment_id, lpo_id, amount, allocation_type)
     VALUES (:pay, :lpo, 12000000, 'FULL')",
    [':pay' => $payId, ':lpo' => $lpoId]
);
$paidChk = $wc->canClose($crId, $pcG);
ok($paidChk['ok'], 'canClose passes after LPO PAID');

/* ── 6. Happy path: close CR → variance snapshot written ───────────── */
h('6. Close CR (happy path)');
$closeRes = $wc->close($crId, 'CloseVerify · scope executed', $pcId, $pcNm, $pcG);
ok($closeRes['ok'], 'close() succeeds');

$crAfter = $wc->findById($crId);
ok($crAfter['status'] === 'CLOSED',                             'CR flipped to CLOSED');
ok(!empty($crAfter['closed_on']),                                'closed_on stamped');
ok((int)$crAfter['closed_by']   === $pcId,                       'closed_by stamped');
ok((string)$crAfter['close_reason'] !== '',                      'close_reason persisted');

$vars = $wc->closureVarianceFor($crId);
$byPillar = [];
foreach ($vars as $v) $byPillar[$v['pillar']] = $v;
ok(count($vars) === count(WorksCrs::PILLARS),                    'variance row for every pillar');
ok(isset($byPillar['MATERIALS']),                                'MATERIALS variance snapshotted');
$m = $byPillar['MATERIALS'];
p(sprintf("  MATERIALS budget=%s paid=%s variance=%s (%.1f%%)",
    number_format((float)$m['budget']), number_format((float)$m['paid']),
    number_format((float)$m['variance_amount']), (float)$m['variance_pct']));
ok(abs((float)$m['budget'] - 12000000) < 0.01,                   'MATERIALS budget = 12M');
ok(abs((float)$m['paid']   - 12000000) < 0.01,                   'MATERIALS paid = 12M');
ok((float)$m['variance_amount'] === 0.0 || abs((float)$m['variance_amount']) < 0.01, 'on-budget → variance 0');

// Other pillars (0 budget) should show 0 across the board.
$prel = $byPillar['PRELIMINARIES'];
ok((float)$prel['budget'] === 0.0, 'PRELIMINARIES budget 0 (no lines)');
ok((float)$prel['paid']   === 0.0, 'PRELIMINARIES paid   0');

/* ── 7. Guard: further sourcing rejected after close ────────────────── */
h('7. Guard · sourcing locked after close');
$blocked = $wf->draftLpoFromCrLines(
    $crId, 'MATERIALS',
    ['vendor_id' => 2, 'vendor_name' => 'Ergo Office Systems', 'currency' => 'TZS'],
    [ ['line_id' => (int)$line['line_id'], 'qty' => 100, 'unit_price' => 3000] ],
    $pcId, $pcNm
);
ok(!$blocked['ok'] && stripos($blocked['msg'], 'must be ACTIVE') !== false, 'sourcing refused on CLOSED CR');

/* ── 8. Phase closure ──────────────────────────────────────────────── */
h('8. Close phase');
// Guard: only PC/PM/Admin
$phRo = $wp->canClose($phId, 'Executive');
ok(!$phRo['ok'] && stripos($phRo['msg'], 'PC / PM / Admin') !== false, 'Executive cannot close phase');

// All CRs on this phase are CLOSED (just the one we made) → happy path.
$phChk = $wp->canClose($phId, $pcG);
ok($phChk['ok'], 'phase canClose passes when only CR is CLOSED');

$phRes = $wp->close($phId, $pcId, $pcNm, $pcG);
ok($phRes['ok'], 'phase close() succeeds');
$phAfter2 = $wp->findById($phId);
ok($phAfter2['status'] === 'CLOSED',              'phase flipped to CLOSED');
ok(!empty($phAfter2['actual_end']),                'phase actual_end stamped');
ok(!empty($phAfter2['closed_on']),                 'phase closed_on stamped');
ok((int)$phAfter2['closed_by']   === $pcId,        'phase closed_by stamped');

// Second call rejected
$again = $wp->canClose($phId, $pcG);
ok(!$again['ok'] && stripos($again['msg'], 'already CLOSED') !== false, 're-close of CLOSED phase rejected');

/* ── 9. Guard: phase close rejects when an open CR exists ──────────── */
h('9. Guard · open CR blocks phase close');
$phId2 = $wp->create($prjId, [
    'code' => 'CV2-' . date('His'),
    'title' => 'CloseVerify OpenCR ' . date('YmdHis'),
    'planned_start' => date('Y-m-d'), 'planned_end' => date('Y-m-d', strtotime('+7 days')),
], $pcId);
$openCrId = $wc->draft($prjId, $phId2, ['title' => 'CloseVerify open ' . date('His')], $pcId, $pcNm);
$openChk = $wp->canClose($phId2, $pcG);
ok(!$openChk['ok'] && stripos($openChk['msg'], 'Open CRs') !== false, 'open CR blocks phase close');

/* ── 10. Perms + view surface ──────────────────────────────────────── */
h('10. Perms + view surface');
foreach (['Projects Coordinator','Project Manager','Administrator'] as $role) {
    $rr = $db->mySelect("SELECT c0d FROM user_groups WHERE user_group_name = :n", [':n' => $role]);
    $gid = (int)($rr[0]['c0d'] ?? 0);
    foreach ([['WorksCrs','closeAj'], ['WorksPhases','closeAj']] as $pair) {
        $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' => $pair[0], ':a' => $pair[1]]
        );
        ok(!empty($r), "$role · {$pair[0]}/{$pair[1]}");
    }
}
$fulfilBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksCrs' . DS . 'fulfil.php');
ok(strpos($fulfilBody, 'btn_close_cr')   !== false, 'fulfil view has Close CR button');
ok(strpos($fulfilBody, 'WorksCrs/closeAj') !== false, 'fulfil view POSTs to closeAj');
$viewBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksCrs' . DS . 'view.php');
ok(strpos($viewBody, 'Closure variance snapshot') !== false, 'CR view renders variance card');
$phaseBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksPhases' . DS . 'view.php');
ok(strpos($phaseBody, 'btn_close_phase')  !== false, 'phase view has Close phase button');
ok(strpos($phaseBody, 'WorksPhases/closeAj') !== false, 'phase view POSTs to closeAj');

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