<?php
/**
 * Slice 8U/V/W · combined verify.
 *
 * 8U · Pricing PDF iframe preview + submission attachment preview
 *   1) pricingPdfAction accepts ?disposition=attachment (buffer + resend logic)
 *   2) submissionAttachmentPreviewAction present + inline disposition
 *   3) View: previewPricingPdf Vue method + btn-preview-proof handler
 *
 * 8V · Company Profile
 *   4) Schema present + seed row (DS Pera)
 *   5) CompanyProfile::writableFieldsFor role matrix
 *   6) save() respects field-level RBAC (CRM cannot write legal_name; Exec cannot write logo_path)
 *   7) Admin can write everything
 *   8) Perms granted correctly (CRM/Exec/Admin write; FM read only)
 *   9) Sidebar wiring (Administrator, CRM, Executive, Finance Manager)
 *  10) Awarded WON auto-fill uses legal_name from CompanyProfile
 *
 * 8W · Awarded reason categories + auto-stage
 *  11) Schema: reason_category ENUM covers all 20 categories
 *  12) recordResult() rejects mismatched reason_category (LOST_* on WON status)
 *  13) recordResult() with WON auto-advances tender stage to AWARDED
 *  14) Vue root wired: #tab_awarded_root + categoriesForStatus + advanceStage
 *  15) Controller returns fresh {result, tender_stage, tender_status} after save
 */
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 (['CompanyProfile.php','Tenders.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();
$cp  = new CompanyProfile();
$cBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'TendersController.php');
$vBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Tenders' . DS . 'view.php');

/* ── 8U · 1-3 ──────────────────────────────────────────────────────── */
h('8U · Pricing PDF preview + submission attachment preview');
ok(strpos($cBody, "\$_GET['disposition']") !== false && strpos($cBody, "=== 'attachment'") !== false, 'pricingPdfAction supports ?disposition=attachment');
ok(strpos($cBody, "function submissionAttachmentPreviewAction") !== false, 'submissionAttachmentPreviewAction present');
ok(strpos($cBody, "\$disp = \$inline ? 'inline' : 'attachment'") !== false, 'preview sends Content-Disposition: inline');
ok(strpos($vBody, "previewPricingPdf: function")   !== false, 'Vue method previewPricingPdf defined');
ok(strpos($vBody, "@click=\"previewPricingPdf('client')")   !== false, 'Client PDF button opens preview modal');
ok(strpos($vBody, "@click=\"previewPricingPdf('internal')") !== false, 'Internal PDF button opens preview modal');
ok(strpos($vBody, "btn-preview-proof") !== false, 'submission attachment preview button present');
ok(strpos($vBody, "submissionAttachmentPreview/") !== false, 'view targets preview endpoint');

/* ── 8V · 4 Schema + seed row ──────────────────────────────────────── */
h('8V · Schema + seed');
$cols = array_column($db->mySelect("SHOW COLUMNS FROM company_profile", []) ?: [], 'Field');
foreach (['trading_name','short_name','legal_name','reg_number','tin','vat_number','tax_regime','bank_name','bank_account_no','edited_by'] as $col) {
    ok(in_array($col, $cols, true), "column: $col");
}
$seed = $cp->get();
ok((string)$seed['trading_name'] === 'DS Pera', 'trading_name defaults to DS Pera');
ok((string)$seed['legal_name']   === 'DS Pera Ltd', 'legal_name defaults to DS Pera Ltd');

/* ── 8V · 5 Role matrix ────────────────────────────────────────────── */
h('8V · Role matrix');
$crmW   = CompanyProfile::writableFieldsFor('CRM');
$execW  = CompanyProfile::writableFieldsFor('Executive');
$adminW = CompanyProfile::writableFieldsFor('Administrator');
$fmW    = CompanyProfile::writableFieldsFor('Finance Manager');
ok(in_array('trading_name', $crmW, true),  'CRM writes trading_name');
ok(!in_array('legal_name',  $crmW, true),  'CRM does NOT write legal_name');
ok(in_array('legal_name',   $execW, true), 'Exec writes legal_name');
ok(!in_array('phone_primary', $execW, true), 'Exec does NOT write phone_primary');
ok(count($adminW) === count($crmW) + count($execW), 'Admin writes all fields (superset)');
ok(count($fmW)    === 0,                   'FM has no write fields (read only)');

/* ── 8V · 6-7 save() enforces RBAC ─────────────────────────────────── */
h('8V · save() enforces RBAC');
// Snapshot
$before = $cp->get();
$rC = $cp->save(['legal_name' => '8V-VERIFY hack', 'phone_primary' => '+255111111111'], 'CRM', 900, 'Verify CRM');
$after = $cp->get();
ok($rC['ok'], 'CRM save returns ok');
ok((string)$after['legal_name'] === (string)$before['legal_name'], 'CRM legal_name blocked (silent ignore)');
ok((string)$after['phone_primary'] === '+255111111111', 'CRM phone_primary saved');
ok(in_array('legal_name', $rC['ignored'] ?? [], true), 'legal_name in ignored list for CRM');

$rE = $cp->save(['legal_name' => '8V-VERIFY DS Pera Ltd', 'phone_primary' => '+255999999999'], 'Executive', 901, 'Verify Exec');
$after2 = $cp->get();
ok((string)$after2['legal_name']    === '8V-VERIFY DS Pera Ltd', 'Exec legal_name saved');
ok((string)$after2['phone_primary'] === '+255111111111', 'Exec phone_primary blocked (silent)');

$rA = $cp->save(['legal_name' => '8V-VERIFY final', 'phone_primary' => '+255222222222'], 'Administrator', 902, 'Verify Admin');
$after3 = $cp->get();
ok((string)$after3['legal_name']    === '8V-VERIFY final',      'Admin legal_name saved');
ok((string)$after3['phone_primary'] === '+255222222222',        'Admin phone_primary saved');

$rNoRole = $cp->save(['legal_name' => 'nope'], 'Field Supervisor', 903, 'Verify FS');
ok(!$rNoRole['ok'] && stripos($rNoRole['msg'], 'role') !== false, 'Field Supervisor blocked entirely');

/* ── 8V · 8 Perms + 9 Sidebars ─────────────────────────────────────── */
h('8V · Perms + sidebars');
$permRows = $db->mySelect(
    "SELECT g.user_group_name, p.action_allowed FROM user_groups_perms p JOIN user_groups g ON g.c0d=p.user_group_id
      WHERE p.controller_allowed='CompanyProfile' ORDER BY p.action_allowed, g.user_group_name", []) ?: [];
$permSet = [];
foreach ($permRows as $r) $permSet[$r['user_group_name'] . '::' . $r['action_allowed']] = true;
foreach (['CRM','Executive','Administrator','Finance Manager'] as $g) ok(isset($permSet["$g::index"]), "$g has index");
foreach (['CRM','Executive','Administrator']                   as $g) ok(isset($permSet["$g::saveAj"]), "$g has saveAj");
ok(!isset($permSet['Finance Manager::saveAj']), 'FM does NOT have saveAj');

foreach (['administrator','crm','executive','finance_manager'] as $sidebar) {
    $sBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'layouts' . DS . 'partials' . DS . '_sidebar_' . $sidebar . '.php');
    ok(strpos($sBody, 'CompanyProfile') !== false, "sidebar '$sidebar' has CompanyProfile link");
}

/* ── 8V · 10 Awarded WON auto-fill uses CompanyProfile.legal_name ── */
h('8V · Awarded winner default');
ok(strpos($vBody, '(new CompanyProfile())->get()') !== false, 'view fetches CompanyProfile');
ok(strpos($vBody, "\$ourBidderName = trim((string)(\$ourProfile['legal_name'] ?? ''))") !== false, 'winner defaults from legal_name');

/* ── 8W · 11 Schema ────────────────────────────────────────────────── */
h('8W · Reason category schema');
$rc = $db->mySelect("SHOW COLUMNS FROM tender_tracker_results LIKE 'reason_category'", []);
ok(!empty($rc), 'reason_category column exists');
foreach (['WON_BEST_PRICE','LOST_UNFINISHED_DOCS','CANCELLED_SCOPE_CHANGE','WITHDRAWN_DECIDED_NOT_TO_BID'] as $cat) {
    ok(stripos($rc[0]['Type'], $cat) !== false, "ENUM includes $cat");
}

/* ── 8W · 12 recordResult rejects mismatched category ─────────────── */
h('8W · recordResult rejects mismatched category');
$rowT = $db->mySelect("SELECT c0d, tender_stage FROM tender_tracker WHERE deleted=0 ORDER BY c0d DESC LIMIT 1", []);
if (empty($rowT)) boom('no tender available');
$tid = (int)$rowT[0]['c0d'];
$origStage = (string)$rowT[0]['tender_stage'];
$origResult = (new Tenders())->result($tid); // for restore

$threw = false; $why = '';
try {
    (new Tenders())->recordResult($tid, 'WON', date('Y-m-d'), 'DS Pera Ltd', 100000, 'note', '', 999, 'Verify', 'LOST_UNFINISHED_DOCS');
} catch (Throwable $e) { $threw = true; $why = $e->getMessage(); }
ok($threw && stripos($why, 'reason category') !== false, 'mismatched category refused: ' . $why);

/* ── 8W · 13 WON auto-advances stage ──────────────────────────────── */
h('8W · WON auto-advances stage');
// First set the tender to PREP so we can watch it advance
$db->insertUpdate("UPDATE tender_tracker SET tender_stage='PREP' WHERE c0d = :id", [':id' => $tid]);
(new Tenders())->recordResult($tid, 'WON', date('Y-m-d'), 'DS Pera Ltd', 100000, 'note', '', 999, 'Verify', 'WON_BEST_PRICE');
$after = $db->mySelect("SELECT tender_stage, tender_status FROM tender_tracker WHERE c0d = :id", [':id' => $tid])[0];
ok((string)$after['tender_stage']  === 'AWARDED', 'tender_stage advanced to AWARDED');
ok((string)$after['tender_status'] === 'WON',     'tender_status set to WON');

/* ── 8W · 14 View surface ─────────────────────────────────────────── */
h('8W · View surface');
ok(strpos($vBody, 'id="tab_awarded_root"')       !== false, 'Vue root #tab_awarded_root present');
ok(strpos($vBody, 'categoriesForStatus')         !== false, 'categoriesForStatus method defined');
ok(strpos($vBody, 'reason_category')             !== false, 'reason_category v-model bound');
ok(strpos($vBody, 'advanceStage: function')      !== false, 'advanceStage method defined');
ok(strpos($vBody, "categoriesForStatus(draft.result_status)") !== false, 'category dropdown depends on status');
ok(strpos($vBody, 'Manually advance stage')      !== false, 'manual advance widget present');

/* ── 8W · 15 Controller returns fresh state ───────────────────────── */
h('8W · Controller returns fresh state');
ok(strpos($cBody, "'result' => \$tm->result(\$id)") !== false, 'recordResultAj returns result row');
ok(strpos($cBody, "'tender_stage'  => \$t['tender_stage']")  !== false, 'recordResultAj returns tender_stage');
ok(strpos($cBody, "'tender_status' => \$t['tender_status']") !== false, 'recordResultAj returns tender_status');

/* ── Z. Restore ────────────────────────────────────────────────────── */
h('Z. Restore');
$db->insertUpdate("UPDATE tender_tracker SET tender_stage = :s WHERE c0d = :id", [':s' => $origStage, ':id' => $tid]);
if ($origResult) {
    $db->insertUpdate("UPDATE tender_tracker_results SET result_status = :s WHERE tender_id = :id",
        [':s' => (string)$origResult['result_status'], ':id' => $tid]);
    $db->insertUpdate("UPDATE tender_tracker SET tender_status = :s WHERE c0d = :id",
        [':s' => (string)$origResult['result_status'], ':id' => $tid]);
} else {
    $db->insertUpdate("DELETE FROM tender_tracker_results WHERE tender_id = :id", [':id' => $tid]);
    $db->insertUpdate("UPDATE tender_tracker SET tender_status = 'OPEN' WHERE c0d = :id", [':id' => $tid]);
}
// Restore company profile
$cp->save([
    'trading_name'      => $before['trading_name'],
    'short_name'        => $before['short_name'],
    'legal_name'        => $before['legal_name'],
    'phone_primary'     => $before['phone_primary'] ?? '',
], 'Administrator', (int)($before['edited_by'] ?? 0), (string)($before['edited_by_name'] ?? ''));
p('tender + profile restored');

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