<?php
/**
 * Slice 8X-ContractHandoff · verify.
 *
 * A. pricingPdf perm mirrors pricingXlsx (CRM inherited)
 * B. advanceStage rejects EXECUTION when tender_contract_signing missing; accepts when present
 * C. Schema: 4 assigned_* columns on tender_contract_terms
 * D. saveContractTerms with per-role RBAC:
 *      - CRM   writes  assigned_fm_user_id, ignores assigned_pm_user_id
 *      - FM    writes  assigned_accountant_user_id + assigned_procurement_user_id, ignores assigned_fm_user_id
 *      - Exec  writes  assigned_pm_user_id, ignores assigned_accountant_user_id
 *      - Admin writes all four
 * E. usersInGroup helper returns rows scoped by group
 * F. View surface: Handover chip on both tabs, Team assignments card, collectTerms JS carries 4 seat fields
 * G. Terms remain editable while approval status = IN_PROGRESS (only APPROVED locks)
 */
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 ?? [];
require_once ROOT . DS . 'app' . DS . 'models' . DS . 'Tenders.php';

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();
$tm = new Tenders();

/* ── A. Perms ──────────────────────────────────────────────────────── */
h('A · pricingPdf perms');
$rows = $db->mySelect(
    "SELECT g.user_group_name FROM user_groups_perms p JOIN user_groups g ON g.c0d=p.user_group_id
      WHERE p.controller_allowed='Tenders' AND p.action_allowed='pricingPdf' ORDER BY g.user_group_name", []) ?: [];
$granted = array_column($rows, 'user_group_name');
foreach (['CRM','Administrator','Executive','Finance Manager','Project Manager','Projects Coordinator'] as $g) {
    ok(in_array($g, $granted, true), "$g has pricingPdf");
}
// parity check with pricingXlsx
$parityRows = $db->mySelect(
    "SELECT user_group_id FROM user_groups_perms
      WHERE controller_allowed='Tenders' AND action_allowed='pricingXlsx'
      AND user_group_id NOT IN (SELECT user_group_id FROM user_groups_perms WHERE controller_allowed='Tenders' AND action_allowed='pricingPdf')", []);
ok(empty($parityRows), 'pricingPdf covers every group that had pricingXlsx (no gap)');

/* ── C. Schema ─────────────────────────────────────────────────────── */
h('C · Assignment columns');
$cols = array_column($db->mySelect("SHOW COLUMNS FROM tender_contract_terms", []) ?: [], 'Field');
foreach (['assigned_fm_user_id','assigned_accountant_user_id','assigned_procurement_user_id','assigned_pm_user_id'] as $c) {
    ok(in_array($c, $cols, true), "column: $c");
}

/* ── E. usersInGroup helper ────────────────────────────────────────── */
h('E · usersInGroup helper');
$fmUsers = $tm->usersInGroup('Finance Manager');
$pmUsers = $tm->usersInGroup('Project Manager');
ok(is_array($fmUsers), 'usersInGroup returns array');
foreach ($fmUsers as $u) {
    if (!isset($u['c0d']) || !isset($u['full_name'])) { ok(false, 'user row shape has c0d + full_name'); break; }
}
ok(true, 'user row shape has c0d + full_name');
p('FM users: ' . count($fmUsers) . ' · PM users: ' . count($pmUsers));

/* ── B. EXECUTION stage guard ──────────────────────────────────────── */
h('B · EXECUTION stage guard');
$anchorRow = $db->mySelect("SELECT c0d, tender_stage, tender_status FROM tender_tracker WHERE deleted=0 ORDER BY c0d DESC LIMIT 1", []);
if (empty($anchorRow)) boom('no tender');
$tid = (int)$anchorRow[0]['c0d'];
$origStage  = (string)$anchorRow[0]['tender_stage'];
$origStatus = (string)$anchorRow[0]['tender_status'];
$origSign   = $db->mySelect("SELECT * FROM tender_contract_signing WHERE tender_id = :t", [':t' => $tid]);

// Move tender to CONTRACT_SIGNING and drop any signing row so guard trips
$db->insertUpdate("UPDATE tender_tracker SET tender_stage='CONTRACT_SIGNING' WHERE c0d = :id", [':id' => $tid]);
$db->insertUpdate("DELETE FROM tender_contract_signing WHERE tender_id = :t", [':t' => $tid]);

$threw = false; $why = '';
try { $tm->advanceStage($tid, 'EXECUTION', 'try', 999, 'Verify'); }
catch (Throwable $e) { $threw = true; $why = $e->getMessage(); }
ok($threw && stripos($why, 'Contract Signing') !== false, 'blocked without signing: ' . $why);

// Now insert a signing row and retry
$db->insertUpdate(
    "INSERT INTO tender_contract_signing (tender_id, signed_date, our_signatories, their_signatories, notes)
     VALUES (:t, CURDATE(), '8X-verify', '8X-verify', '8X-verify')",
    [':t' => $tid]
);
$threw2 = false; try { $tm->advanceStage($tid, 'EXECUTION', 'retry', 999, 'Verify'); } catch (Throwable $e) { $threw2 = true; $why = $e->getMessage(); }
ok(!$threw2, 'allowed once signing recorded' . ($threw2 ? ' — got: ' . $why : ''));

/* ── Reset for D tests: put tender at AWARDED + WON, clear any existing approval so saveContractTerms is unlocked ── */
$db->insertUpdate("UPDATE tender_tracker SET tender_stage='AWARDED', tender_status='WON' WHERE c0d = :id", [':id' => $tid]);
$origApprovals = $db->mySelect("SELECT * FROM fin_approvals WHERE entity_type='TENDER_CONTRACT' AND entity_id = :t", [':t' => $tid]) ?: [];
$db->insertUpdate("DELETE FROM fin_approvals WHERE entity_type='TENDER_CONTRACT' AND entity_id = :t", [':t' => $tid]);
// Also clear terms_unlock_active so we're in the vanilla pre-approval state
$db->insertUpdate("UPDATE tender_contract_terms SET terms_unlock_active = 0 WHERE tender_id = :t", [':t' => $tid]);
$origTermsRow = $db->mySelect("SELECT * FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid]);
$origTerms = $origTermsRow[0] ?? null;

/* ── D. saveContractTerms per-role RBAC ────────────────────────────── */
h('D · saveContractTerms per-role RBAC');
$baseline = [
    'contract_no' => '8X-VERIFY-001', 'contract_date' => date('Y-m-d'),
    'contract_value' => 500000, 'payment_terms' => '', 'payment_basis' => '',
    'advance_pct' => 0, 'apg_required' => 0, 'apg_bank' => '', 'apg_validity_months' => 0,
    'performance_bond_value' => 0, 'pg_type' => '', 'performance_bond_ref' => '', 'pg_bank' => '',
    'retention_pct' => 0, 'retention_release_trigger' => '',
    'warranty_months' => 0, 'warranty_coverage' => '', 'ld_pct_per_day' => 0, 'ld_cap_pct' => 0,
    'vat_treatment' => '', 'tax_treatment' => '', 'tax_exemption_basis' => '', 'wht_pct' => 0,
    'gl_code' => '', 'fx_matches_cost_base' => 1, 'bid_bond_value' => 0, 'delivery_timeline_days' => 0,
    'insurance_ref' => '', 'notes' => '',
];

// CRM writes FM, tries PM → PM should be blocked
$tm->saveContractTerms($tid, $baseline + [
    'assigned_fm_user_id' => 111, 'assigned_pm_user_id' => 999,
], 900, 'CRM', 'Verify CRM', '');
$after = $db->mySelect("SELECT * FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid])[0];
ok((int)$after['assigned_fm_user_id'] === 111, 'CRM saved assigned_fm_user_id');
ok((int)$after['assigned_pm_user_id'] === 0,   'CRM blocked from assigned_pm_user_id');

// FM writes Accountant + Procurement, tries FM → FM should be blocked (stays as 111)
$tm->saveContractTerms($tid, $baseline + [
    'assigned_fm_user_id' => 222, 'assigned_accountant_user_id' => 333, 'assigned_procurement_user_id' => 444,
], 901, 'Finance Manager', 'Verify FM', '');
$after2 = $db->mySelect("SELECT * FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid])[0];
ok((int)$after2['assigned_fm_user_id']          === 111, 'FM blocked from changing assigned_fm_user_id');
ok((int)$after2['assigned_accountant_user_id']  === 333, 'FM saved assigned_accountant_user_id');
ok((int)$after2['assigned_procurement_user_id'] === 444, 'FM saved assigned_procurement_user_id');

// Exec writes PM, tries Accountant → Accountant should be blocked (stays as 333)
$tm->saveContractTerms($tid, $baseline + [
    'assigned_pm_user_id' => 555, 'assigned_accountant_user_id' => 777,
], 902, 'Executive', 'Verify Exec', '');
$after3 = $db->mySelect("SELECT * FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid])[0];
ok((int)$after3['assigned_pm_user_id']         === 555, 'Exec saved assigned_pm_user_id');
ok((int)$after3['assigned_accountant_user_id'] === 333, 'Exec blocked from changing assigned_accountant_user_id');

// Admin writes everything
$tm->saveContractTerms($tid, $baseline + [
    'assigned_fm_user_id' => 666, 'assigned_accountant_user_id' => 888,
    'assigned_procurement_user_id' => 999, 'assigned_pm_user_id' => 101,
], 903, 'Administrator', 'Verify Admin', '');
$after4 = $db->mySelect("SELECT * FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid])[0];
ok((int)$after4['assigned_fm_user_id']          === 666, 'Admin saved assigned_fm_user_id');
ok((int)$after4['assigned_accountant_user_id']  === 888, 'Admin saved assigned_accountant_user_id');
ok((int)$after4['assigned_procurement_user_id'] === 999, 'Admin saved assigned_procurement_user_id');
ok((int)$after4['assigned_pm_user_id']          === 101, 'Admin saved assigned_pm_user_id');

// SEAT_RBAC constant exposes the mapping
$rbac = Tenders::SEAT_RBAC;
ok(in_array('CRM',             $rbac['assigned_fm_user_id']),          'CRM in assigned_fm_user_id RBAC');
ok(in_array('FINANCE MANAGER', $rbac['assigned_accountant_user_id']),  'FM in assigned_accountant RBAC');
ok(in_array('EXECUTIVE',       $rbac['assigned_pm_user_id']),          'Exec in assigned_pm RBAC');
ok(in_array('ADMINISTRATOR',   $rbac['assigned_fm_user_id']),          'Admin in all seat RBAC');

/* ── F. View surface ───────────────────────────────────────────────── */
h('F · View surface');
$vBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Tenders' . DS . 'view.php');
ok(strpos($vBody, 'Handover to FM · steps')            !== false, 'Handover chip label present');
ok(strpos($vBody, 'ct_assigned_fm')                    !== false, 'assigned_fm select present');
ok(strpos($vBody, 'ct_assigned_acc')                   !== false, 'assigned_acc select present');
ok(strpos($vBody, 'ct_assigned_proc')                  !== false, 'assigned_proc select present');
ok(strpos($vBody, 'ct_assigned_pm')                    !== false, 'assigned_pm select present');
ok(strpos($vBody, 'assigned_fm_user_id:')              !== false, 'collectTerms JS carries assigned_fm_user_id');
ok(strpos($vBody, 'assigned_accountant_user_id:')      !== false, 'collectTerms carries assigned_accountant_user_id');
ok(strpos($vBody, 'assigned_procurement_user_id:')     !== false, 'collectTerms carries assigned_procurement_user_id');
ok(strpos($vBody, 'assigned_pm_user_id:')              !== false, 'collectTerms carries assigned_pm_user_id');
ok(strpos($vBody, 'handoverSteps: function')           !== false, 'Vue handoverSteps computed present');
ok(strpos($vBody, 'handoverChipStyle')                 !== false, 'Vue handoverChipStyle method present');
ok(strpos($vBody, 'bootHandover')                      !== false, 'bootHandover seed present');
ok(strpos($vBody, 'usersInGroup(\'Finance Manager\')') !== false, 'usersInGroup called for FM');
ok(strpos($vBody, 'usersInGroup(\'Project Manager\')') !== false, 'usersInGroup called for PM');

/* ── G. Terms editable during IN_PROGRESS ─────────────────────────── */
h('G · Terms editable while approval IN_PROGRESS');
// Insert an IN_PROGRESS fin_approvals row for TENDER_CONTRACT so canWriteTerms path fires
$db->insertUpdate("DELETE FROM fin_approvals WHERE entity_type='TENDER_CONTRACT' AND entity_id = :t", [':t' => $tid]);
$db->insertUpdate(
    "INSERT INTO fin_approvals (chain_id, entity_type, entity_id, ref_label, status, submitted_by, submitted_on, created_by)
     VALUES ((SELECT c0d FROM fin_chain_defs WHERE chain_key='TENDER_CONTRACT_APPROVAL' LIMIT 1),
             'TENDER_CONTRACT', :t, '8X-verify', 'IN_PROGRESS', 999, NOW(), 999)",
    [':t' => $tid]
);
$threwG = false; $whyG = '';
try { $tm->saveContractTerms($tid, $baseline, 900, 'CRM', 'Verify CRM', ''); }
catch (Throwable $e) { $threwG = true; $whyG = $e->getMessage(); }
ok(!$threwG, 'terms save accepted while approval IN_PROGRESS' . ($threwG ? ' — got: ' . $whyG : ''));

/* ── Z. Restore ────────────────────────────────────────────────────── */
h('Z. Restore');
$db->insertUpdate("DELETE FROM fin_approvals WHERE entity_type='TENDER_CONTRACT' AND entity_id = :t", [':t' => $tid]);
// Restore the original approval row(s) if the tender had one before we started
foreach ($origApprovals as $oa) {
    $db->insertUpdate(
        "INSERT INTO fin_approvals (c0d, chain_id, entity_type, entity_id, ref_label, status,
                                    current_step_id, submitted_by, submitted_on, created_by)
         VALUES (:id, :ch, :et, :en, :rl, :s, :cs, :sb, :so, :cb)
         ON DUPLICATE KEY UPDATE status = VALUES(status)",
        [
            ':id' => (int)$oa['c0d'], ':ch' => (int)$oa['chain_id'],
            ':et' => (string)$oa['entity_type'], ':en' => (int)$oa['entity_id'],
            ':rl' => (string)$oa['ref_label'], ':s'  => (string)$oa['status'],
            ':cs' => $oa['current_step_id'] !== null ? (int)$oa['current_step_id'] : null,
            ':sb' => (int)($oa['submitted_by'] ?? 0),
            ':so' => $oa['submitted_on'] !== null ? (string)$oa['submitted_on'] : null,
            ':cb' => (int)($oa['created_by'] ?? 0),
        ]
    );
}
$db->insertUpdate("DELETE FROM tender_contract_signing WHERE tender_id = :t AND our_signatories = '8X-verify'", [':t' => $tid]);
$db->insertUpdate("UPDATE tender_tracker SET tender_stage = :s, tender_status = :st WHERE c0d = :id",
    [':s' => $origStage, ':st' => $origStatus, ':id' => $tid]);
if ($origTerms) {
    // Restore seat columns
    $db->insertUpdate(
        "UPDATE tender_contract_terms SET
            assigned_fm_user_id = :fm, assigned_accountant_user_id = :acc,
            assigned_procurement_user_id = :proc, assigned_pm_user_id = :pm,
            contract_no = :cn, contract_value = :cv
          WHERE tender_id = :t",
        [
            ':fm'  => $origTerms['assigned_fm_user_id']          !== null ? (int)$origTerms['assigned_fm_user_id']          : null,
            ':acc' => $origTerms['assigned_accountant_user_id']  !== null ? (int)$origTerms['assigned_accountant_user_id']  : null,
            ':proc'=> $origTerms['assigned_procurement_user_id'] !== null ? (int)$origTerms['assigned_procurement_user_id'] : null,
            ':pm'  => $origTerms['assigned_pm_user_id']          !== null ? (int)$origTerms['assigned_pm_user_id']          : null,
            ':cn'  => (string)$origTerms['contract_no'],
            ':cv'  => (float)$origTerms['contract_value'],
            ':t'   => $tid,
        ]
    );
} else {
    $db->insertUpdate("DELETE FROM tender_contract_terms WHERE tender_id = :t", [':t' => $tid]);
}
if (empty($origSign)) {
    $db->insertUpdate("DELETE FROM tender_contract_signing WHERE tender_id = :t", [':t' => $tid]);
}
p('anchor + terms + signing + approval restored');

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