<?php
/**
 * Slice 8S-OverheadPools · overhead pool distinct from tender_items, audience-aware pricing.
 *
 *  1) Schema present + new columns on tender_pricing_versions + pricing_defaults
 *  2) PricingVersionOverheads CRUD (add/find/update/remove)
 *  3) sumsByMode returns per-mode totals
 *  4) loadTemplate GOODS_LOCAL / GOODS_IMPORT seed presets, idempotent
 *  5) TenderPricing::rollupVersion: overhead affects sum_cost (all modes), sum_price (DISTRIBUTED + PRELIMS_LINE only)
 *  6) INTERNAL_ONLY overhead bumps sum_cost but not sum_price → true margin < client margin
 *  7) createVersion(fresh) on a Goods tender auto-loads GOODS_LOCAL template + inherits procurement_mode
 *  8) createVersion(clone) copies overhead lines from seed version
 *  9) Cloned version's overhead sums roll up correctly
 * 10) updateVersionHeader accepts procurement_mode + client_boq_visibility (whitelist enforced)
 * 11) PricingDefaults get + save round-trip includes procurement_mode + client_boq_visibility
 * 12) Perms: pricingOverheads* granted to whoever has pricingAddLineAj
 * 13) Controller endpoints defined + view surface + PricingDefaults form fields
 */
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 (['PricingDefaults.php','PricingVersionOverheads.php','TenderPricing.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();
$tp  = new TenderPricing();
$pvo = new PricingVersionOverheads();
$pd  = new PricingDefaults();
$uid = 301; $unm = 'Verify 8S';

/* ── 0. Snapshot defaults + anchor a Goods tender ──────────────────── */
h('0. Snapshot + anchor');
$defSnap = $pd->get();
$goodsRow = $db->mySelect("SELECT c0d FROM tender_tracker WHERE deleted=0 AND project_type='goods' ORDER BY c0d DESC LIMIT 1", []);
if (empty($goodsRow)) boom('no goods tender available');
$tenderId = (int)$goodsRow[0]['c0d'];
p("anchor goods tender #$tenderId");

/* ── 1. Schema ─────────────────────────────────────────────────────── */
h('1. Schema');
$cols = array_column($db->mySelect("SHOW COLUMNS FROM pricing_version_overheads", []) ?: [], 'Field');
foreach (['version_id','tender_id','line_no','category','label','amount','display_mode','deleted'] as $c) {
    ok(in_array($c, $cols, true), "column present: $c");
}
$vcols = array_column($db->mySelect("SHOW COLUMNS FROM tender_pricing_versions", []) ?: [], 'Field');
foreach (['procurement_mode','client_boq_visibility','sum_overhead'] as $c) {
    ok(in_array($c, $vcols, true), "tender_pricing_versions.$c added");
}
$dcols = array_column($db->mySelect("SHOW COLUMNS FROM pricing_defaults", []) ?: [], 'Field');
foreach (['default_procurement_mode','default_client_boq_visibility'] as $c) {
    ok(in_array($c, $dcols, true), "pricing_defaults.$c added");
}

/* ── 2-4. Create a fresh draft to work with ────────────────────────── */
h('2-4. Fresh Goods draft — createVersion auto-loads GOODS_LOCAL template');
$existing = $tp->activeDraft($tenderId);
$restoreDraftId = 0;
if ($existing) {
    $restoreDraftId = (int)$existing['c0d'];
    $db->insertUpdate("UPDATE tender_pricing_versions SET status='SUPERSEDED' WHERE c0d = :id", [':id' => $restoreDraftId]);
}
$vid = $tp->createVersion($tenderId, null, $uid, $unm);
$v = $tp->findVersion($vid);
ok(!empty($v),                                    'fresh draft created');
ok($v['procurement_mode'] === 'LOCAL',            'procurement_mode = LOCAL (from goods default)');
ok($v['client_boq_visibility'] === 'ITEMS_AND_SECTIONS', 'client_boq_visibility inherits ITEMS_AND_SECTIONS');
$autoOh = $pvo->listFor($vid);
p("auto-seeded overhead lines: " . count($autoOh));
ok(count($autoOh) === 5, 'GOODS_LOCAL template auto-loaded (5 preset lines)');
$autoLabels = array_column($autoOh, 'label');
ok(in_array('Loading / offloading crew', $autoLabels, true), 'includes local-purchase MANPOWER preset');

/* ── 5. Add + update + remove one overhead ─────────────────────────── */
h('5. Overhead CRUD');
$r1 = $pvo->add($vid, $tenderId, ['category' => 'ADMIN_OVERHEAD', 'label' => '8S manual overhead', 'amount' => 250000, 'display_mode' => 'INTERNAL_ONLY'], $uid, $unm);
ok($r1['ok'] && $r1['id'] > 0, 'add returns ok + id');
$oid = (int)$r1['id'];
$got = $pvo->find($oid);
ok((float)$got['amount'] === 250000.0 && $got['category'] === 'ADMIN_OVERHEAD' && $got['display_mode'] === 'INTERNAL_ONLY', 'stored values round-trip');
$rU = $pvo->update($oid, ['amount' => 500000, 'display_mode' => 'GARBAGE'], $uid);
ok($rU['ok'], 'update ok');
$got2 = $pvo->find($oid);
ok((float)$got2['amount'] === 500000.0, 'amount updated');
ok($got2['display_mode'] === 'INTERNAL_ONLY', 'invalid display_mode falls back to previous');
$rR = $pvo->remove($oid, $uid);
ok($rR['ok'] && $pvo->find($oid) === null, 'remove soft-deletes');

/* ── 6. sumsByMode ─────────────────────────────────────────────────── */
h('6. sumsByMode');
$pvo->add($vid, $tenderId, ['category' => 'TRANSPORT', 'label' => 'DIST', 'amount' => 100000, 'display_mode' => 'DISTRIBUTED'], $uid, $unm);
$pvo->add($vid, $tenderId, ['category' => 'SITE_SETUP','label' => 'PREL', 'amount' => 200000, 'display_mode' => 'PRELIMS_LINE'], $uid, $unm);
$pvo->add($vid, $tenderId, ['category' => 'ADMIN_OVERHEAD','label' => 'INT', 'amount' => 300000, 'display_mode' => 'INTERNAL_ONLY'], $uid, $unm);
$sums = $pvo->sumsByMode($vid);
p("dist=$sums[DISTRIBUTED] prelims=$sums[PRELIMS_LINE] internal=$sums[INTERNAL_ONLY]");
ok((float)$sums['DISTRIBUTED']   === 100000.0, 'DISTRIBUTED total');
ok((float)$sums['PRELIMS_LINE']  === 200000.0, 'PRELIMS_LINE total');
ok((float)$sums['INTERNAL_ONLY'] === 300000.0, 'INTERNAL_ONLY total');

/* ── 7. rollupVersion accounts for overhead correctly ──────────────── */
h('7. rollupVersion');
// Add one priced item so we have direct cost > 0
$secId = $tp->addSection($vid, $tenderId, 'Test section', '', $uid);
$tp->addLine($secId, ['item_ref'=>'X','description'=>'X','qty'=>1,'unit_cost'=>1000000,'markup_pct'=>20], $uid);
$tp->rollupVersion($vid);
$v2 = $tp->findVersion($vid);
$sums2 = $pvo->sumsByMode($vid);
$totalOh = $sums2['DISTRIBUTED'] + $sums2['PRELIMS_LINE'] + $sums2['INTERNAL_ONLY'];
p("sum_cost={$v2['sum_cost']} sum_overhead={$v2['sum_overhead']} sum_price={$v2['sum_price']}");
// direct = 1,000,000 + auto-loaded template overheads (all zero) + 600k explicit → cost = 1,000,000 + 600,000 = 1,600,000
ok(abs((float)$v2['sum_overhead'] - $totalOh) < 0.01, 'sum_overhead matches total across modes');
// sum_price: 1,000,000 * 1.20 (item sell) + prelims_amount (default) + DISTRIBUTED 100k + PRELIMS_LINE 200k = 1,200,000 + 0 + 100,000 + 200,000 = 1,500,000
$expectedSell = 1200000 + (float)$v2['prelims_amount'] + $sums2['DISTRIBUTED'] + $sums2['PRELIMS_LINE'];
ok(abs((float)$v2['sum_price'] - $expectedSell) < 1, "sum_price = $expectedSell (got {$v2['sum_price']})");

/* ── 8. INTERNAL_ONLY eats margin (true < client) ──────────────────── */
h('8. INTERNAL_ONLY overhead eats margin (true < client margin)');
$sp = (float)$v2['sum_price'];
$sc = (float)$v2['sum_cost'];
$so = (float)$v2['sum_overhead'];
$directCost = $sc - $so;
$trueMargin   = $sp > 0 ? (($sp - $sc) / $sp) * 100 : 0;
$clientMargin = $sp > 0 ? (($sp - $directCost) / $sp) * 100 : 0;
p("true={$trueMargin}% · client={$clientMargin}%");
ok($trueMargin < $clientMargin, 'true margin < client margin (overheads eat true margin)');

/* ── 9. Clone version carries overheads ────────────────────────────── */
h('9. Clone version carries overheads');
$db->insertUpdate("UPDATE tender_pricing_versions SET status='SUBMITTED' WHERE c0d = :id", [':id' => $vid]);
$vidClone = $tp->createVersion($tenderId, $vid, $uid, $unm);
$clonedOh = $pvo->listFor($vidClone);
ok(count($clonedOh) === count($pvo->listFor($vid)), 'cloned overhead count matches seed');
$origLabels   = array_column($pvo->listFor($vid),      'label');
$cloneLabels  = array_column($clonedOh,                'label');
sort($origLabels); sort($cloneLabels);
ok($origLabels === $cloneLabels, 'cloned overhead labels match seed exactly');

/* ── 10. updateVersionHeader accepts new fields (whitelist enforced) ─ */
h('10. updateVersionHeader accepts procurement_mode + client_boq_visibility');
$db->insertUpdate("UPDATE tender_pricing_versions SET status='DRAFT' WHERE c0d = :id", [':id' => $vidClone]);
$tp->updateVersionHeader($vidClone, ['procurement_mode' => 'IMPORT', 'client_boq_visibility' => 'LUMPSUM_ONLY'], $uid);
$vcU = $tp->findVersion($vidClone);
ok($vcU['procurement_mode'] === 'IMPORT',       'procurement_mode saved as IMPORT');
ok($vcU['client_boq_visibility'] === 'LUMPSUM_ONLY', 'client_boq_visibility saved as LUMPSUM_ONLY');
$tp->updateVersionHeader($vidClone, ['procurement_mode' => 'GARBAGE'], $uid);
$vcU2 = $tp->findVersion($vidClone);
ok($vcU2['procurement_mode'] === 'IMPORT', 'invalid procurement_mode falls back to previous');

/* ── 11. loadTemplate idempotent ───────────────────────────────────── */
h('11. loadTemplate idempotent (2nd call skips duplicates)');
$rT = $pvo->loadTemplate($vid, $tenderId, 'GOODS_LOCAL', $uid, $unm);
p("2nd GOODS_LOCAL: added={$rT['added']} skipped={$rT['skipped']}");
ok($rT['ok'] && $rT['added'] === 0 && $rT['skipped'] === 5, 'idempotent by label — nothing added, 5 skipped');

/* ── 12. PricingDefaults includes 8S fields ────────────────────────── */
h('12. PricingDefaults get/save includes 8S fields');
$g = $pd->get();
ok(isset($g['default_procurement_mode']),      'get exposes default_procurement_mode');
ok(isset($g['default_client_boq_visibility']), 'get exposes default_client_boq_visibility');
$pd->save([
    'default_markup_pct'            => 22,
    'default_contingency_pct'       => 2,
    'default_vat_pct'               => 18,
    'default_prelims_amount'        => 0,
    'default_currency'              => 'TZS',
    'default_procurement_mode'      => 'IMPORT',
    'default_client_boq_visibility' => 'LUMPSUM_ONLY',
], 302, 'Verify 8S');
$g2 = $pd->get();
ok($g2['default_procurement_mode']      === 'IMPORT',       'default_procurement_mode round-trips');
ok($g2['default_client_boq_visibility'] === 'LUMPSUM_ONLY', 'default_client_boq_visibility round-trips');
$pd->save(['default_markup_pct'=>22,'default_currency'=>'TZS','default_procurement_mode'=>'BOGUS','default_client_boq_visibility'=>'BOGUS'], 302, 'Verify');
$g3 = $pd->get();
ok($g3['default_procurement_mode']      === 'LOCAL',              'bogus procurement_mode falls back to LOCAL');
ok($g3['default_client_boq_visibility'] === 'ITEMS_AND_SECTIONS', 'bogus BOQ visibility falls back to ITEMS_AND_SECTIONS');

/* ── 13. Perms + controller + view ─────────────────────────────────── */
h('13. Perms + controller + view surface');
$permCount = (int)$db->mySelect("SELECT COUNT(DISTINCT user_group_id) n FROM user_groups_perms WHERE controller_allowed='Tenders' AND action_allowed='pricingOverheadsAddAj'", [])[0]['n'];
ok($permCount >= 3, "pricingOverheadsAddAj granted to $permCount groups");
$cBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'TendersController.php');
foreach (['pricingOverheadsAddAjAction','pricingOverheadsUpdateAjAction','pricingOverheadsRemoveAjAction','pricingOverheadsLoadTemplateAjAction'] as $a) {
    ok(strpos($cBody, "function $a") !== false, "TendersController::$a defined");
}
ok(strpos($cBody, "audience === 'client'") !== false, 'controller branches by ?audience=client');
ok(strpos($cBody, 'pricingXlsxClient')     !== false, 'pricingXlsxClient private method defined');
ok(strpos($cBody, 'pricingXlsxInternal')   !== false, 'pricingXlsxInternal private method defined');
$vBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Tenders' . DS . 'view.php');
ok(strpos($vBody, 'Overhead pool')                   !== false, 'overhead pool card present in view');
ok(strpos($vBody, 'pricingOverheadsAddAj')           !== false, 'view posts to pricingOverheadsAddAj');
ok(strpos($vBody, 'pricingOverheadsLoadTemplateAj')  !== false, 'view posts to pricingOverheadsLoadTemplateAj');
ok(strpos($vBody, 'Quick-load local')                !== false, 'quick-load local button present');
ok(strpos($vBody, 'Quick-load import')               !== false, 'quick-load import button present');
ok(strpos($vBody, 'headerDraft.procurement_mode')    !== false, 'procurement_mode bound in Vue');
ok(strpos($vBody, 'headerDraft.client_boq_visibility')!== false,'client_boq_visibility bound in Vue');
ok(strpos($vBody, 'audience=client')                 !== false, 'Client xlsx link present');
ok(strpos($vBody, 'audience=internal')               !== false, 'Internal xlsx link present');
ok(strpos($vBody, 'clientMarginPct')                 !== false, 'clientMarginPct computed present');
$pdBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'PricingDefaults' . DS . 'index.php');
ok(strpos($pdBody, 'pd_proc_mode')                   !== false, 'PricingDefaults form has procurement_mode field');
ok(strpos($pdBody, 'pd_boq_vis')                     !== false, 'PricingDefaults form has BOQ visibility field');

/* ── Z. Restore ────────────────────────────────────────────────────── */
h('Z. Restore');
$pd->save([
    'default_markup_pct'            => $defSnap['default_markup_pct'],
    'default_contingency_pct'       => $defSnap['default_contingency_pct'],
    'default_vat_pct'               => $defSnap['default_vat_pct'],
    'default_prelims_amount'        => $defSnap['default_prelims_amount'],
    'default_currency'              => $defSnap['default_currency'],
    'default_procurement_mode'      => $defSnap['default_procurement_mode']      ?? 'LOCAL',
    'default_client_boq_visibility' => $defSnap['default_client_boq_visibility'] ?? 'ITEMS_AND_SECTIONS',
    'notes'                         => $defSnap['notes'] ?? '',
], (int)($defSnap['edited_by'] ?? 0), (string)($defSnap['edited_by_name'] ?? ''));
$db->insertUpdate("UPDATE tender_pricing_versions SET deleted=1 WHERE c0d IN (:v1, :v2)", [':v1' => $vid, ':v2' => $vidClone]);
if ($restoreDraftId > 0) $db->insertUpdate("UPDATE tender_pricing_versions SET status='DRAFT' WHERE c0d = :id", [':id' => $restoreDraftId]);
p('defaults + tender state restored · verify versions soft-deleted');

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