<?php
/**
 * Slice 8g-GoodsCrm · CRM-authored Client PO + 4-stage GOODS_PROJECT_ONBOARDING chain.
 *
 *  1) Seed a fresh goods project (crm_status = PENDING, migrated_flag = 0, has customer_id).
 *  2) getOrCreateDraftClientPo creates the DRAFT Client PO.
 *  3) addItem × 3 with brand + source_pref + delivery_method + margin.
 *  4) recomputeTotals is idempotent + correct.
 *  5) Guards: legacy project refuses; goods project with no items refuses to submit.
 *  6) submitOnboarding kicks the chain; crm_status flips to SUBMITTED.
 *  7) Chain walk:
 *      - CRM step: originator implicit approve
 *      - FM step: refuses without ACCOUNTANT + PROCUREMENT slot assignments; passes with them
 *      - EXEC step: refuses without PROJECT_MANAGER assignment; passes with it
 *      - PM terminal step: refuses without PROJECTS_COORDINATOR assignment; passes with it
 *  8) On terminal APPROVED: onChainDecision stamps 4 assignments + flips crm_status + confirms PO.
 *  9) DECLINED branch (fresh project) flips crm_status to DECLINED.
 * 10) Wiring surface: dispatch case + perms + view + AJAX endpoints.
 */
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','GoodsOnboarding.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();
$ac = new ApprovalChain();
$go = new GoodsOnboarding();

$crmId=101; $crmNm='Verify CRM';  $crmG='CRM';
$fmId =3;   $fmNm ='Verify FM';   $fmG ='Finance Manager';
$exId =2;   $exNm ='Verify Exec'; $exG ='Executive';
$pmId =5;   $pmNm ='Verify PM';   $pmG ='Project Manager';
$pcId =100; $pcNm ='Verify PC';   $pcG ='Projects Coordinator';
$accId=4;   $accNm='Verify Acct';
$prcId=8;   $prcNm='Verify Proc';

/* ── 0. Purge prior 8g artefacts ───────────────────────────────────── */
h('0. Purge prior 8g projects');
$oldPrj = $db->mySelect("SELECT c0d FROM c_projects WHERE project_no LIKE '8G-VERIFY-%'", []) ?: [];
foreach ($oldPrj as $op) {
    $pid = (int)$op['c0d'];
    // Chain approvals
    $aps = $db->mySelect("SELECT c0d FROM fin_approvals WHERE entity_type = 'goods_project_onboarding' AND entity_id = :p", [':p' => $pid]) ?: [];
    foreach ($aps as $a) {
        $db->insertUpdate("DELETE FROM fin_approval_log WHERE approval_id = :a", [':a' => (int)$a['c0d']]);
        $db->insertUpdate("DELETE FROM fin_approvals    WHERE c0d          = :a", [':a' => (int)$a['c0d']]);
    }
    // Client PO + items
    $pos = $db->mySelect("SELECT c0d FROM goods_client_pos WHERE project_id = :p", [':p' => $pid]) ?: [];
    foreach ($pos as $po) {
        $db->insertUpdate("DELETE FROM goods_client_po_items WHERE client_po_id = :c", [':c' => (int)$po['c0d']]);
        $db->insertUpdate("DELETE FROM goods_client_pos      WHERE c0d          = :c", [':c' => (int)$po['c0d']]);
    }
    $db->insertUpdate("DELETE FROM c_projects WHERE c0d = :p", [':p' => $pid]);
}
p("purged " . count($oldPrj) . " prior 8g test projects");

/* ── 1. Seed a fresh goods project ─────────────────────────────────── */
h('1. Seed fresh goods project');
$stamp = date('YmdHis');
$fb = $db->insertUpdate(
    "INSERT INTO c_projects
        (project_no, project_name, project_type, project_customer, project_customer_id,
         crm_status, migrated_flag, origin, currency, contract_value, default_margin_pct,
         created_by, created_on, edited_by, edited_on)
     VALUES (:no, :nm, 'goods', 'TTCL Ltd', 1,
             'PENDING', 0, 'DIRECT_AWARD', 'TZS', 50000000, 25.00,
             0, NOW(), 0, NOW())",
    [':no' => '8G-VERIFY-' . $stamp, ':nm' => '8G verify medical consumables ' . $stamp]
);
$prjId = (int)($fb['last_id'] ?? 0);
if ($prjId <= 0) boom('project seed failed');
p("Project #$prjId · 8G-VERIFY-$stamp");

/* ── 2. getOrCreateDraftClientPo ────────────────────────────────────── */
h('2. getOrCreateDraftClientPo');
$poR = $go->getOrCreateDraftClientPo($prjId, $crmId, $crmNm);
ok($poR['ok'],                              'ok');
ok(!empty($poR['created']),                 'created a new PO');
$poId = (int)$poR['po_id'];
ok($poId > 0,                                "po_id = $poId");
$poR2 = $go->getOrCreateDraftClientPo($prjId, $crmId, $crmNm);
ok($poR2['ok'] && (int)$poR2['po_id'] === $poId && empty($poR2['created']), 'second call is idempotent (returns same PO)');

/* ── 3. addItem × 3 ────────────────────────────────────────────────── */
h('3. addItem with brand + source + delivery');
$i1 = $go->addItem($poId, ['description'=>'Surgical gloves size M', 'brand'=>'Ansell', 'source_pref'=>'IMPORT',
    'delivery_method'=>'Air', 'qty'=>1000, 'unit'=>'box', 'unit_price_sell'=>15000, 'margin_pct'=>30], $crmId);
ok($i1['ok'], 'item 1 added');
$i2 = $go->addItem($poId, ['description'=>'Face masks 3-ply', 'brand'=>'Generic', 'source_pref'=>'LOCAL',
    'delivery_method'=>'Courier', 'qty'=>500, 'unit'=>'box', 'unit_price_sell'=>8000, 'margin_pct'=>25], $crmId);
ok($i2['ok'], 'item 2 added');
$i3 = $go->addItem($poId, ['description'=>'Cotton swabs', 'brand'=>'Generic', 'source_pref'=>'ANY',
    'delivery_method'=>'Courier', 'qty'=>2000, 'unit'=>'pack', 'unit_price_sell'=>1200, 'margin_pct'=>25], $crmId);
ok($i3['ok'], 'item 3 added');

$items = $go->itemsFor($poId);
ok(count($items) === 3, '3 items on the PO');
$expSubtotal = 1000*15000 + 500*8000 + 2000*1200;
$po = $db->mySelect("SELECT subtotal_sell, vat_amount, total FROM goods_client_pos WHERE c0d = :id", [':id' => $poId]);
ok(abs((float)$po[0]['subtotal_sell'] - $expSubtotal) < 0.01, "subtotal_sell = " . number_format($expSubtotal));
ok(abs((float)$po[0]['vat_amount']   - round($expSubtotal * 0.18, 2)) < 0.01, 'vat_amount = subtotal * 18%');
ok(abs((float)$po[0]['total']        - round($expSubtotal * 1.18, 2)) < 0.01, 'total     = subtotal * 1.18');

/* ── 4. Legacy project refuses ─────────────────────────────────────── */
h('4. Guard · legacy project refuses new chain path');
$legFb = $db->insertUpdate(
    "INSERT INTO c_projects (project_no, project_name, project_type, project_customer, project_customer_id,
        crm_status, migrated_flag, origin, currency, contract_value, default_margin_pct)
     VALUES (:no, :nm, 'goods', 'TTCL Ltd', 1, 'APPROVED', 1, 'MIGRATED', 'TZS', 10000000, 25.00)",
    [':no' => '8G-VERIFY-LEGACY-' . $stamp, ':nm' => 'legacy']
);
$legId = (int)($legFb['last_id'] ?? 0);
$legR = $go->getOrCreateDraftClientPo($legId, $crmId, $crmNm);
ok(!$legR['ok'] && stripos($legR['msg'], 'Legacy') !== false, 'legacy project blocks new-path Client PO');
$db->insertUpdate("DELETE FROM c_projects WHERE c0d = :id", [':id' => $legId]);

/* ── 5. Submit chain ───────────────────────────────────────────────── */
h('5. submitOnboarding kicks the chain');
$sub = $go->submitOnboarding($prjId, $crmId, $crmNm, $crmG);
ok($sub['ok'], 'submitted: ' . ($sub['msg'] ?? ''));
$aid = (int)$sub['approval_id'];
$crmStatus = $db->mySelect("SELECT crm_status FROM c_projects WHERE c0d = :p", [':p' => $prjId]);
ok((string)$crmStatus[0]['crm_status'] === 'SUBMITTED', 'crm_status = SUBMITTED');

/* ── 6. Walk chain step 1 → step 2 → step 3 → terminal ────────────── */
h('6. Walk chain: CRM_CAPTURE originator');
$ac->approve(['approval_id'=>$aid,'user_id'=>$crmId,'user_name'=>$crmNm,'user_group'=>$crmG,'comment'=>'CRM ok']);
$aRow = $ac->approvalById($aid);
ok((string)$aRow['status']       === 'IN_PROGRESS', 'still IN_PROGRESS');
ok((string)$aRow['current_step_role'] === 'FINANCE MANAGER', 'now on FM step');

h('7. FM_REVIEW refuses without assignments');
$fmBad = $ac->approve(['approval_id'=>$aid,'user_id'=>$fmId,'user_name'=>$fmNm,'user_group'=>$fmG,'comment'=>'FM ok']);
ok(!$fmBad['ok'] && stripos($fmBad['msg'], 'Assign') !== false, 'FM step blocked without ACCOUNTANT + PROCUREMENT');

h('8. FM_REVIEW passes with staged assignments');
$fmOk = $ac->approve(['approval_id'=>$aid,'user_id'=>$fmId,'user_name'=>$fmNm,'user_group'=>$fmG,'comment'=>'FM ok',
    'assignments' => [
        'ACCOUNTANT'  => ['user_id'=>$accId, 'user_name'=>$accNm],
        'PROCUREMENT' => ['user_id'=>$prcId, 'user_name'=>$prcNm],
    ]]);
ok($fmOk['ok'], 'FM passed with assignments');
$aRow = $ac->approvalById($aid);
ok((string)$aRow['current_step_role'] === 'EXECUTIVE', 'now on EXEC step');

h('9. EXEC refuses without PM assignment');
$exBad = $ac->approve(['approval_id'=>$aid,'user_id'=>$exId,'user_name'=>$exNm,'user_group'=>$exG,'comment'=>'exec ok']);
ok(!$exBad['ok'] && stripos($exBad['msg'], 'PROJECT_MANAGER') !== false, 'EXEC blocked without PM');

h('10. EXEC passes with PM assignment');
$exOk = $ac->approve(['approval_id'=>$aid,'user_id'=>$exId,'user_name'=>$exNm,'user_group'=>$exG,'comment'=>'exec ok',
    'assignments' => ['PROJECT_MANAGER' => ['user_id'=>$pmId, 'user_name'=>$pmNm]]]);
ok($exOk['ok'], 'EXEC passed with PM');
$aRow = $ac->approvalById($aid);
ok((string)$aRow['current_step_role'] === 'PROJECT MANAGER', 'now on PM terminal step');

h('11. PM terminal refuses without PC assignment');
$pmBad = $ac->approve(['approval_id'=>$aid,'user_id'=>$pmId,'user_name'=>$pmNm,'user_group'=>$pmG,'comment'=>'PM ok']);
ok(!$pmBad['ok'] && stripos($pmBad['msg'], 'PROJECTS_COORDINATOR') !== false, 'PM blocked without PC');

h('12. PM terminal passes with PC → chain APPROVED');
$pmOk = $ac->approve(['approval_id'=>$aid,'user_id'=>$pmId,'user_name'=>$pmNm,'user_group'=>$pmG,'comment'=>'PM ok',
    'assignments' => ['PROJECTS_COORDINATOR' => ['user_id'=>$pcId, 'user_name'=>$pcNm]]]);
ok($pmOk['ok'], 'PM passed with PC');
$aRow = $ac->approvalById($aid);
ok((string)$aRow['status'] === 'APPROVED', 'chain terminal APPROVED');

// Simulate FinChainsController::postDecisionHook for CLI verify
$stagedPayload = $ac->decodePayload((string)($aRow['step_payload_json'] ?? ''));
$hookRes = $go->onChainDecision($prjId, (string)$aRow['status'], $stagedPayload);
ok($hookRes['ok'], 'onChainDecision hook returned ok');

/* ── 13. Assert final state ───────────────────────────────────────── */
h('13. Final project state');
$prj = $db->mySelect("SELECT crm_status, accountant_id, procurement_id, project_manager_id, pc_id FROM c_projects WHERE c0d = :p", [':p' => $prjId]);
p(sprintf("crm=%s · acct=%s proc=%s pm=%s pc=%s",
    $prj[0]['crm_status'],
    $prj[0]['accountant_id'], $prj[0]['procurement_id'], $prj[0]['project_manager_id'], $prj[0]['pc_id']));
ok((string)$prj[0]['crm_status']        === 'APPROVED', 'crm_status → APPROVED');
ok((int)$prj[0]['accountant_id']        === $accId,     'accountant_id stamped');
ok((int)$prj[0]['procurement_id']       === $prcId,     'procurement_id stamped');
ok((int)$prj[0]['project_manager_id']   === $pmId,      'project_manager_id stamped');
ok((int)$prj[0]['pc_id']                === $pcId,      'pc_id stamped');
$poFinal = $db->mySelect("SELECT status FROM goods_client_pos WHERE c0d = :id", [':id' => $poId]);
ok((string)$poFinal[0]['status'] === 'CONFIRMED', 'Client PO lifted DRAFT → CONFIRMED');

/* ── 14. DECLINED branch on a second fresh project ───────────────── */
h('14. Second project · DECLINED terminal branch');
$fb2 = $db->insertUpdate(
    "INSERT INTO c_projects (project_no, project_name, project_type, project_customer, project_customer_id,
        crm_status, migrated_flag, origin, currency, contract_value, default_margin_pct)
     VALUES (:no, :nm, 'goods', 'TTCL Ltd', 1, 'PENDING', 0, 'DIRECT_AWARD', 'TZS', 5000000, 25.00)",
    [':no' => '8G-VERIFY-D-' . $stamp, ':nm' => 'declined']
);
$prjId2 = (int)$fb2['last_id'];
$po2 = $go->getOrCreateDraftClientPo($prjId2, $crmId, $crmNm);
$go->addItem((int)$po2['po_id'], ['description'=>'Test item','qty'=>1,'unit'=>'ea','unit_price_sell'=>1000], $crmId);
$sub2 = $go->submitOnboarding($prjId2, $crmId, $crmNm, $crmG);
$aid2 = (int)$sub2['approval_id'];
$ac->approve(['approval_id'=>$aid2,'user_id'=>$crmId,'user_name'=>$crmNm,'user_group'=>$crmG,'comment'=>'ok']);
$dec = $ac->decline(['approval_id'=>$aid2,'user_id'=>$fmId,'user_name'=>$fmNm,'user_group'=>$fmG,'comment'=>'no cash']);
ok($dec['ok'], 'FM declined');
$aRow2 = $ac->approvalById($aid2);
$stagedPayload2 = $ac->decodePayload((string)($aRow2['step_payload_json'] ?? ''));
$go->onChainDecision($prjId2, (string)$aRow2['status'], $stagedPayload2);
$prj2 = $db->mySelect("SELECT crm_status FROM c_projects WHERE c0d = :p", [':p' => $prjId2]);
ok((string)$prj2[0]['crm_status'] === 'DECLINED', 'crm_status → DECLINED on chain decline');

/* ── 15. Wiring surface ──────────────────────────────────────────── */
h('15. Wiring surface');
$fcBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'FinChainsController.php');
ok(strpos($fcBody, "'goods_project_onboarding'") !== false, 'FinChainsController dispatches goods_project_onboarding');
ok(strpos($fcBody, 'GoodsOnboarding')             !== false, 'FinChainsController calls GoodsOnboarding');
$pcBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'ProjectsController.php');
ok(strpos($pcBody, 'goodsAddItemAjAction')       !== false, 'ProjectsController::goodsAddItemAjAction exists');
ok(strpos($pcBody, 'goodsRemoveItemAjAction')    !== false, 'ProjectsController::goodsRemoveItemAjAction exists');
ok(strpos($pcBody, 'goodsSubmitOnboardingAjAction') !== false, 'ProjectsController::goodsSubmitOnboardingAjAction exists');
$vBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Projects' . DS . 'view.php');
ok(strpos($vBody, 'Items to Supply · CRM authoring') !== false, 'Projects/view has CRM authoring panel');
ok(strpos($vBody, 'goodsSubmitOnboardingAj')          !== false, 'Projects/view wires submit button');
ok(strpos($vBody, 'source_pref')                      !== false, 'Projects/view has source_pref');
// Perms
foreach (['CRM','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 (['goodsAddItemAj','goodsRemoveItemAj','goodsSubmitOnboardingAj'] as $act) {
        $r = $db->mySelect(
            "SELECT 1 FROM user_groups_perms WHERE user_group_id = :g AND controller_allowed = 'Projects' AND action_allowed = :a LIMIT 1",
            [':g' => $gid, ':a' => $act]
        );
        ok(!empty($r), "$role · Projects/$act");
    }
}
// Chain seed
$chain = $db->mySelect("SELECT c0d, status FROM fin_chain_defs WHERE chain_key = 'GOODS_PROJECT_ONBOARDING'", []);
ok(!empty($chain) && (string)$chain[0]['status'] === 'ACTIVE', 'GOODS_PROJECT_ONBOARDING chain ACTIVE');

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