<?php
/**
 * Slice 8d-LpoChain · WORKS_LPO_APPROVAL end-to-end.
 *
 *  1) Draft a Works-sourced LPO via WorksCrFulfilment::draftLpoFromCrLines.
 *  2) Verify approval_chain_key = 'WORKS_LPO_APPROVAL' (not GOODS_LPO_APPROVAL).
 *  3) prepFromLpo refuses (LPO is DRAFT, not APPROVED).
 *  4) Submit via existing Lpos submit path → chain begins.
 *  5) Walk chain: PC → PM → FM → EXEC. On terminal APPROVED, cash_out_status flips to APPROVED.
 *  6) Duplicate submit refused (already in chain).
 *  7) prepFromLpo now succeeds.
 *  8) Full payment loop still works end-to-end (PV → schedule → recordPayment → allocation).
 *  9) Second scenario: DECLINE terminal → cash_out_status flips to DECLINED, PV blocked.
 * 10) Chain seed integrity + surface wiring.
 */
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','WorksCrs.php','WorksPhases.php','WorksCrFulfilment.php',
    'WorksBoq.php','WorksRetention.php','Lpos.php','PaymentVouchers.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();
$wc  = new WorksCrs();
$wp  = new WorksPhases();
$wf  = new WorksCrFulfilment();
$wr  = new WorksRetention();
$lm  = new Lpos();
$pvs = new PaymentVouchers();

$pcId=100; $pcNm='Verify PC'; $pcG='Projects Coordinator';
$pmId=5;   $pmNm='Verify PM'; $pmG='Project Manager';
$fmId=3;   $fmNm='Verify FM'; $fmG='Finance Manager';
$exId=2;   $exNm='Verify Exec'; $exG='Executive';

/* ── 0. Anchor + prime ─────────────────────────────────────────────── */
h('0. Anchor works project + prime state');
$anchor = $db->mySelect(
    "SELECT p.c0d, p.project_no, p.project_customer_id, p.crm_status
       FROM c_projects p JOIN works_boqs b ON b.project_id = p.c0d AND b.deleted = 0
      WHERE p.project_type='works' AND p.deleted=0 ORDER BY p.c0d ASC LIMIT 1",
    []
);
if (empty($anchor)) boom('No works project with a BOQ.');
$prjId       = (int)$anchor[0]['c0d'];
$prevCustId  = (int)$anchor[0]['project_customer_id'];
$prevCrm     = (string)$anchor[0]['crm_status'];
$prevTerms   = $wr->termsFor($prjId) ?: ['pct' => 0.0, 'dlp_days' => 365];
$db->insertUpdate("UPDATE c_projects SET project_customer_id = 1, crm_status = 'APPROVED' WHERE c0d = :p", [':p' => $prjId]);
$wr->setTerms($prjId, 0.00, 365); // disable retention to keep math simple
p("Project #$prjId (retention temporarily disabled)");

/* ── 0b. Purge prior 8d-verify artefacts ───────────────────────────── */
h('0b. Purge prior 8d-verify artefacts');
$oldPhases = $db->mySelect("SELECT c0d FROM works_phases WHERE project_id = :p AND title LIKE 'LpoChainVerify %'", [':p' => $prjId]) ?: [];
foreach ($oldPhases as $op) {
    $phId = (int)$op['c0d'];
    $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, approval_id FROM lpos WHERE source_cr_id = :c", [':c' => $cid]) ?: [];
        foreach ($lpos as $lp) {
            $lid = (int)$lp['c0d']; $aid = (int)$lp['approval_id'];
            $pays = $db->mySelect("SELECT c0d FROM payments WHERE EXISTS (SELECT 1 FROM payment_allocations a WHERE a.payment_id=payments.c0d AND a.lpo_id=:l)", [':l' => $lid]) ?: [];
            foreach ($pays as $pay) {
                $db->insertUpdate("DELETE FROM works_retention_holdings WHERE payment_id = :p", [':p' => (int)$pay['c0d']]);
                $db->insertUpdate("DELETE FROM payment_allocations WHERE payment_id = :p", [':p' => (int)$pay['c0d']]);
                $db->insertUpdate("DELETE FROM journal_lines WHERE journal_entry_id IN (SELECT c0d FROM journal_entries WHERE source_type='PAYMENT' AND source_id=:p)", [':p' => (int)$pay['c0d']]);
                $db->insertUpdate("DELETE FROM journal_entries WHERE source_type='PAYMENT' AND source_id = :p", [':p' => (int)$pay['c0d']]);
                $db->insertUpdate("DELETE FROM payments WHERE c0d = :p", [':p' => (int)$pay['c0d']]);
            }
            $db->insertUpdate("DELETE FROM payment_vouchers 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]);
            // Also nuke the approval trail
            if ($aid > 0) {
                $db->insertUpdate("DELETE FROM fin_approval_log WHERE approval_id = :a", [':a' => $aid]);
                $db->insertUpdate("DELETE FROM fin_approvals     WHERE c0d          = :a", [':a' => $aid]);
            }
        }
        $db->insertUpdate("DELETE FROM works_phase_items_issued 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 8d phases");

/* ── 1. Fresh phase + CR + Works LPO ───────────────────────────────── */
h('1. Draft a Works-sourced LPO');
$phId = $wp->create($prjId, [
    'code' => 'LC-' . date('His'),
    'title' => 'LpoChainVerify ' . date('YmdHis'),
    'planned_start' => date('Y-m-d'),
    'planned_end'   => date('Y-m-d', strtotime('+30 days')),
], $pcId);
$crId = $wc->draft($prjId, $phId, ['title' => 'LpoChainVerify CR ' . date('His')], $pcId, $pcNm);
$wc->addLine($crId, 'MATERIALS', ['description' => 'LC Diesel', 'qty' => 4000, 'unit' => 'L', 'unit_cost' => 2500]);
$wc->addActivity($crId, ['title' => 'Access', 'unit' => 'm²', 'planned_qty' => 5000]);
$db->insertUpdate("UPDATE works_crs SET status='APPROVED' WHERE c0d=:id", [':id'=>$crId]);
$wc->activate($crId);
$lpoRes = $wf->draftLpoFromCrLines(
    $crId, 'MATERIALS',
    ['vendor_id' => 1, 'vendor_name' => 'Furniture Direct Ltd', 'currency' => 'TZS'],
    [ ['line_id' => (int)$wf->sourceableLines($crId)['MATERIALS'][0]['line_id'], 'qty' => 3600, 'unit_price' => 2500] ], // 9M (leave 1M for LPO2)
    $pcId, $pcNm
);
if (!$lpoRes['ok']) boom('LPO draft failed: ' . ($lpoRes['msg'] ?? '?'));
$lpoId = (int)$lpoRes['id'];
p("LPO {$lpoRes['lpo_no']} c0d=$lpoId");

/* ── 2. approval_chain_key = WORKS_LPO_APPROVAL ────────────────────── */
h('2. approval_chain_key stamped correctly');
$row = $db->mySelect("SELECT cash_out_status, approval_chain_key FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
ok((string)$row[0]['cash_out_status']      === 'DRAFT',                'LPO status DRAFT');
ok((string)$row[0]['approval_chain_key']   === 'WORKS_LPO_APPROVAL',   'approval_chain_key = WORKS_LPO_APPROVAL');

/* ── 3. prepFromLpo refuses on DRAFT ───────────────────────────────── */
h('3. Guard · prepFromLpo refuses DRAFT LPO');
$prep0 = $pvs->prepFromLpo($lpoId, $fmId, $fmNm, 'try prep');
ok(!$prep0['ok'] && stripos($prep0['msg'], 'not APPROVED') !== false, 'PV prep blocked on DRAFT');

/* ── 4. Submit via existing Lpos submit path ───────────────────────── */
h('4. Submit via LposController-style path (ApprovalChain::submit)');
$submitRes = $ac->submit([
    'chain_key'   => 'WORKS_LPO_APPROVAL',
    'entity_type' => 'lpo',
    'entity_id'   => $lpoId,
    'ref_label'   => $lpoRes['lpo_no'] . ' · Furniture Direct Ltd · TZS 10,000,000',
    'return_url'  => 'Lpos/view/' . $lpoId,
    'user_id'     => $pcId, 'user_name' => $pcNm, 'user_group' => $pcG,
    'comment'     => 'Submitted by verify',
]);
if (!$submitRes['ok']) boom('submit failed: ' . ($submitRes['msg'] ?? '?'));
$aid = (int)$submitRes['approval_id'];
$lm->markSubmitted($lpoId, $aid);
$rowAft = $db->mySelect("SELECT cash_out_status, approval_id FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
ok((string)$rowAft[0]['cash_out_status'] === 'SUBMITTED',              'LPO flipped to SUBMITTED');
ok((int)$rowAft[0]['approval_id']         === $aid,                    'LPO.approval_id = new approval');

/* ── 5. Duplicate submit refused ─────────────────────────────────── */
h('5. Duplicate submit refused');
$dup = $ac->submit([
    'chain_key'   => 'WORKS_LPO_APPROVAL',
    'entity_type' => 'lpo',
    'entity_id'   => $lpoId,
    'ref_label'   => 'dup',
    'return_url'  => 'x',
    'user_id'     => $pcId, 'user_name' => $pcNm, 'user_group' => $pcG,
]);
ok(!$dup['ok'] && stripos($dup['msg'], 'Already submitted') !== false, 'dup submit refused');

/* ── 6. Walk chain PC → PM → FM → EXEC ─────────────────────────── */
h('6. Walk chain PC → PM → FM → EXEC');
$ac->approve(['approval_id'=>$aid,'user_id'=>$pcId,'user_name'=>$pcNm,'user_group'=>$pcG,'comment'=>'PC ok']);
$ac->approve(['approval_id'=>$aid,'user_id'=>$pmId,'user_name'=>$pmNm,'user_group'=>$pmG,'comment'=>'PM ok']);
$ac->approve(['approval_id'=>$aid,'user_id'=>$fmId,'user_name'=>$fmNm,'user_group'=>$fmG,'comment'=>'FM ok']);
$ac->approve(['approval_id'=>$aid,'user_id'=>$exId,'user_name'=>$exNm,'user_group'=>$exG,'comment'=>'Exec ok']);
// The postDecisionHook lives in FinChainsController; simulate for CLI verify.
$aRow = $ac->approvalById($aid);
$lm->markDecision($lpoId, (string)$aRow['status']);
$rowFin = $db->mySelect("SELECT cash_out_status FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
ok((string)$aRow['status']              === 'APPROVED',                'chain terminal APPROVED');
ok((string)$rowFin[0]['cash_out_status'] === 'APPROVED',               'LPO flipped to APPROVED via markDecision');

/* ── 7. prepFromLpo now succeeds ─────────────────────────────────── */
h('7. Payables can now prep a PV');
$prep = $pvs->prepFromLpo($lpoId, $fmId, $fmNm, 'post-approval');
ok($prep['ok'] && (int)$prep['id'] > 0, 'PV prep succeeds: ' . $prep['msg']);
$pvId = (int)$prep['id'];

/* ── 8. Full payment loop still works ────────────────────────────── */
h('8. Full payment loop: schedule → recordPayment → allocation');
$sch = $pvs->schedule($pvId, date('Y-m-d', strtotime('+1 day')), 1, $fmId, $fmNm, '');
ok($sch['ok'], 'PV scheduled');
$pay = $pvs->recordPayment($pvId, [
    'payment_method' => 'BANK_TRANSFER', 'reference_no' => 'LC-VERIFY',
    'payment_date' => date('Y-m-d'), 'bank_account_id' => 1,
], $fmId);
ok($pay['ok'], 'payment recorded');
$paidRow = $db->mySelect("SELECT cash_out_status FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
ok((string)$paidRow[0]['cash_out_status'] === 'PAID', 'LPO flipped to PAID');
$alloc = $db->mySelect("SELECT amount FROM payment_allocations WHERE lpo_id = :l", [':l' => $lpoId]);
ok(!empty($alloc) && abs((float)$alloc[0]['amount'] - 9000000) < 0.01, 'allocation = 9M');

/* ── 9. DECLINE terminal blocks Payables ─────────────────────────── */
h('9. Second LPO · DECLINE terminal → PV blocked');
$lpoRes2 = $wf->draftLpoFromCrLines(
    $crId, 'MATERIALS',
    ['vendor_id' => 2, 'vendor_name' => 'Ergo Office Systems', 'currency' => 'TZS'],
    [ ['line_id' => (int)$wf->sourceableLines($crId)['MATERIALS'][0]['line_id'], 'qty' => 400, 'unit_price' => 2500] ], // small remainder
    $pcId, $pcNm
);
if (!$lpoRes2['ok']) boom('LPO2 draft failed: ' . ($lpoRes2['msg'] ?? '?'));
$lpoId2 = (int)$lpoRes2['id'];
$sub2 = $ac->submit([
    'chain_key'=>'WORKS_LPO_APPROVAL','entity_type'=>'lpo','entity_id'=>$lpoId2,
    'ref_label'=>'x','return_url'=>'x',
    'user_id'=>$pcId,'user_name'=>$pcNm,'user_group'=>$pcG,
]);
$aid2 = (int)$sub2['approval_id'];
$lm->markSubmitted($lpoId2, $aid2);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$pcId,'user_name'=>$pcNm,'user_group'=>$pcG,'comment'=>'PC ok']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$pmId,'user_name'=>$pmNm,'user_group'=>$pmG,'comment'=>'PM ok']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$fmId,'user_name'=>$fmNm,'user_group'=>$fmG,'comment'=>'FM ok']);
$ac->decline(['approval_id'=>$aid2,'user_id'=>$exId,'user_name'=>$exNm,'user_group'=>$exG,'comment'=>'Exec no']);
$aRow2 = $ac->approvalById($aid2);
$lm->markDecision($lpoId2, (string)$aRow2['status']);
$row2 = $db->mySelect("SELECT cash_out_status FROM lpos WHERE c0d = :id", [':id' => $lpoId2]);
ok((string)$aRow2['status']              === 'DECLINED',               'chain terminal DECLINED');
ok((string)$row2[0]['cash_out_status']   === 'DECLINED',               'LPO2 flipped to DECLINED');
$prep2 = $pvs->prepFromLpo($lpoId2, $fmId, $fmNm, 'try declined');
ok(!$prep2['ok'] && stripos($prep2['msg'], 'not APPROVED') !== false, 'PV prep blocked on DECLINED');

/* ── 10. Chain seed + FinChainsController hook + surface wiring ──── */
h('10. Seed + surface wiring');
$chain = $db->mySelect("SELECT c0d, chain_key, status FROM fin_chain_defs WHERE chain_key = 'WORKS_LPO_APPROVAL'", []);
ok(!empty($chain) && (string)$chain[0]['status'] === 'ACTIVE',         'WORKS_LPO_APPROVAL chain ACTIVE');
$steps = $db->mySelect("SELECT step_no, step_key, role_required, is_terminal FROM fin_chain_steps WHERE chain_id = :c ORDER BY step_no", [':c' => (int)$chain[0]['c0d']]) ?: [];
ok(count($steps) === 4,                                                '4 chain steps seeded');
$expected = [
    [1, 'PC_REVIEW',  'PROJECTS COORDINATOR', 0],
    [2, 'PM_APPROVE', 'PROJECT MANAGER',      0],
    [3, 'FM_REVIEW',  'FINANCE MANAGER',      0],
    [4, 'EXEC',       'EXECUTIVE',            1],
];
foreach ($expected as $i => $exp) {
    $s = $steps[$i];
    ok(
        (int)$s['step_no'] === $exp[0]
        && (string)$s['step_key'] === $exp[1]
        && (string)$s['role_required'] === $exp[2]
        && (int)$s['is_terminal'] === $exp[3],
        "step {$exp[0]} · {$exp[1]} · {$exp[2]}" . ($exp[3] ? ' · terminal' : '')
    );
}
// FinChainsController already dispatches entity_type='lpo' → Lpos::markDecision
$fcBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'FinChainsController.php');
ok(strpos($fcBody, "'lpo'")               !== false, "FinChainsController dispatches entity_type='lpo'");
ok(strpos($fcBody, 'markDecision')        !== false, 'FinChainsController calls Lpos::markDecision');
// WorksCrFulfilment stamps the chain key
$mBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'models' . DS . 'WorksCrFulfilment.php');
ok(strpos($mBody, "approval_chain_key = 'WORKS_LPO_APPROVAL'") !== false, 'WorksCrFulfilment stamps chain key');
// Fulfil view has Submit button + WORKS_LPO_APPROVAL mention
$fBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksCrs' . DS . 'fulfil.php');
ok(strpos($fBody, 'btn-submit-lpo')                  !== false, 'fulfil view has btn-submit-lpo');
ok(strpos($fBody, 'WORKS_LPO_APPROVAL')              !== false, 'fulfil view mentions the chain');
ok(strpos($fBody, 'Lpos/submitAj')                   !== false, 'fulfil view POSTs to Lpos/submitAj');

/* ── Z. Restore state ────────────────────────────────────────────── */
h('Z. Restore anchor state');
$db->insertUpdate("UPDATE c_projects SET project_customer_id = :cid, crm_status = :cs WHERE c0d = :p",
    [':cid' => $prevCustId, ':cs' => $prevCrm, ':p' => $prjId]);
$wr->setTerms($prjId, (float)$prevTerms['pct'], (int)$prevTerms['dlp_days']);
p("Restored: customer_id=$prevCustId · crm=$prevCrm · retention_pct={$prevTerms['pct']}");

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