<?php
/**
 * Slice 8c-Fulfil · Wire Works CRs to fulfilment · end-to-end.
 *
 * Assumes Slice 8c-Spine verify has already produced an ACTIVE CR (v2 on phase 1 of
 * project 17). We source LPOs from that CR's MATERIALS pillar, record deliveries against
 * the LPO lines, and verify progressPerPillar closes the loop.
 *
 *   1) Find an ACTIVE CR with a MATERIALS line that has remaining budget. If v2 has no
 *      MATERIALS, we hydrate one so the surface is exercised.
 *   2) sourceableLines() returns MATERIALS with remaining_amount = line_total.
 *   3) draftLpoFromCrLines() creates a supplier LPO stamped with source_cr_* on header
 *      and every item. Verify source_cr_line_id, source_cr_pillar populated.
 *   4) sourceableLines() now shows sourced_amount > 0 on the picked line.
 *   5) recordIssue() creates a works_phase_items_issued row; lpo_items.qty_received bumps.
 *   6) Over-issue guard rejects extra qty.
 *   7) progressPerPillar() returns coherent {budget, sourced, delivered, *_pct}.
 *   8) Perms + view file + schema surface checks.
 */
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 (['WorksCrs.php','WorksCrFulfilment.php','WorksPhases.php','Lpos.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();
$wc = new WorksCrs();
$wf = new WorksCrFulfilment();

/* ── 0. Schema surface ─────────────────────────────────────────────── */
h('0. Schema surface added by slice');
$colsLpos = array_column(
    $db->mySelect("SHOW COLUMNS FROM lpos", []) ?: [], 'Field'
);
$colsItems = array_column(
    $db->mySelect("SHOW COLUMNS FROM lpo_items", []) ?: [], 'Field'
);
foreach (['source_cr_id','source_cr_pillar','source_phase_id'] as $c) {
    ok(in_array($c, $colsLpos, true), "lpos.$c exists");
}
foreach (['source_cr_pillar','source_cr_line_id'] as $c) {
    ok(in_array($c, $colsItems, true), "lpo_items.$c exists");
}
$tabl = $db->mySelect("SHOW TABLES LIKE 'works_phase_items_issued'", []);
ok(!empty($tabl), 'works_phase_items_issued table exists');

/* ── 1. Anchor an ACTIVE CR to test against ─────────────────────────── */
h('1. Anchor ACTIVE Works CR');
$anchor = $db->mySelect(
    "SELECT c0d, project_id, phase_id, status FROM works_crs WHERE status='ACTIVE' AND deleted=0 ORDER BY c0d DESC LIMIT 1",
    []
);
if (empty($anchor)) boom("No ACTIVE Works CR found. Run scripts/slice8cSpine_e2e_verify.php first.");
$crId    = (int)$anchor[0]['c0d'];
$phaseId = (int)$anchor[0]['phase_id'];
$prjId   = (int)$anchor[0]['project_id'];
p("Using CR c0d=$crId · project=$prjId · phase=$phaseId · status={$anchor[0]['status']}");

$cr = $wc->findById($crId);
$matBudgetInitial = (float)$cr['total_materials'];
p("MATERIALS budget on CR: " . number_format($matBudgetInitial));

/* ── 1b. Purge prior verify artifacts so reruns work cleanly ─────────── */
h('1b. Purge prior verify artifacts on this CR');
// Prior LPOs sourced from this CR
$oldLpos = $db->mySelect("SELECT c0d FROM lpos WHERE source_cr_id = :c", [':c' => $crId]) ?: [];
foreach ($oldLpos as $ol) {
    $db->insertUpdate("DELETE FROM lpo_items WHERE lpo_id = :l", [':l' => (int)$ol['c0d']]);
    $db->insertUpdate("DELETE FROM lpos      WHERE c0d   = :l", [':l' => (int)$ol['c0d']]);
}
$db->insertUpdate("DELETE FROM works_phase_items_issued WHERE cr_id = :c", [':c' => $crId]);
// Verify-injected MATERIALS lines and their contribution to pillar totals
$oldLines = $db->mySelect(
    "SELECT c0d, total FROM works_cr_materials WHERE cr_id = :c AND description LIKE 'Diesel (verify %'",
    [':c' => $crId]
) ?: [];
$sumOld = 0;
foreach ($oldLines as $ol) $sumOld += (float)$ol['total'];
if ($sumOld > 0) {
    $db->insertUpdate(
        "UPDATE works_crs SET total_materials = total_materials - :s, grand_total = grand_total - :s WHERE c0d = :c",
        [':s' => $sumOld, ':c' => $crId]
    );
    $db->insertUpdate("DELETE FROM works_cr_materials WHERE cr_id = :c AND description LIKE 'Diesel (verify %'", [':c' => $crId]);
}
p("Purged " . count($oldLpos) . " lpos · " . count($oldLines) . " verify materials lines");

/* ── 2. Inject a fresh MATERIALS line so the script is repeatable ─────── */
h('2. Inject a fresh MATERIALS line on the CR');
$desc = 'Diesel (verify ' . date('His') . ')';
$db->insertUpdate(
    "INSERT INTO works_cr_materials (cr_id, line_no, description, qty, unit, unit_cost, total, created_on)
     VALUES (:c, 1, :d, 4000, 'L', 3000, 12000000, NOW())",
    [':c' => $crId, ':d' => $desc]
);
$db->insertUpdate(
    "UPDATE works_crs
        SET total_materials = total_materials + 12000000,
            grand_total     = grand_total     + 12000000
      WHERE c0d = :c",
    [':c' => $crId]
);
$cr = $wc->findById($crId);
$matBudgetInitial = (float)$cr['total_materials'];
p("MATERIALS pillar total after inject: " . number_format($matBudgetInitial));

$materials = $wf->sourceableLines($crId)['MATERIALS'];
ok(count($materials) >= 1, 'sourceableLines(MATERIALS) returns >= 1 line');
// Grab THE line we just inserted (freshest, matches our description)
$firstLine = null;
foreach ($materials as $m) if ($m['description'] === $desc) { $firstLine = $m; break; }
if (!$firstLine) boom('freshly-inserted MATERIALS line not returned by sourceableLines');
p("Line #{$firstLine['line_id']} · {$firstLine['description']} · budget=" . number_format((float)$firstLine['line_total']));
ok((float)$firstLine['sourced_amount'] === 0.0 || (float)$firstLine['sourced_amount'] === 0, 'sourced_amount starts at 0');
ok(abs((float)$firstLine['remaining_amount'] - (float)$firstLine['line_total']) < 0.01, 'remaining_amount = line_total initially');

/* ── 3. Draft an LPO from the picked line (under-source) ─────────────── */
h('3. Draft supplier LPO from MATERIALS · under-source');
$pickQty  = 3000;
$pickPrice = 2950;   // 3000 L @ 2,950 = 8,850,000 (line budget 12,000,000)
$res = $wf->draftLpoFromCrLines(
    $crId, 'MATERIALS',
    ['vendor_id' => 1, 'vendor_name' => 'Furniture Direct Ltd', 'currency' => 'TZS'],
    [ ['line_id' => (int)$firstLine['line_id'], 'qty' => $pickQty, 'unit_price' => $pickPrice] ],
    100, 'Verify PC'
);
if (!$res['ok']) boom("draftLpoFromCrLines failed: " . ($res['msg'] ?? '?'));
$lpoId = (int)$res['id'];
p("LPO created: {$res['lpo_no']} (c0d=$lpoId)");

$lpoRow = $db->mySelect("SELECT * FROM lpos WHERE c0d = :id", [':id' => $lpoId]);
ok(!empty($lpoRow), 'lpos row exists');
ok((int)$lpoRow[0]['source_cr_id']    === $crId,   'lpos.source_cr_id stamped');
ok((string)$lpoRow[0]['source_cr_pillar'] === 'MATERIALS', 'lpos.source_cr_pillar stamped');
ok((int)$lpoRow[0]['source_phase_id'] === $phaseId, 'lpos.source_phase_id stamped');
ok((string)$lpoRow[0]['source_module'] === 'works', 'lpos.source_module = works');
ok(abs((float)$lpoRow[0]['amount'] - ($pickQty * $pickPrice)) < 0.01, 'lpos.amount = qty * price');

$liRows = $db->mySelect("SELECT * FROM lpo_items WHERE lpo_id = :l ORDER BY item_no", [':l' => $lpoId]);
ok(count($liRows) === 1, 'lpo_items row inserted');
$li = $liRows[0];
ok((string)$li['source_cr_pillar'] === 'MATERIALS', 'lpo_items.source_cr_pillar stamped');
ok((int)$li['source_cr_line_id']  === (int)$firstLine['line_id'], 'lpo_items.source_cr_line_id stamped');

/* ── 4. sourceableLines reflects sourced_amount now ─────────────────── */
h('4. sourceableLines reflects sourcing');
$after = $wf->sourceableLines($crId)['MATERIALS'];
$updated = null;
foreach ($after as $l) if ((int)$l['line_id'] === (int)$firstLine['line_id']) { $updated = $l; break; }
ok($updated !== null, 'line still in sourceable list');
ok(abs((float)$updated['sourced_amount'] - ($pickQty * $pickPrice)) < 0.01, "sourced_amount=" . number_format((float)$updated['sourced_amount']));
$expectRemaining = (float)$firstLine['line_total'] - ($pickQty * $pickPrice);
ok(abs((float)$updated['remaining_amount'] - $expectRemaining) < 0.01, "remaining_amount=" . number_format((float)$updated['remaining_amount']));

/* ── 5. Over-source guard on LPO drafting ─────────────────────────────── */
h('5. Over-source guard');
$over = $wf->draftLpoFromCrLines(
    $crId, 'MATERIALS',
    ['vendor_id' => 2, 'vendor_name' => 'Ergo Office Systems', 'currency' => 'TZS'],
    [ ['line_id' => (int)$firstLine['line_id'], 'qty' => 5000, 'unit_price' => 3000] ], // 15M > remaining 3.15M
    100, 'Verify PC'
);
ok(!$over['ok'] && stripos((string)$over['msg'], 'over-sourced') !== false, 'over-source LPO refused');

/* ── 6. recordIssue creates delivery row + bumps qty_received ────────── */
h('6. recordIssue on the LPO line');
$issueQty = 1500;   // partial delivery
$issueId = $wf->recordIssue([
    'lpo_item_id' => (int)$li['c0d'],
    'qty'         => $issueQty,
    'issued_on'   => date('Y-m-d'),
    'notes'       => 'Half-truck delivery',
], 100, 'Verify PC');
ok($issueId > 0, "works_phase_items_issued.c0d = $issueId");

$issRow = $db->mySelect("SELECT * FROM works_phase_items_issued WHERE c0d = :id", [':id' => $issueId]);
$iss = $issRow[0];
ok((int)$iss['cr_id']      === $crId,   'issued.cr_id stamped');
ok((string)$iss['cr_pillar'] === 'MATERIALS', 'issued.cr_pillar stamped');
ok((int)$iss['cr_line_id'] === (int)$firstLine['line_id'], 'issued.cr_line_id stamped');
ok((int)$iss['phase_id']   === $phaseId, 'issued.phase_id stamped');
ok(abs((float)$iss['total'] - ($issueQty * $pickPrice)) < 0.01, "issued.total=" . number_format((float)$iss['total']));

$liAfter = $db->mySelect("SELECT qty_received FROM lpo_items WHERE c0d = :id", [':id' => (int)$li['c0d']]);
ok(abs((float)$liAfter[0]['qty_received'] - $issueQty) < 0.01, "lpo_items.qty_received bumped to $issueQty");

/* ── 7. Over-issue guard ─────────────────────────────────────────────── */
h('7. Over-issue guard on recordIssue');
$threw = false;
try {
    $wf->recordIssue([
        'lpo_item_id' => (int)$li['c0d'],
        'qty'         => $pickQty, // would make cumulative = 1500 + 3000 = 4500 > 3000
        'issued_on'   => date('Y-m-d'),
    ], 100, 'Verify PC');
} catch (Throwable $e) {
    $threw = stripos($e->getMessage(), 'over-issue') !== false;
}
ok($threw, 'over-issue rejected');

/* ── 8. progressPerPillar closes the loop ─────────────────────────────── */
h('8. progressPerPillar');
$prg = $wf->progressPerPillar($crId);
$m   = $prg['MATERIALS'];
p(sprintf("MATERIALS budget=%s sourced=%s delivered=%s remaining=%s (sourced %.1f%% · delivered %.1f%%)",
    number_format($m['budget']), number_format($m['sourced']),
    number_format($m['delivered']), number_format($m['remaining']),
    $m['sourced_pct'], $m['delivered_pct']));
ok(abs($m['budget']    - $matBudgetInitial)         < 0.01, 'budget = CR MATERIALS total');
ok(abs($m['sourced']   - ($pickQty * $pickPrice))   < 0.01, 'sourced = LPO amount');
ok(abs($m['delivered'] - ($issueQty * $pickPrice))  < 0.01, 'delivered = issued.total');
ok($m['sourced_pct']   > 0 && $m['delivered_pct'] > 0,      'both pcts > 0');

/* ── 9. Perms + view surface ─────────────────────────────────────────── */
h('9. Perms + view surface');
foreach (['Projects Coordinator','Project Manager','Field Supervisor','Procurement','Administrator'] as $role) {
    $rr = $db->mySelect("SELECT c0d FROM user_groups WHERE user_group_name = :n", [':n' => $role]);
    $gid = (int)($rr[0]['c0d'] ?? 0);
    foreach (['fulfil','draftLpoAj','recordIssueAj'] as $act) {
        $r = $db->mySelect(
            "SELECT 1 FROM user_groups_perms WHERE user_group_id = :g AND controller_allowed = 'WorksCrs' AND action_allowed = :a LIMIT 1",
            [':g' => $gid, ':a' => $act]
        );
        ok(!empty($r), "$role · WorksCrs/$act");
    }
}
foreach (['Finance Manager','Accountant','Executive'] as $role) {
    $rr = $db->mySelect("SELECT c0d FROM user_groups WHERE user_group_name = :n", [':n' => $role]);
    $gid = (int)($rr[0]['c0d'] ?? 0);
    $r = $db->mySelect(
        "SELECT 1 FROM user_groups_perms WHERE user_group_id = :g AND controller_allowed = 'WorksCrs' AND action_allowed = 'fulfil' LIMIT 1",
        [':g' => $gid]
    );
    ok(!empty($r), "$role · WorksCrs/fulfil (read-only)");
}
ok(is_file(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksCrs' . DS . 'fulfil.php'), 'fulfil.php view exists');

// CR view exposes the Fulfil button when ACTIVE
$viewBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksCrs' . DS . 'view.php');
ok(strpos($viewBody, "WorksCrs/fulfil/") !== false, 'CR view links to WorksCrs/fulfil/');
ok(strpos($viewBody, "cr['status'] === 'ACTIVE'") !== false, "Fulfil button gated on cr.status === 'ACTIVE'");

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