<?php
/**
 * Slice 8Z-z-G smoke test — full callout lifecycle including findings, client
 * acceptance and close. Cleans up only rows this test creates.
 */
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';
foreach (['Callouts.php','ServiceContracts.php','SpareRequests.php'] as $m) {
    require_once ROOT . DS . 'app' . DS . 'models' . DS . $m;
}

function ok(bool $c, string $m): void { echo ($c ? "  ✓ " : "  ✗ ") . $m . "\n"; }
function h(string $t): void { echo "\n==== $t ====\n"; }

$db = new DB();
$co = new Callouts();
$sc = new ServiceContracts();
$sr = new SpareRequests();

$CID = 12;
$UID_PC = 6; $PN_PC = 'Projects Coordinator Test';
$UID_FS = 0;
$fsRow = $db->mySelect("SELECT u.c0d, u.full_name FROM users u JOIN user_groups g ON g.c0d = u.user_group_id WHERE g.user_group_name = 'Field Supervisor' AND u.status = 'ACTIVE' AND u.deleted = 0 LIMIT 1", []);
$UID_FS = !empty($fsRow) ? (int)$fsRow[0]['c0d'] : $UID_PC;
$PN_FS  = !empty($fsRow) ? (string)$fsRow[0]['full_name'] : $PN_PC;

$sites = $sc->sitesFor($CID);
if (count($sites) < 1) { echo "✗ seed sites first (seed_vodacom_sites.php)\n"; exit(1); }
$siteId = (int)$sites[0]['c0d'];

/* Cleanup only test rows */
$db->insertUpdate("DELETE FROM spare_request_items WHERE request_id IN (SELECT c0d FROM spare_requests WHERE reason LIKE 'SMOKE-G%')", []);
$db->insertUpdate("DELETE FROM spare_requests WHERE reason LIKE 'SMOKE-G%'", []);
$db->insertUpdate("DELETE FROM callouts WHERE description LIKE 'SMOKE-G%'", []);

/* ── 1. Raise callout + dispatch + arrive ───────────────────────── */
h('1. Raise → dispatch → arrive');
$catId = (int)$db->mySelect("SELECT c0d FROM callout_categories WHERE code = 'URGENT'", [])[0]['c0d'];
$coId = $co->saveIntake([
    'contract_id' => $CID, 'category_id' => $catId, 'site_id' => $siteId,
    'reported_by_name' => 'Test PC', 'description' => 'SMOKE-G · AVR fault reported',
], $UID_PC, $PN_PC);
ok($coId > 0, "callout raised id=$coId");

$co->dispatch($coId, $UID_FS, $PN_FS);
$after = $co->find($coId);
ok($after['status'] === 'DISPATCHED', 'status = DISPATCHED');

$co->arrive($coId);
$after = $co->find($coId);
ok($after['status'] === 'ON_SITE', 'status = ON_SITE');
ok(!empty($after['arrived_at']), 'arrived_at stamped');

/* ── 2. Mid-flow save findings (no status change) ───────────────── */
h('2. saveFindings mid-flow');
$co->saveFindings($coId, [
    'investigation_notes' => 'AVR module smells burnt · no output on load test',
    'root_cause'          => 'Voltage surge from grid switch · surge protector under-rated',
    'snags'               => 'Air filter also worn · client to procure larger surge protector',
]);
$after = $co->find($coId);
ok($after['status'] === 'ON_SITE', 'status still ON_SITE after saveFindings');
ok(stripos((string)$after['investigation_notes'], 'AVR module smells burnt') !== false, 'investigation_notes persisted');
ok(stripos((string)$after['root_cause'], 'surge protector under-rated') !== false, 'root_cause persisted');
ok(stripos((string)$after['snags'], 'Air filter') !== false, 'snags persisted');

/* Cannot edit findings after CLOSED. Test the guard on RESOLVED path a bit later. */

/* ── 3. Resolve carries findings forward + accepts overrides ────── */
h('3. Resolve · overrides findings + resolution fields');
$co->resolve($coId, [
    'resolution_description' => 'Replaced AVR module Cummins P/N ABC · load-tested 15min stable',
    'investigation_notes'    => 'AVR module smells burnt · no output on load test · confirmed with meter',
    'root_cause'             => 'Voltage surge from grid switch · surge protector under-rated',
    'snags'                  => 'Air filter also worn · client to procure larger surge protector',
    'labour_hrs'             => 2.5,
    'mileage_km'             => 42,
]);
$after = $co->find($coId);
ok($after['status'] === 'RESOLVED', 'status = RESOLVED');
ok(stripos((string)$after['investigation_notes'], 'confirmed with meter') !== false, 'resolve() updated investigation_notes');
ok((float)$after['labour_hrs'] == 2.5, 'labour_hrs = 2.5');
ok(!empty($after['resolved_at']), 'resolved_at stamped');

/* ── 4. Guard: findings edit blocked after CLIENT_ACCEPTED ──────── */
h('4. Guards');
$co->clientAccept($coId, 'John Mwanza · client', 'uploads/sig.png');
$after = $co->find($coId);
ok($after['status'] === 'CLIENT_ACCEPTED', 'status = CLIENT_ACCEPTED');
ok($after['client_accepted_by_name'] === 'John Mwanza · client', 'client_accepted_by_name captured');

$blocked = false;
try { $co->saveFindings($coId, ['investigation_notes' => 'edit attempt']); }
catch (\Throwable $e) { $blocked = strpos($e->getMessage(), 'Cannot edit findings') !== false; }
ok($blocked, 'saveFindings blocked after CLIENT_ACCEPTED');

/* ── 5. Close (terminal) ────────────────────────────────────────── */
h('5. Close');
$co->close($coId, $UID_PC, $PN_PC);
$after = $co->find($coId);
ok($after['status'] === 'CLOSED', 'status = CLOSED');
ok(!empty($after['closed_at']), 'closed_at stamped');
ok($after['closed_by_name'] === $PN_PC, 'closed_by_name captured');

/* ── 6. Guard: re-close blocked ─────────────────────────────────── */
$reBlocked = false;
try { $co->close($coId, $UID_PC, $PN_PC); }
catch (\Throwable $e) { $reBlocked = strpos($e->getMessage(), 'Only RESOLVED or CLIENT_ACCEPTED') !== false; }
ok($reBlocked, 'second close() rejected');

/* ── 7. Linked spare requests — 8Z-z-D bridge ───────────────────── */
h('7. linkedSpareRequests()');
$srR = $sr->save([
    'contract_id' => $CID, 'site_id' => $siteId, 'callout_id' => $coId,
    'urgency' => 'URGENT', 'who_pays' => 'CLIENT',
    'reason' => 'SMOKE-G · From callout smoke',
], $UID_PC);
$sr->itemSave((int)$srR['id'], ['part_name_free' => 'AVR module', 'qty' => 1, 'uom' => 'ea', 'unit_cost_estimate' => 250000]);

$links = $co->linkedSpareRequests($coId);
ok(count($links) === 1, 'linkedSpareRequests returns 1');
ok($links[0]['request_no'] === $srR['request_no'], 'request_no matches');
ok((int)$links[0]['item_count'] === 1, 'item_count = 1');

/* Cleanup */
$db->insertUpdate("DELETE FROM spare_request_items WHERE request_id IN (SELECT c0d FROM spare_requests WHERE reason LIKE 'SMOKE-G%')", []);
$db->insertUpdate("DELETE FROM spare_requests WHERE reason LIKE 'SMOKE-G%'", []);
$db->insertUpdate("DELETE FROM callouts WHERE description LIKE 'SMOKE-G%'", []);

echo "\n=== DONE (state restored) ===\n";
