<?php
/**
 * Slice 8k-LpoNoOnApprove · LPO numbering deferred until Exec approval.
 *
 *  1) DRAFT LPO created via WorksCrFulfilment::draftLpoFromCrLines has PENDING-* ref.
 *  2) DocNumberSettings LPO next_no unchanged (no allocation at draft time).
 *  3) Submit + walk chain PC → PM → FM → Exec.
 *  4) On terminal APPROVED, markDecision allocates real lpo_no; PENDING ref replaced.
 *  5) DocNumberSettings LPO next_no bumped by 1.
 *  6) Second LPO gets next sequential number.
 *  7) DECLINE branch: LPO stays PENDING, DocNumberSettings NOT bumped, cash_out=DECLINED.
 *  8) Idempotent markDecision: second APPROVED call doesn't reallocate.
 *  9) Views surface PENDING chip.
 */
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','DocNumberSettings.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();
$dns = new DocNumberSettings();

$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 + purge ─────────────────────────────────────────────── */
h('0. Anchor works project + purge prior 8k artefacts');
$anchor = $db->mySelect(
    "SELECT p.c0d, 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 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);

$oldPhases = $db->mySelect("SELECT c0d FROM works_phases WHERE project_id = :p AND title LIKE '8k-Verify %'", [':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'];
            $db->insertUpdate("DELETE FROM works_retention_holdings WHERE lpo_id = :l", [':l' => $lid]);
            $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]);
            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]);
            }
        }
        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_phases WHERE c0d = :p", [':p' => $phId]);
}
p("purged " . count($oldPhases) . " prior 8k phases");

/* ── 1. Create fresh phase + CR + Works LPO ───────────────────────── */
h('1. Draft Works LPO from CR fulfilment');
$phId = $wp->create($prjId, [
    'code'=>'8kv-' . date('His'), 'title'=>'8k-Verify ' . date('YmdHis'),
    'planned_start'=>date('Y-m-d'), 'planned_end'=>date('Y-m-d', strtotime('+30 days')),
], $pcId);
$crId = $wc->draft($prjId, $phId, ['title' => '8k CR ' . date('His')], $pcId, $pcNm);
$wc->addLine($crId, 'MATERIALS', ['description'=>'8k 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);

// Capture LPO doc-number counter BEFORE
$lpoSettingBefore = $dns->findByKey('LPO');
$nextBefore       = (int)$lpoSettingBefore['next_no'];
p("LPO doc_number_settings.next_no BEFORE draft: $nextBefore");

$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' => 3000, 'unit_price' => 2500] ], // 7.5M
    $pcId, $pcNm
);
if (!$lpoRes['ok']) boom('LPO draft failed: ' . ($lpoRes['msg'] ?? '?'));
$lpoId = (int)$lpoRes['id'];

/* ── 2. Assert DRAFT LPO has PENDING ref; counter unchanged ─────── */
h('2. DRAFT LPO gets PENDING-* placeholder · doc counter unchanged');
$row = $db->mySelect("SELECT lpo_no, cash_out_status FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
p("LPO row: lpo_no='{$row[0]['lpo_no']}' cash_out_status='{$row[0]['cash_out_status']}'");
ok(strpos((string)$row[0]['lpo_no'], 'PENDING-') === 0, 'lpo_no starts with PENDING-');
ok((string)$row[0]['cash_out_status'] === 'DRAFT',       'cash_out_status = DRAFT');

$lpoSettingAfterDraft = $dns->findByKey('LPO');
$nextAfterDraft       = (int)$lpoSettingAfterDraft['next_no'];
ok($nextAfterDraft === $nextBefore, "LPO doc counter unchanged: was $nextBefore, still $nextAfterDraft");

/* ── 3. Submit + walk chain PC → PM → FM → Exec ─────────────────── */
h('3. Submit + walk chain · terminal APPROVED');
$submitRes = $ac->submit([
    'chain_key'=>'WORKS_LPO_APPROVAL', 'entity_type'=>'lpo', 'entity_id'=>$lpoId,
    'ref_label'=>'8k verify', 'return_url'=>'x',
    'user_id'=>$pcId, 'user_name'=>$pcNm, 'user_group'=>$pcG,
]);
$aid = (int)$submitRes['approval_id'];
$lm->markSubmitted($lpoId, $aid);
$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']);
$aRow = $ac->approvalById($aid);
ok((string)$aRow['status'] === 'APPROVED', 'chain terminal APPROVED');

$lm->markDecision($lpoId, (string)$aRow['status']);
$rowAppr = $db->mySelect("SELECT lpo_no, cash_out_status FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
p("Post-APPROVED: lpo_no='{$rowAppr[0]['lpo_no']}' cash_out='{$rowAppr[0]['cash_out_status']}'");
ok(strpos((string)$rowAppr[0]['lpo_no'], 'PENDING-') !== 0, 'lpo_no no longer PENDING');
// Format is doc_number_settings-driven (prefix/year/seq configurable in Admin); assert allocator shape not literal LPO prefix.
ok(preg_match('/^[A-Z]+-\d{4}-\d+$/', (string)$rowAppr[0]['lpo_no']) === 1, 'lpo_no matches allocator format PREFIX-YYYY-####');
ok((string)$rowAppr[0]['cash_out_status'] === 'APPROVED', 'cash_out_status = APPROVED');

$lpoSettingAfter = $dns->findByKey('LPO');
$nextAfter       = (int)$lpoSettingAfter['next_no'];
ok($nextAfter === $nextBefore + 1, "LPO doc counter bumped by 1: $nextBefore → $nextAfter");

/* ── 4. Second APPROVED LPO gets next sequential number ─────────── */
h('4. Second LPO gets next sequential number');
$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] ],
    $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'=>'8k v2','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']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$pmId,'user_name'=>$pmNm,'user_group'=>$pmG,'comment'=>'PM']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$fmId,'user_name'=>$fmNm,'user_group'=>$fmG,'comment'=>'FM']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$exId,'user_name'=>$exNm,'user_group'=>$exG,'comment'=>'Exec']);
$aRow2 = $ac->approvalById($aid2);
$lm->markDecision($lpoId2, (string)$aRow2['status']);
$row2 = $db->mySelect("SELECT lpo_no FROM lpos WHERE c0d = :id", [':id' => $lpoId2]);
p("LPO2: {$row2[0]['lpo_no']}");
ok(preg_match('/^[A-Z]+-\d{4}-\d+$/', (string)$row2[0]['lpo_no']) === 1, 'LPO2 has allocator format');
$seq1 = (int)substr(strrchr($rowAppr[0]['lpo_no'], '-'), 1);
$seq2 = (int)substr(strrchr($row2[0]['lpo_no'],    '-'), 1);
ok($seq2 === $seq1 + 1, "LPO2 seq = LPO1 seq + 1: $seq1 → $seq2");

/* ── 5. DECLINE branch: LPO stays PENDING, counter NOT bumped ────── */
h('5. DECLINE branch · LPO stays PENDING · counter not bumped');
$lpoRes3 = $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' => 100, 'unit_price' => 2500] ],
    $pcId, $pcNm
);
if (!$lpoRes3['ok']) boom('LPO3 draft failed: ' . ($lpoRes3['msg'] ?? '?'));
$lpoId3 = (int)$lpoRes3['id'];
$row3Before = $db->mySelect("SELECT lpo_no FROM lpos WHERE c0d = :id", [':id' => $lpoId3]);
$pendingRef3 = (string)$row3Before[0]['lpo_no'];
ok(strpos($pendingRef3, 'PENDING-') === 0, 'LPO3 starts as PENDING');

$counterBeforeDecline = (int)$dns->findByKey('LPO')['next_no'];

$sub3 = $ac->submit([
    'chain_key'=>'WORKS_LPO_APPROVAL','entity_type'=>'lpo','entity_id'=>$lpoId3,
    'ref_label'=>'8k decline','return_url'=>'x',
    'user_id'=>$pcId,'user_name'=>$pcNm,'user_group'=>$pcG,
]);
$aid3 = (int)$sub3['approval_id'];
$lm->markSubmitted($lpoId3, $aid3);
$ac->approve(['approval_id'=>$aid3,'user_id'=>$pcId,'user_name'=>$pcNm,'user_group'=>$pcG,'comment'=>'PC']);
$ac->approve(['approval_id'=>$aid3,'user_id'=>$pmId,'user_name'=>$pmNm,'user_group'=>$pmG,'comment'=>'PM']);
$ac->approve(['approval_id'=>$aid3,'user_id'=>$fmId,'user_name'=>$fmNm,'user_group'=>$fmG,'comment'=>'FM']);
$ac->decline(['approval_id'=>$aid3,'user_id'=>$exId,'user_name'=>$exNm,'user_group'=>$exG,'comment'=>'Exec no']);
$aRow3 = $ac->approvalById($aid3);
$lm->markDecision($lpoId3, (string)$aRow3['status']);

$row3After = $db->mySelect("SELECT lpo_no, cash_out_status FROM lpos WHERE c0d = :id", [':id' => $lpoId3]);
p("LPO3 post-decline: lpo_no='{$row3After[0]['lpo_no']}' cash_out='{$row3After[0]['cash_out_status']}'");
ok((string)$row3After[0]['lpo_no']       === $pendingRef3,          'LPO3 keeps its PENDING ref (no real number wasted)');
ok((string)$row3After[0]['cash_out_status'] === 'DECLINED',          'LPO3 cash_out_status = DECLINED');
$counterAfterDecline = (int)$dns->findByKey('LPO')['next_no'];
ok($counterAfterDecline === $counterBeforeDecline, "counter NOT bumped: $counterBeforeDecline → $counterAfterDecline");

/* ── 6. Idempotent markDecision — second APPROVED call is safe ───── */
h('6. Idempotent markDecision(APPROVED)');
$counter6Before = (int)$dns->findByKey('LPO')['next_no'];
$lm->markDecision($lpoId, 'APPROVED');       // LPO1 was already APPROVED
$counter6After = (int)$dns->findByKey('LPO')['next_no'];
$row1Again     = $db->mySelect("SELECT lpo_no FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
ok($counter6After === $counter6Before,                          'counter unchanged on re-approve');
ok((string)$row1Again[0]['lpo_no'] === (string)$rowAppr[0]['lpo_no'], 'lpo_no unchanged on re-approve');

/* ── 7. Views surface PENDING chip ─────────────────────────────── */
h('7. Views surface PENDING chip');
$fBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksCrs' . DS . 'fulfil.php');
ok(strpos($fBody, "PENDING-'")      !== false || strpos($fBody, 'PENDING-')      !== false, 'fulfil view detects PENDING');
ok(strpos($fBody, "'PENDING'")       !== false || strpos($fBody, 'PENDING</span') !== false, 'fulfil view shows PENDING chip');
$lBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Lpos' . DS . 'view.php');
ok(strpos($lBody, 'PENDING-')        !== false, 'Lpos/view detects PENDING');
ok(strpos($lBody, 'allocated on Exec') !== false, 'Lpos/view explains the rule');

/* ── Z. Restore ─────────────────────────────────────────────────── */
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('anchor state restored');

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