<?php
/**
 * Slice 8M-PricingVue · Vue-reactive Pricing tab · reload-free CRUD +
 * "Seed pricing from tender items" + narrow Item ref column.
 *
 *  1) TenderPricing::seedFromItems copies tender_items → tender_pricing_lines
 *  2) Seed is idempotent by (item_ref, description) — duplicates skipped
 *  3) Seed uses version.default_markup_pct on new lines
 *  4) Seed refuses when tender has no items
 *  5) Seed refuses on non-DRAFT versions
 *  6) TenderPricing::snapshot() returns version + sections + lines_by_section
 *  7) Controller: pricingSeedFromItemsAjAction + pricingSnapshotAjAction present
 *  8) Perms: pricingSeedFromItemsAj (CRM/FM/Admin), pricingSnapshotAj (all pricing readers)
 *  9) View: Vue root #pv_app_root + all handlers migrated (no location.reload on core CRUD)
 * 10) Old jQuery pricing handlers (btn_pv_new / btn_pv_save / btn_add_section / .btn-del-section etc.) removed
 * 11) Item ref column narrowed (width:6rem in Vue template)
 * 12) hasTenderItems + tenderItemCount surfaced to the Vue root
 */
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 (['Tenders.php','TenderPricing.php','TenderItems.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();
$ti = new TenderItems();
$crmId = 122; $crmNm = 'Verify CRM 8M';

/* ── 0. Anchor an existing tender + snapshot state ─────────────────── */
h('0. Anchor tender + purge prior 8M items');
$anchor = $db->mySelect("SELECT c0d, project_type, tender_title FROM tender_tracker WHERE deleted=0 ORDER BY c0d ASC LIMIT 1", []);
if (empty($anchor)) boom('no tenders found');
$tenderId = (int)$anchor[0]['c0d'];
p("anchor tender #{$tenderId} · type={$anchor[0]['project_type']}");

$db->insertUpdate("DELETE FROM tender_items WHERE tender_id = :t AND description LIKE '8M-VERIFY%'", [':t' => $tenderId]);

/* Ensure the anchor has at least 3 items so we can seed from them */
$items = [
    ['description' => '8M-VERIFY item A', 'qty' => 10, 'uom' => 'ea', 'item_ref' => 'A-1'],
    ['description' => '8M-VERIFY item B', 'qty' =>  5, 'uom' => 'm',  'item_ref' => 'B-2'],
    ['description' => '8M-VERIFY item C', 'qty' =>  1, 'uom' => 'lot','item_ref' => 'C-3'],
];
foreach ($items as $r) $ti->add($tenderId, $r, $crmId, $crmNm);
p('3 verify tender items seeded');

/* ── 1. Create a fresh DRAFT pricing version to work against ───────── */
h('1. Create DRAFT pricing version');
// Retire any existing DRAFT (soft-delete-only paths — safe rollback below)
$existingDraft = $tp->activeDraft($tenderId);
$restoreDraftId = 0;
if ($existingDraft) {
    $restoreDraftId = (int)$existingDraft['c0d'];
    $db->insertUpdate("UPDATE tender_pricing_versions SET status='SUPERSEDED' WHERE c0d = :id", [':id' => $restoreDraftId]);
    p("existing draft #{$restoreDraftId} temporarily set SUPERSEDED so we can create a fresh one");
}
$vid = $tp->createVersion($tenderId, null, $crmId, $crmNm);
p("draft version #{$vid} created");
// Ensure default_markup_pct is > 0 so we can prove seed applies it
$tp->updateVersionHeader($vid, ['version_label' => 'v-8M', 'vat_pct' => 18, 'contingency_pct' => 0, 'prelims_amount' => 0, 'default_markup_pct' => 22.5], $crmId);
$vHdr = $tp->findVersion($vid);
ok((float)$vHdr['default_markup_pct'] === 22.5, 'default_markup_pct = 22.5');

/* ── 2. seedFromItems adds 3 lines with markup = 22.5 ──────────────── */
h('2. seedFromItems adds N lines with default markup');
$secBefore = count($tp->sectionsFor($vid));
$r1 = $tp->seedFromItems($vid, 0, $crmId);
ok($r1['ok'] && $r1['added'] === 3, "seed added 3 lines (got " . ($r1['added'] ?? '?') . ")");
$secAfter = count($tp->sectionsFor($vid));
ok($secAfter === $secBefore + 1, 'a default "Buyer\'s items" section was auto-created');
$secId = (int)$r1['section_id'];
$lines = $tp->linesFor($secId);
ok(count($lines) === 3, 'section has 3 lines');
$allDefaultMarkup = true;
foreach ($lines as $ln) if ((float)$ln['markup_pct'] !== 22.5) $allDefaultMarkup = false;
ok($allDefaultMarkup, 'every seeded line has markup_pct = version default (22.5)');
$refs = array_column($lines, 'item_ref');
ok(in_array('A-1', $refs, true) && in_array('B-2', $refs, true) && in_array('C-3', $refs, true), 'item_refs preserved from tender_items');

/* ── 3. seedFromItems idempotent (2nd run skips duplicates) ────────── */
h('3. seedFromItems is idempotent by (item_ref, description)');
$r2 = $tp->seedFromItems($vid, $secId, $crmId);
ok($r2['ok'] && $r2['added'] === 0 && $r2['skipped'] === 3, "2nd seed: added=0 skipped=3 (got added={$r2['added']} skipped={$r2['skipped']})");
$linesAfter = count($tp->linesFor($secId));
ok($linesAfter === 3, 'still 3 lines · no duplicates written');

/* ── 4. Add a partial-overlap item; only the new one is added ──────── */
h('4. Partial-overlap seed adds only the truly new item');
$ti->add($tenderId, ['description' => '8M-VERIFY item D', 'item_ref' => 'D-4', 'qty' => 2, 'uom' => 'kg'], $crmId, $crmNm);
$r3 = $tp->seedFromItems($vid, $secId, $crmId);
ok($r3['ok'] && $r3['added'] === 1 && $r3['skipped'] === 3, "seed: added=1 skipped=3 (got added={$r3['added']} skipped={$r3['skipped']})");

/* ── 5. seedFromItems refuses on non-DRAFT version ─────────────────── */
h('5. seedFromItems refuses on non-DRAFT version');
$db->insertUpdate("UPDATE tender_pricing_versions SET status='SUBMITTED' WHERE c0d = :id", [':id' => $vid]);
$threw = false; $why = '';
try { $tp->seedFromItems($vid, $secId, $crmId); } catch (Throwable $e) { $threw = true; $why = $e->getMessage(); }
ok($threw && stripos($why, 'draft') !== false, 'non-DRAFT throws "Only DRAFT…" (got: ' . $why . ')');
$db->insertUpdate("UPDATE tender_pricing_versions SET status='DRAFT' WHERE c0d = :id", [':id' => $vid]);

/* ── 6. seedFromItems refuses when tender has no items ─────────────── */
h('6. seedFromItems refuses when tender has no items');
$emptyTenderRows = $db->mySelect(
    "SELECT c0d FROM tender_tracker t
      WHERE deleted = 0
        AND NOT EXISTS (SELECT 1 FROM tender_items ti WHERE ti.tender_id = t.c0d AND ti.deleted = 0)
      LIMIT 1", []);
if (!empty($emptyTenderRows)) {
    $emptyTid = (int)$emptyTenderRows[0]['c0d'];
    // Temp draft on the empty tender
    $emptyDraft = $tp->activeDraft($emptyTid);
    if ($emptyDraft) $db->insertUpdate("UPDATE tender_pricing_versions SET status='SUPERSEDED' WHERE c0d = :id", [':id' => (int)$emptyDraft['c0d']]);
    $tmpVid = $tp->createVersion($emptyTid, null, $crmId, $crmNm);
    $rEmp = $tp->seedFromItems($tmpVid, 0, $crmId);
    ok(!$rEmp['ok'] && stripos($rEmp['msg'], 'no tender items') !== false, 'refused with "No tender items…" message');
    // Restore
    $db->insertUpdate("UPDATE tender_pricing_versions SET deleted=1 WHERE c0d = :id", [':id' => $tmpVid]);
    if ($emptyDraft) $db->insertUpdate("UPDATE tender_pricing_versions SET status='DRAFT' WHERE c0d = :id", [':id' => (int)$emptyDraft['c0d']]);
} else {
    p('(skip: no empty tenders available in DB)');
    ok(true, 'skipped — no empty tender to test against');
}

/* ── 7. snapshot() returns everything in one payload ───────────────── */
h('7. snapshot() single-payload');
$snap = $tp->snapshot($vid);
ok(is_array($snap) && !empty($snap['version']), 'version present');
ok(is_array($snap['sections']) && count($snap['sections']) >= 1, 'sections present');
ok(isset($snap['lines_by_section'][$secId]) && count($snap['lines_by_section'][$secId]) >= 4, 'lines_by_section keyed by section_id');
ok((int)$snap['version']['c0d'] === $vid, 'version c0d matches');

/* ── 8. Controller endpoints registered ────────────────────────────── */
h('8. Controller endpoints');
$cBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'TendersController.php');
ok(strpos($cBody, 'function pricingSeedFromItemsAjAction') !== false, 'pricingSeedFromItemsAjAction defined');
ok(strpos($cBody, 'function pricingSnapshotAjAction')     !== false, 'pricingSnapshotAjAction defined');

/* ── 9. Perms surface ──────────────────────────────────────────────── */
h('9. Perms');
$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 = 'Tenders' AND p.action_allowed IN ('pricingSnapshotAj','pricingSeedFromItemsAj')",
    []) ?: [];
$permSet = [];
foreach ($permRows as $r) $permSet[$r['user_group_name'] . '::' . $r['action_allowed']] = true;
ok(isset($permSet['CRM::pricingSnapshotAj']),                'CRM has pricingSnapshotAj');
ok(isset($permSet['Project Manager::pricingSnapshotAj']),    'PM has pricingSnapshotAj');
ok(isset($permSet['Finance Manager::pricingSnapshotAj']),    'FM has pricingSnapshotAj');
ok(isset($permSet['Administrator::pricingSnapshotAj']),      'Admin has pricingSnapshotAj');
ok(isset($permSet['CRM::pricingSeedFromItemsAj']),           'CRM has pricingSeedFromItemsAj');
ok(isset($permSet['Administrator::pricingSeedFromItemsAj']), 'Admin has pricingSeedFromItemsAj');

/* ── 10. View: Vue root migrated · no reloads on core CRUD ─────────── */
h('10. View surface');
$vBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Tenders' . DS . 'view.php');
ok(strpos($vBody, 'id="pv_app_root"') !== false, 'Vue root #pv_app_root present');
ok(strpos($vBody, 'Slice 8M · Pricing tab Vue root') !== false, 'Slice 8M block marker present');
ok(strpos($vBody, 'refreshSnapshot') !== false,  'refreshSnapshot() defined');
ok(strpos($vBody, 'seedFromItems')   !== false,  'seedFromItems() defined');
ok(strpos($vBody, 'pricingSeedFromItemsAj') !== false, 'view posts to pricingSeedFromItemsAj');
ok(strpos($vBody, 'pricingSnapshotAj')      !== false, 'view posts to pricingSnapshotAj');

/* Old jQuery pricing handlers must be gone (no more location.reload() on core CRUD) */
ok(strpos($vBody, "\$('#btn_pv_new').on('click'")   === false, 'legacy #btn_pv_new jQuery handler removed');
ok(strpos($vBody, "\$('#btn_pv_save').on('click'")  === false, 'legacy #btn_pv_save jQuery handler removed');
ok(strpos($vBody, "\$('#btn_add_section').on('click'") === false, 'legacy #btn_add_section handler removed');
ok(strpos($vBody, "'.btn-add-line'") === false,  'legacy .btn-add-line delegate removed');
ok(strpos($vBody, "'.btn-del-line'") === false,  'legacy .btn-del-line delegate removed');
ok(strpos($vBody, "'.btn-save-line'") === false, 'legacy .btn-save-line delegate removed');
ok(strpos($vBody, "'.btn-del-section'") === false, 'legacy .btn-del-section delegate removed');

/* Item ref column narrowed */
ok(strpos($vBody, 'style="width:6rem;color:#334155;">Item ref') !== false, 'Item ref column narrowed to width:6rem');

/* Vue root exposes canSeed, hasTenderItems, tenderItemCount */
ok(strpos($vBody, 'hasTenderItems') !== false,  'hasTenderItems flag surfaced');
ok(strpos($vBody, 'tenderItemCount') !== false, 'tenderItemCount surfaced');
ok(strpos($vBody, 'canSeed') !== false,         'canSeed gate present');

/* ── 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]);
$db->insertUpdate("DELETE FROM tender_items WHERE tender_id = :t AND description LIKE '8M-VERIFY%'", [':t' => $tenderId]);
p('anchor state restored · verify version soft-deleted');

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