<?php
/**
 * Slice 8c-Spine · Works Framework end-to-end verify.
 *
 *   1) Drive a Works tender through Slices 3/4/5 → project auto-provisioned.
 *      Verify works_boqs + works_boq_items snapshotted from tender pricing.
 *   2) Create Phase 1 · attach 2 BOQ items with planned qty · add 2 activities.
 *   3) Draft CR v1 against Phase 1 · add lines across all 7 pillars · add activities.
 *      Verify grand_total rollup matches sum of pillars.
 *   4) Try to submit with no activities → BLOCKED. Add activity, submit → chain in progress.
 *   5) Walk 4-step chain PC → PM → FM → Exec. On terminal, CR → ACTIVE via postDecisionHook,
 *      phase → ACTIVE.
 *   6) Try to draft another CR while ACTIVE exists → ALLOWED (revision path).
 *   7) Draft CR v2 (revision), submit + approve chain → v2 ACTIVE, v1 SUPERSEDED.
 *   8) Draft a Variation, price 2 items, submit chain → 4 steps → APPROVED →
 *      2 rows appended to works_boq_items with origin='VARIATION'; BOQ header value bumped.
 */
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 ([
    'ApprovalChain.php','Tenders.php','TenderPricing.php','ProjectStarter.php','Notifications.php',
    'WorksBoq.php','WorksPhases.php','WorksCrs.php','WorksVariations.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();
$ac = new ApprovalChain();
$tm = new Tenders();
$pm = new TenderPricing();
$wb = new WorksBoq();
$wp = new WorksPhases();
$wc = new WorksCrs();
$wv = new WorksVariations();

$uidCrm = 100; $unmCrm = 'CRM Test User';       $ugCrm  = 'CRM';
$uidFm  = 3;   $unmFm  = 'Finance Manager Test'; $ugFm  = 'Finance Manager';
$uidExec= 2;   $unmExec= 'Executive Test';      $ugExec = 'Executive';
$accId  = 4;   $accNm  = 'Accountant Test';
$procId = 8;   $procNm = 'Procurement Test';
$pmId   = 5;   $pmNm   = 'Project Manager Test';
$pcRow  = $db->mySelect("SELECT c0d, full_name FROM users WHERE user_group_name='Projects Coordinator' AND status='ACTIVE' AND deleted=0 ORDER BY c0d LIMIT 1", []);
$pcId   = (int)($pcRow[0]['c0d'] ?? 0);
$pcNm   = (string)($pcRow[0]['full_name'] ?? '');

/* ── Step 1 ── Full pipeline to project + BOQ snapshot ─────────────── */
h('1. Drive Works pipeline → project provisioned → BOQ snapshot');
$stamp = date('YmdHis');
$fb = $db->insertUpdate(
    "INSERT INTO tender_tracker
        (tender_no, tender_title, source_type, buyer_name, project_type,
         date_received, estimated_value, currency, tender_stage, tender_status,
         created_by, created_by_name, created_on)
     VALUES (:tn, :tt, 'PRIVATE_CLIENT', 'TARURA', 'works',
         CURDATE(), 500000000, 'TZS', 'PREP', 'OPEN', :u, :un, NOW())",
    [':tn' => 'T-WORKS-' . $stamp, ':tt' => 'SLICE 8c · Works framework verify · road drainage', ':u' => $uidCrm, ':un' => $unmCrm]
);
$tid = (int)($fb['last_id'] ?? 0);
foreach (['SUBMISSION','FOLLOW_UP','OPENING'] as $s) $tm->advanceStage($tid, $s, 'x', $uidCrm, $unmCrm);
// Build pricing (BOQ source): 2 sections, 4 lines
$vid = $pm->createVersion($tid, null, $uidCrm, $unmCrm);
$s1  = $pm->addSection($vid, $tid, 'Site works',   'Clearance + earthworks', $uidCrm);
$s2  = $pm->addSection($vid, $tid, 'Drainage',     'Culverts + kerbs',       $uidCrm);
$pm->addLine($s1, ['item_ref'=>'SW-01','description'=>'Site clearance','qty'=>10000,'uom'=>'m²','unit_cost'=>500,   'markup_pct'=>20], $uidCrm);
$pm->addLine($s1, ['item_ref'=>'SW-02','description'=>'Cut to spoil',  'qty'=>3000, 'uom'=>'m³','unit_cost'=>8000,  'markup_pct'=>20], $uidCrm);
$pm->addLine($s2, ['item_ref'=>'DR-01','description'=>'900mm concrete culvert','qty'=>200,'uom'=>'m','unit_cost'=>450000, 'markup_pct'=>18], $uidCrm);
$pm->addLine($s2, ['item_ref'=>'DR-02','description'=>'Kerbstones',          'qty'=>2000,'uom'=>'m','unit_cost'=>12000,  'markup_pct'=>22], $uidCrm);
$res = $pm->submitForApproval($vid, $uidCrm, $unmCrm, $ugCrm);
$aidP = (int)$res['approval_id'];
$ac->approve(['approval_id'=>$aidP,'user_id'=>$pmId, 'user_name'=>$pmNm, 'user_group'=>'Project Manager', 'comment'=>'ok']);
$ac->approve(['approval_id'=>$aidP,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,'comment'=>'ok']);
$ac->approve(['approval_id'=>$aidP,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,'comment'=>'ok']);
$pm->onPricingDecision($vid, 'APPROVED');
$pm->applyToTender($vid);
$tm->recordResult($tid, 'WON', date('Y-m-d'), '', 500000000, '', '', $uidCrm, $unmCrm);
$tm->advanceStage($tid, 'AWARDED', 'a', $uidCrm, $unmCrm);
$tm->saveContractTerms($tid, ['contract_value'=>$pm->findVersion($vid)['sum_price']], $uidFm, $ugFm, $unmFm);
$rC = $ac->submit(['chain_key'=>'TENDER_CONTRACT_APPROVAL','entity_type'=>'TENDER_CONTRACT','entity_id'=>$tid,'ref_label'=>'x','return_url'=>PROOT.'Tenders/view/'.$tid,'user_id'=>$uidCrm,'user_name'=>$unmCrm,'user_group'=>$ugCrm]);
$aidC = (int)$rC['approval_id'];
$ac->approve(['approval_id'=>$aidC,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,'comment'=>'','assignments'=>['ACCOUNTANT'=>['user_id'=>$accId,'user_name'=>$accNm],'PROCUREMENT'=>['user_id'=>$procId,'user_name'=>$procNm]]]);
$ac->approve(['approval_id'=>$aidC,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,'comment'=>'','assignments'=>['PROJECT_MANAGER'=>['user_id'=>$pmId,'user_name'=>$pmNm]]]);
$aC = $ac->approvalById($aidC);
$stg = $ac->decodePayload((string)($aC['step_payload_json'] ?? ''));
$tm->onContractApproved($tid, $uidExec, $unmExec, $stg['assignments'] ?? []);
$t = $tm->find($tid); $pid = (int)$t['linked_project_id'];
p("Project $pid provisioned");
$boq = $wb->findByProject($pid);
if (!$boq) boom('BOQ not snapshotted');
$boqId = (int)$boq['c0d'];
$items = $wb->itemsFor($boqId);
$tot = $wb->totals($boqId);
p("BOQ #$boqId · {$boq['title']} · contract_value=" . number_format((float)$boq['contract_value']) . " · items=" . count($items));
ok((int)$boq['project_id'] === $pid,  'BOQ linked to project');
ok(count($items) === 4,               '4 BOQ items snapshotted (matches 4 pricing lines)');
ok((int)$tot['contract_total'] === (int)round((float)$boq['contract_value']), 'Header value matches sum of item amounts');
ok($items[0]['origin'] === 'CONTRACT', 'Origin CONTRACT on all 4 items');

/* ── Step 2 ── Phase + BOQ subset + activities ─────────────────────── */
h('2. Create Phase 1 · attach BOQ items · add activities');
$phaseId = $wp->create($pid, [
    'code' => 'PH-1', 'title' => 'Phase 1 · Site prep + earthworks',
    'description' => 'Clear site, cut to spoil, prep for drainage',
    'planned_start' => date('Y-m-d'),
    'planned_end'   => date('Y-m-d', strtotime('+60 days')),
], $pcId);
p("Phase c0d=$phaseId");
$wp->attachBoqItem($phaseId, (int)$items[0]['c0d'], 5000, 'Half the clearance this phase'); // SW-01: 5000 of 10000 m²
$wp->attachBoqItem($phaseId, (int)$items[1]['c0d'], 3000, 'All the cut-to-spoil this phase'); // SW-02: 3000 of 3000 m³
$wp->addActivity($phaseId, ['code'=>'A1','title'=>'Access track clearance','unit'=>'m²','planned_qty'=>5000,'linked_boq_item_id'=>(int)$items[0]['c0d']]);
$wp->addActivity($phaseId, ['code'=>'A2','title'=>'Bulk cut + haul','unit'=>'m³','planned_qty'=>3000,'linked_boq_item_id'=>(int)$items[1]['c0d']]);
$phaseBoq = $wp->boqItemsFor($phaseId);
$phaseAct = $wp->activitiesFor($phaseId);
ok(count($phaseBoq) === 2 && count($phaseAct) === 2, '2 BOQ items attached · 2 activities added');

/* ── Step 3 ── Draft CR v1 with 7 pillars + activities ─────────────── */
h('3. Draft CR v1 · add 7 pillars + activities · verify rollup');
$crId = $wc->draft($pid, $phaseId, ['title'=>'Phase 1 execution package v1', 'notes'=>'Standard site prep budget'], $pcId, $pcNm);
p("CR c0d=$crId");
// 7 pillars (small realistic values)
$wc->addLine($crId, 'PRELIMINARIES', ['description'=>'Site office setup','amount'=>2500000]);
$wc->addLine($crId, 'PRELIMINARIES', ['description'=>'Perimeter fencing','amount'=>4500000]);
$wc->addLine($crId, 'MANPOWER', ['role'=>'Foreman','persons'=>1,'days'=>60,'rate'=>50000]);      // 3,000,000
$wc->addLine($crId, 'MANPOWER', ['role'=>'General labour','persons'=>10,'days'=>60,'rate'=>15000]); // 9,000,000
$wc->addLine($crId, 'MATERIALS', ['description'=>'Diesel (site vehicles)','qty'=>4000,'unit'=>'L','unit_cost'=>3000]); // 12,000,000
$wc->addLine($crId, 'EQUIPMENT', ['description'=>'Excavator','asset_id'=>0,'hours'=>200,'rate'=>120000,'in_house_flag'=>1]); // 24,000,000
$wc->addLine($crId, 'TOOLS', ['description'=>'PPE kits','qty'=>15,'unit'=>'kit','unit_cost'=>85000]); // 1,275,000
$wc->addLine($crId, 'SUBCONTRACTS', ['subcontractor_name'=>'ABC Earthworks','scope'=>'Bulk cut haulage','lump_sum'=>6000000]);
$wc->addLine($crId, 'OVERHEADS', ['category'=>'HSE','description'=>'HSE officer + inductions','amount'=>1500000]);
$wc->addLine($crId, 'OVERHEADS', ['category'=>'TESTING','description'=>'Soil tests','amount'=>800000]);
// Activities on the CR (usually mirror the phase activities but frozen)
$wc->addActivity($crId, ['title'=>'Access track clearance','unit'=>'m²','planned_qty'=>5000,'linked_boq_item_id'=>(int)$items[0]['c0d']]);
$wc->addActivity($crId, ['title'=>'Bulk cut + haul','unit'=>'m³','planned_qty'=>3000,'linked_boq_item_id'=>(int)$items[1]['c0d']]);

$cr = $wc->findById($crId);
$expPre = 2500000 + 4500000;                       // 7,000,000
$expMan = 3000000 + 9000000;                        // 12,000,000
$expMat = 4000 * 3000;                              // 12,000,000
$expEqp = 200 * 120000;                             // 24,000,000
$expTls = 15 * 85000;                               // 1,275,000
$expSub = 6000000;
$expOvh = 1500000 + 800000;                         // 2,300,000
$expGrand = $expPre + $expMan + $expMat + $expEqp + $expTls + $expSub + $expOvh;
p(sprintf("Rollup: prelims=%s mp=%s mat=%s eq=%s tls=%s sub=%s ovh=%s → grand=%s",
    number_format((float)$cr['total_preliminaries']), number_format((float)$cr['total_manpower']),
    number_format((float)$cr['total_materials']), number_format((float)$cr['total_equipment']),
    number_format((float)$cr['total_tools']), number_format((float)$cr['total_subcontracts']),
    number_format((float)$cr['total_overheads']), number_format((float)$cr['grand_total'])
));
ok(abs((float)$cr['total_preliminaries'] - $expPre) < 0.01, 'Preliminaries total');
ok(abs((float)$cr['total_manpower']      - $expMan) < 0.01, 'Manpower total');
ok(abs((float)$cr['total_materials']     - $expMat) < 0.01, 'Materials total');
ok(abs((float)$cr['total_equipment']     - $expEqp) < 0.01, 'Equipment total');
ok(abs((float)$cr['total_tools']         - $expTls) < 0.01, 'Tools total');
ok(abs((float)$cr['total_subcontracts']  - $expSub) < 0.01, 'Subcontracts total');
ok(abs((float)$cr['total_overheads']     - $expOvh) < 0.01, 'Overheads total');
ok(abs((float)$cr['grand_total']         - $expGrand) < 0.01, 'Grand total = sum of pillars');

/* ── Step 4 ── Submit CR chain ─────────────────────────────────────── */
h('4. Submit CR chain');
$res = $wc->submitForApproval($crId, $pcId, $pcNm, 'Projects Coordinator');
if (!$res['ok']) boom('Submit failed: ' . $res['msg']);
$aidCR = (int)$res['approval_id'];
p("CR chain approval c0d=$aidCR");
$after = $wc->findById($crId);
ok($after['status'] === 'SUBMITTED' && (int)$after['approval_id'] === $aidCR, 'CR → SUBMITTED');

/* ── Step 5 ── Walk 4-step chain PC → PM → FM → Exec ──────────────── */
h('5. Walk 4-step chain');
// Step 1 already visible to PC (originator); ApprovalChain::submit lands at step 1 (PC_REVIEW). PC approves first.
$ac->approve(['approval_id'=>$aidCR,'user_id'=>$pcId, 'user_name'=>$pcNm, 'user_group'=>'Projects Coordinator','comment'=>'OK']);
$ac->approve(['approval_id'=>$aidCR,'user_id'=>$pmId, 'user_name'=>$pmNm, 'user_group'=>'Project Manager',     'comment'=>'OK']);
$ac->approve(['approval_id'=>$aidCR,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,               'comment'=>'OK']);
$ac->approve(['approval_id'=>$aidCR,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,         'comment'=>'OK']);
// Simulate the postDecisionHook (CLI bypasses controller layer)
$aRow = $ac->approvalById($aidCR);
$wc->onChainDecision($crId, (string)$aRow['status']);
$final = $wc->findById($crId);
p("Post-terminal: CR status={$final['status']} · activated_on={$final['activated_on']}");
ok($final['status'] === 'ACTIVE', 'CR → ACTIVE (via postDecisionHook)');
$phaseAfter = $wp->findById($phaseId);
ok($phaseAfter['status'] === 'ACTIVE', 'Phase → ACTIVE');

/* ── Step 6 ── Try to draft another CR while ACTIVE exists — allowed as revision ── */
h('6. Draft CR v2 (revision of ACTIVE v1) → allowed');
$cr2Id = $wc->draft($pid, $phaseId, ['title'=>'Phase 1 execution package v2 · scope revised'], $pcId, $pcNm);
p("CR v2 c0d=$cr2Id");
$cr2 = $wc->findById($cr2Id);
ok((int)$cr2['version'] === 2 && (int)$cr2['supersedes_id'] === $crId, 'v2 stamped version=2, supersedes v1');

/* ── Step 7 ── Approve v2 → v2 ACTIVE, v1 SUPERSEDED ─────────────── */
h('7. Populate + approve v2 → supersedes v1');
$wc->addLine($cr2Id, 'PRELIMINARIES', ['description'=>'Extra fencing','amount'=>1000000]);
$wc->addLine($cr2Id, 'MANPOWER', ['role'=>'Foreman','persons'=>1,'days'=>30,'rate'=>50000]);
$wc->addActivity($cr2Id, ['title'=>'Revised access track scope','unit'=>'m²','planned_qty'=>3000]);
$res2 = $wc->submitForApproval($cr2Id, $pcId, $pcNm, 'Projects Coordinator');
$aidCR2 = (int)$res2['approval_id'];
$ac->approve(['approval_id'=>$aidCR2,'user_id'=>$pcId, 'user_name'=>$pcNm, 'user_group'=>'Projects Coordinator','comment'=>'ok']);
$ac->approve(['approval_id'=>$aidCR2,'user_id'=>$pmId, 'user_name'=>$pmNm, 'user_group'=>'Project Manager',     'comment'=>'ok']);
$ac->approve(['approval_id'=>$aidCR2,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,               'comment'=>'ok']);
$ac->approve(['approval_id'=>$aidCR2,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,         'comment'=>'ok']);
$aRow2 = $ac->approvalById($aidCR2);
$wc->onChainDecision($cr2Id, (string)$aRow2['status']);
$v1After = $wc->findById($crId);
$v2After = $wc->findById($cr2Id);
p("v1 status={$v1After['status']} · v2 status={$v2After['status']}");
ok($v1After['status'] === 'SUPERSEDED' && !empty($v1After['superseded_on']), 'v1 auto-SUPERSEDED');
ok($v2After['status'] === 'ACTIVE',                                          'v2 now ACTIVE');
$activeAfter = $wc->activeForPhase($phaseId);
ok($activeAfter && (int)$activeAfter['c0d'] === $cr2Id, 'activeForPhase() returns v2');

/* ── Step 8 ── Variation Order flow ────────────────────────────────── */
h('8. Draft Variation → 2 items → chain → approved → appended to BOQ');
$boqBefore = $wb->findByProject($pid);
$valBefore = (float)$boqBefore['contract_value'];
$itemsBefore = count($wb->itemsFor((int)$boqBefore['c0d']));
p("BOQ before: value=" . number_format($valBefore) . " · items=$itemsBefore");
$voId = $wv->draft($pid, [
    'title' => 'Additional gabion baskets @ chainage 2+200',
    'description' => 'Engineer instructed 40m gabion wall to protect embankment.',
    'engineer_ref' => 'INST-TRR-2026-014',
    'engineer_instruction_date' => date('Y-m-d'),
], $pcId, $pcNm);
$wv->addItem($voId, ['item_no'=>'VO-01','section'=>'Drainage / protection','description'=>'Gabion baskets 1m³','unit'=>'m³','qty'=>40,'rate'=>350000]);   // 14,000,000
$wv->addItem($voId, ['item_no'=>'VO-02','section'=>'Drainage / protection','description'=>'Geotextile fabric','unit'=>'m²','qty'=>200,'rate'=>4500]); // 900,000
$voRow = $wv->findById($voId);
p("VO {$voRow['vo_no']} · total_value=" . number_format((float)$voRow['total_value']) . " · status={$voRow['status']}");
ok($voRow['status'] === 'PRICED' && abs((float)$voRow['total_value'] - 14900000) < 0.01, 'VO auto-flipped to PRICED · total 14,900,000');

$resVO = $wv->submitForApproval($voId, $pcId, $pcNm, 'Projects Coordinator');
if (!$resVO['ok']) boom('VO submit failed: ' . $resVO['msg']);
$aidVO = (int)$resVO['approval_id'];
$ac->approve(['approval_id'=>$aidVO,'user_id'=>$pcId, 'user_name'=>$pcNm, 'user_group'=>'Projects Coordinator','comment'=>'ok']);
$ac->approve(['approval_id'=>$aidVO,'user_id'=>$pmId, 'user_name'=>$pmNm, 'user_group'=>'Project Manager',     'comment'=>'ok']);
$ac->approve(['approval_id'=>$aidVO,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,               'comment'=>'ok']);
$ac->approve(['approval_id'=>$aidVO,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,         'comment'=>'ok']);
$aVo = $ac->approvalById($aidVO);
$wv->onChainDecision($voId, (string)$aVo['status']);

$voAfter = $wv->findById($voId);
$boqAfter = $wb->findByProject($pid);
$itemsAfter = $wb->itemsFor((int)$boqAfter['c0d']);
$variationItems = array_filter($itemsAfter, fn($i) => $i['origin'] === 'VARIATION' && (int)$i['source_variation_id'] === $voId);
p("VO after: status={$voAfter['status']} · applied_on={$voAfter['applied_on']}");
p("BOQ after: value=" . number_format((float)$boqAfter['contract_value']) . " · items=" . count($itemsAfter) . " · variation items=" . count($variationItems));
ok($voAfter['status'] === 'APPROVED' && !empty($voAfter['applied_on']), 'VO APPROVED + applied_on stamped');
ok(count($variationItems) === 2, '2 VARIATION rows appended to works_boq_items');
ok(abs((float)$boqAfter['contract_value'] - ($valBefore + 14900000)) < 0.01, 'BOQ contract_value bumped by VO total');
ok((float)$boqAfter['variation_value'] >= 14900000, 'BOQ variation_value tracked');
foreach ($variationItems as $vi) ok((int)$vi['source_variation_id'] === $voId, "  item {$vi['description']} · source_variation_id → VO $voId");

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