<?php
/**
 * Slice 8-UX Exec-unlocks-for-FM verify.
 *
 *   1) Tender at AWARDED/WON, terms saved, chain APPROVED → locked.
 *   2) FM tries to amend → BLOCKED (locked, no unlock).
 *   3) FM tries to unlock → BLOCKED (only Exec/Admin can unlock).
 *   4) Exec unlocks without reason → BLOCKED.
 *   5) Exec unlocks WITH reason → terms_unlock_active=1, unlock reason stored, synthetic
 *      _UNLOCK_ISSUED audit row written, all FMs notified.
 *   6) FM edits 2 fields → succeeds; audit shows post_approval=1 for each field, unlock reason
 *      auto-attached; auto-relock happens; _UNLOCK_CONSUMED audit row written.
 *   7) Second FM save → BLOCKED again (unlock consumed).
 *   8) Exec unlocks again, then relocks (cancel) without a save → _UNLOCK_CANCELLED audit row.
 */
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','Notifications.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();
$nm = new Notifications();

$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';

$stamp = date('YmdHis');
$fmUnreadBefore = $nm->unreadCountForUser($uidFm);

h('1. Fresh tender to APPROVED (locked) state');
$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', 'ACME Ltd.', 'goods',
         CURDATE(), 90000000, 'TZS', 'PREP', 'OPEN', :u, :un, NOW())",
    [':tn' => 'T-UNLOCK-' . $stamp, ':tt' => 'SLICE 8-UX unlock flow', ':u' => $uidCrm, ':un' => $unmCrm]
);
$tid = (int)($fb['last_id'] ?? 0);
foreach (['SUBMISSION','FOLLOW_UP','OPENING'] as $s) $tm->advanceStage($tid, $s, 'x', $uidCrm, $unmCrm);
$tm->recordResult($tid, 'WON', date('Y-m-d'), '', 90000000, '', '', $uidCrm, $unmCrm);
$tm->advanceStage($tid, 'AWARDED', 'a', $uidCrm, $unmCrm);
$terms = ['contract_no'=>'X-001','contract_date'=>date('Y-m-d'),'contract_value'=>90000000,
          'payment_terms'=>'Net 30','payment_basis'=>'MILESTONE','advance_pct'=>20,'retention_pct'=>10,'warranty_months'=>12];
$tm->saveContractTerms($tid, $terms, $uidFm, $ugFm, $unmFm, '');
$res = $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]);
$aid = (int)$res['approval_id'];
$ac->approve(['approval_id'=>$aid,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,'comment'=>'ok','assignments'=>['ACCOUNTANT'=>['user_id'=>$accId,'user_name'=>$accNm],'PROCUREMENT'=>['user_id'=>$procId,'user_name'=>$procNm]]]);
$ac->approve(['approval_id'=>$aid,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,'comment'=>'ok','assignments'=>['PROJECT_MANAGER'=>['user_id'=>$pmId,'user_name'=>$pmNm]]]);
ok($tm->contractApprovalStatus($tid) === 'APPROVED', 'Chain APPROVED · terms locked');

/* 2 */
h('2. FM amend on locked terms → BLOCKED');
try { $tm->saveContractTerms($tid, array_merge($terms, ['advance_pct'=>15]), $uidFm, $ugFm, $unmFm, 'x'); ok(false, 'FM was allowed'); }
catch (\Throwable $e) { ok(strpos($e->getMessage(), 'ask Executive to unlock') !== false || strpos($e->getMessage(), 'only Exec/Admin') !== false, 'FM blocked: ' . $e->getMessage()); }

/* 3 */
h('3. FM tries to unlock → BLOCKED (only Exec/Admin)');
try { $tm->unlockTerms($tid, $uidFm, $unmFm, $ugFm, 'trying'); ok(false, 'FM was allowed to unlock'); }
catch (\Throwable $e) { ok(strpos($e->getMessage(), 'Only Executive') !== false, 'FM blocked: ' . $e->getMessage()); }

/* 4 */
h('4. Exec unlocks without reason → BLOCKED');
try { $tm->unlockTerms($tid, $uidExec, $unmExec, $ugExec, ''); ok(false, 'Exec was allowed to unlock without reason'); }
catch (\Throwable $e) { ok(strpos($e->getMessage(), 'reason is required') !== false, 'Exec blocked: ' . $e->getMessage()); }

/* 5 */
h('5. Exec unlocks WITH reason → unlock_active=1 + audit + FM notified');
$tm->unlockTerms($tid, $uidExec, $unmExec, $ugExec, 'Client renegotiated retention %');
$row = $db->mySelect("SELECT terms_unlock_active, terms_unlock_by_name, terms_unlock_reason FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid])[0];
p("unlock_active={$row['terms_unlock_active']} by={$row['terms_unlock_by_name']} reason='{$row['terms_unlock_reason']}'");
ok((int)$row['terms_unlock_active'] === 1 && $row['terms_unlock_by_name'] === $unmExec, 'unlock state set');
$issued = $db->mySelect("SELECT COUNT(*) n FROM tender_contract_terms_audit WHERE tender_id = :t AND field_name = '_UNLOCK_ISSUED'", [':t' => $tid])[0]['n'];
ok((int)$issued === 1, '_UNLOCK_ISSUED audit row written');
$fmUnreadAfter = $nm->unreadCountForUser($uidFm);
ok($fmUnreadAfter > $fmUnreadBefore, "FM notification fired (unread {$fmUnreadBefore} → {$fmUnreadAfter})");

/* 6 */
h('6. FM amends 2 fields under unlock → succeeds, auto-relocks');
$termsV2 = array_merge($terms, ['retention_pct'=>5, 'notes'=>'FM: retention lowered to 5% per client']);
$tm->saveContractTerms($tid, $termsV2, $uidFm, $ugFm, $unmFm, '');
$after = $db->mySelect("SELECT terms_unlock_active FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid])[0];
ok((int)$after['terms_unlock_active'] === 0, 'Auto-relocked (unlock_active=0)');
$consumed = $db->mySelect("SELECT COUNT(*) n FROM tender_contract_terms_audit WHERE tender_id = :t AND field_name = '_UNLOCK_CONSUMED'", [':t' => $tid])[0]['n'];
ok((int)$consumed === 1, '_UNLOCK_CONSUMED audit row written');
$fmChanges = $db->mySelect("SELECT field_name, changed_by_group, reason, post_approval FROM tender_contract_terms_audit WHERE tender_id = :t AND changed_by_group = :g AND field_name NOT LIKE '\\_%' ORDER BY c0d DESC LIMIT 2", [':t' => $tid, ':g' => $ugFm]);
p("FM-authored change rows: " . count($fmChanges));
foreach ($fmChanges as $c) p("  · {$c['field_name']} · post_approval={$c['post_approval']} · reason='{$c['reason']}'");
ok(count($fmChanges) === 2, '2 FM field changes recorded');
$allPost = true; $allHaveReason = true;
foreach ($fmChanges as $c) {
    if ((int)$c['post_approval'] !== 1) $allPost = false;
    if (trim((string)$c['reason']) === '') $allHaveReason = false;
}
ok($allPost, 'Each FM row tagged post_approval=1');
ok($allHaveReason, 'Each FM row carries a reason (auto-filled from Exec unlock)');

/* 7 */
h('7. Second FM save without new unlock → BLOCKED again');
try { $tm->saveContractTerms($tid, array_merge($terms, ['advance_pct'=>18]), $uidFm, $ugFm, $unmFm, 'try again'); ok(false, 'FM was allowed after unlock consumed'); }
catch (\Throwable $e) { ok(strpos($e->getMessage(), 'ask Executive to unlock') !== false || strpos($e->getMessage(), 'only Exec/Admin') !== false, 'FM re-blocked: ' . $e->getMessage()); }

/* 8 */
h('8. Exec unlocks, then cancels (relockTerms) without a save → _UNLOCK_CANCELLED');
$tm->unlockTerms($tid, $uidExec, $unmExec, $ugExec, 'Second attempt');
$tm->relockTerms($tid, $uidExec, $unmExec, $ugExec);
$state = $db->mySelect("SELECT terms_unlock_active FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid])[0];
ok((int)$state['terms_unlock_active'] === 0, 'Relock cleared unlock_active');
$cancelled = $db->mySelect("SELECT COUNT(*) n FROM tender_contract_terms_audit WHERE tender_id = :t AND field_name = '_UNLOCK_CANCELLED'", [':t' => $tid])[0]['n'];
ok((int)$cancelled === 1, '_UNLOCK_CANCELLED audit row written');

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