<?php
/**
 * Slice 8S · Distribution addendum · pro-rata math for DISTRIBUTED overheads.
 *
 *  1) Build a version with 2 item lines (unequal line_price) + one DISTRIBUTED overhead
 *  2) Compute expected pro-rata shares and prove they hold (sum of shares == pool, no rounding drift beyond 1 unit)
 *  3) sum_price = raw items + prelims + distributed_pool + prelims_line_pool
 *  4) Client-facing line prices (raw + share) sum to (raw_line_total + distributed_pool) — invariant
 *  5) Vue-facing methods surface (clientLinePrice / clientUnitPrice / distributedTotal / directLineTotal)
 *  6) Controller: pricingXlsxClient carries the distribution loop
 */
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 (['TenderPricing.php','PricingVersionOverheads.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();
$uid = 401; $unm = 'Verify 8S-dist';

/* ── 0. Anchor a goods tender + prepare a clean DRAFT ──────────────── */
h('0. Anchor');
$r = $db->mySelect("SELECT c0d FROM tender_tracker WHERE deleted=0 AND project_type='goods' ORDER BY c0d DESC LIMIT 1", []);
if (empty($r)) boom('no goods tender');
$tenderId = (int)$r[0]['c0d'];
$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);
// Set predictable settings + wipe auto-loaded template overheads for clean math
$tp->updateVersionHeader($vid, [
    'vat_pct'=>18,'contingency_pct'=>0,'prelims_amount'=>0,'default_markup_pct'=>0,
    'pricing_mode'=>'PER_ITEM','procurement_mode'=>'LOCAL','client_boq_visibility'=>'ITEMS_AND_SECTIONS',
], $uid);
$db->insertUpdate("UPDATE pricing_version_overheads SET deleted=1 WHERE version_id = :v", [':v' => $vid]);

/* ── 1. Two lines with unequal line_price ──────────────────────────── */
h('1. Add 2 lines with unequal line_price');
$secId = $tp->addSection($vid, $tenderId, 'Buyer items', '', $uid);
// A: 10 × 100k, no markup → line_price = 1,000,000
// B: 20 × 200k, no markup → line_price = 4,000,000
$tp->addLine($secId, ['item_ref'=>'A','description'=>'A','qty'=>10,'unit_cost'=>100000,'markup_pct'=>0], $uid);
$tp->addLine($secId, ['item_ref'=>'B','description'=>'B','qty'=>20,'unit_cost'=>200000,'markup_pct'=>0], $uid);
$lines = $tp->linesFor($secId);
$sumLine = (float)$lines[0]['line_price'] + (float)$lines[1]['line_price'];
p("line_price A={$lines[0]['line_price']} B={$lines[1]['line_price']} sum=$sumLine");
ok(abs($sumLine - 5000000) < 0.01, 'raw line total = 5,000,000');

/* ── 2. Add 500,000 DISTRIBUTED overhead ──────────────────────────── */
h('2. Add distributed overhead 500,000');
$pvo->add($vid, $tenderId, ['category'=>'TRANSPORT','label'=>'Local delivery','amount'=>500000,'display_mode'=>'DISTRIBUTED'], $uid, $unm);
$tp->rollupVersion($vid);
$v = $tp->findVersion($vid);
p("sum_cost={$v['sum_cost']} sum_overhead={$v['sum_overhead']} sum_price={$v['sum_price']}");
ok(abs((float)$v['sum_price'] - 5500000) < 0.01, 'sum_price = raw_lines 5,000,000 + distributed 500,000 = 5,500,000');
ok(abs((float)$v['sum_overhead'] - 500000) < 0.01, 'sum_overhead = 500,000');

/* ── 3. Pro-rata math: A gets 100k, B gets 400k ───────────────────── */
h('3. Pro-rata distribution math');
$distPool = 500000.0;
$lineTotal = $sumLine;
$shareA = (1000000.0 / $lineTotal) * $distPool;
$shareB = (4000000.0 / $lineTotal) * $distPool;
$clientA = 1000000.0 + $shareA;
$clientB = 4000000.0 + $shareB;
p("A share=$shareA (client line={$clientA}) · B share=$shareB (client line={$clientB})");
ok(abs($shareA - 100000) < 0.01,  'A share = 100,000 (20% of pool)');
ok(abs($shareB - 400000) < 0.01,  'B share = 400,000 (80% of pool)');
ok(abs(($shareA + $shareB) - $distPool) < 0.01, 'shares sum to distributed pool (no drift)');
ok(abs(($clientA + $clientB) - ($lineTotal + $distPool)) < 0.01, 'client line totals sum to raw_total + pool');

/* ── 4. Simulate controller distribution loop (mirrors pricingXlsxClient) ─── */
h('4. Controller distribution loop matches expected');
$distributedPool = 0.0;
foreach ($pvo->listFor($vid) as $oh) if ($oh['display_mode'] === 'DISTRIBUTED') $distributedPool += (float)$oh['amount'];
$directLineTotal = 0.0;
foreach ($tp->sectionsFor($vid) as $s) foreach ($tp->linesFor((int)$s['c0d']) as $ln) $directLineTotal += (float)$ln['line_price'];
$shouldDistribute = ($distributedPool > 0 && $directLineTotal > 0);
ok($shouldDistribute, 'shouldDistribute = true');
$sumClientLines = 0.0;
foreach ($tp->sectionsFor($vid) as $s) {
    foreach ($tp->linesFor((int)$s['c0d']) as $ln) {
        $raw = (float)$ln['line_price']; $qty = (float)$ln['qty'];
        $share = ($raw / $directLineTotal) * $distributedPool;
        $clientLine = round($raw + $share, 2);
        $clientUnit = $qty > 0 ? round($clientLine / $qty, 4) : (float)$ln['unit_price'];
        $sumClientLines += $clientLine;
    }
}
p("sum of distributed client-line prices = $sumClientLines");
ok(abs($sumClientLines - ($directLineTotal + $distributedPool)) < 0.05, "sum matches raw_total + pool ({$directLineTotal} + {$distributedPool})");

/* ── 5. Adding INTERNAL_ONLY does NOT bump client prices ───────────── */
h('5. INTERNAL_ONLY overhead does NOT enter distribution');
$pvo->add($vid, $tenderId, ['category'=>'ADMIN_OVERHEAD','label'=>'PM','amount'=>250000,'display_mode'=>'INTERNAL_ONLY'], $uid, $unm);
$tp->rollupVersion($vid);
$distPool2 = 0.0;
foreach ($pvo->listFor($vid) as $oh) if ($oh['display_mode'] === 'DISTRIBUTED') $distPool2 += (float)$oh['amount'];
ok(abs($distPool2 - 500000) < 0.01, 'distributed pool still 500,000 (internal-only excluded)');

/* ── 6. View surface: Vue methods + template ───────────────────────── */
h('6. View surface');
$vBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Tenders' . DS . 'view.php');
ok(strpos($vBody, 'distributedTotal: function') !== false, 'distributedTotal computed defined');
ok(strpos($vBody, 'directLineTotal: function')  !== false, 'directLineTotal computed defined');
ok(strpos($vBody, 'clientLinePrice: function')  !== false, 'clientLinePrice method defined');
ok(strpos($vBody, 'clientUnitPrice: function')  !== false, 'clientUnitPrice method defined');
ok(strpos($vBody, 'Distribution active')        !== false, 'distribution active hint present');
ok(strpos($vBody, 'clientLinePrice(ln)')        !== false, 'template invokes clientLinePrice per row');
ok(strpos($vBody, 'clientUnitPrice(ln)')        !== false, 'template invokes clientUnitPrice per row');

/* ── 7. Controller surface ─────────────────────────────────────────── */
h('7. Controller surface');
$cBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'TendersController.php');
ok(strpos($cBody, '$distributedPool') !== false && strpos($cBody, '$directLineTotal') !== false, 'pricingXlsxClient has distribution locals');
ok(strpos($cBody, '$shouldDistribute') !== false, 'shouldDistribute guard present');
ok(strpos($cBody, '$clientLinePrice') !== false, 'clientLinePrice computed per row');

/* ── Z. Restore ────────────────────────────────────────────────────── */
h('Z. Restore');
$db->insertUpdate("UPDATE tender_pricing_versions SET deleted=1 WHERE c0d = :id", [':id' => $vid]);
if ($restoreDraftId > 0) $db->insertUpdate("UPDATE tender_pricing_versions SET status='DRAFT' WHERE c0d = :id", [':id' => $restoreDraftId]);
p('verify version soft-deleted · previous draft restored');

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