<?php
/**
 * Slice 8j-DirectAwardHardening · server guard + chip + alerts probe.
 *
 *  1) Projects::create refuses DIRECT_AWARD with empty justification
 *  2) Projects::create refuses DIRECT_AWARD with < 20 chars
 *  3) Projects::create refuses DIRECT_AWARD with placeholder ("tbc", "n/a", ".", …)
 *  4) Projects::create accepts DIRECT_AWARD with real justification
 *  5) WorksAlerts::directAwardUnjustified() picks up thin-justification legacy rows
 *  6) Real-justification project does NOT trigger the alert
 *  7) Chip surface + alert category chip wired in views
 *  8) Full 8c → 8j regression stays green
 */
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 (['Projects.php','WorksAlerts.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();
$pm = new Projects();
$wa = new WorksAlerts();

$crmId = 101; $crmNm = 'Verify CRM';

/* ── 0. Purge prior 8j artefacts ───────────────────────────────────── */
h('0. Purge prior 8j artefacts');
$old = $db->mySelect("SELECT c0d FROM c_projects WHERE project_name LIKE '8J-VERIFY-%'", []) ?: [];
foreach ($old as $o) $db->insertUpdate("DELETE FROM c_projects WHERE c0d = :id", [':id' => (int)$o['c0d']]);
p("purged " . count($old) . " prior 8j projects");

$baseAlerts = $wa->directAwardUnjustified();
p("baseline DIRECT_AWARD_UNJUSTIFIED alerts: " . count($baseAlerts));

/* ── 1. Empty justification rejected ───────────────────────────────── */
h('1. Empty justification rejected');
$r1 = $pm->create([
    'project_name' => '8J-VERIFY-empty ' . date('His'),
    'project_type' => 'goods',
    'project_customer_id' => 1,
    'origin' => 'DIRECT_AWARD',
    'direct_award_justification' => '',
], $crmId, $crmNm);
ok(!$r1['ok'] && stripos($r1['msg'], 'required') !== false, 'empty refused');

/* ── 2. Short justification rejected ───────────────────────────────── */
h('2. Short justification rejected');
$r2 = $pm->create([
    'project_name' => '8J-VERIFY-short ' . date('His'),
    'project_type' => 'goods',
    'project_customer_id' => 1,
    'origin' => 'DIRECT_AWARD',
    'direct_award_justification' => 'just do it',
], $crmId, $crmNm);
ok(!$r2['ok'] && stripos($r2['msg'], 'too short') !== false, 'short (< 20 chars) refused');

/* ── 3. Placeholder justification rejected ─────────────────────────── */
h('3. Placeholder justification rejected');
// Placeholder detection runs BEFORE length check, so bare "tbc" trips it (not "too short").
$placeholderCases = ['tbc', 'tba', 'test', 'n/a', 'todo', '.', '?', 'later', 'pending'];
foreach ($placeholderCases as $ph) {
    $rp = $pm->create([
        'project_name' => '8J-VERIFY-ph-' . $ph . ' ' . date('His'),
        'project_type' => 'goods',
        'project_customer_id' => 1,
        'origin' => 'DIRECT_AWARD',
        'direct_award_justification' => $ph,
    ], $crmId, $crmNm);
    ok(!$rp['ok'] && stripos($rp['msg'], 'placeholder') !== false, "placeholder '$ph' refused");
}

/* ── 4. Real justification accepted ─────────────────────────────────── */
h('4. Real justification accepted');
$goodJust = 'Direct award from Ministry of Health procurement director on 2026-08-01. Pre-agreed unit prices per Q3 framework agreement §4.2. Reference letter MOH/PROC/2026/0187.';
$rGood = $pm->create([
    'project_name' => '8J-VERIFY-good ' . date('His'),
    'project_type' => 'goods',
    'project_customer_id' => 1,
    'origin' => 'DIRECT_AWARD',
    'direct_award_justification' => $goodJust,
    'currency' => 'TZS',
    'contract_value' => 10000000,
], $crmId, $crmNm);
ok($rGood['ok'], 'real justification accepted: ' . ($rGood['msg'] ?? ''));
$goodId = (int)$rGood['id'];

/* ── 5. Alerts probe picks up a legacy thin row ────────────────────── */
h('5. Alert probe picks up legacy thin-justification row');
// Insert one BYPASSING the model guard (simulates legacy data from before Slice 8j)
$fbLeg = $db->insertUpdate(
    "INSERT INTO c_projects (project_no, project_name, project_type, project_customer, project_customer_id,
                             crm_status, migrated_flag, origin, currency, contract_value, default_margin_pct,
                             direct_award_justification)
     VALUES (:no, :nm, 'goods', 'TTCL Ltd', 1,
             'PENDING', 0, 'DIRECT_AWARD', 'TZS', 5000000, 25.00, 'tbc')",
    [
        ':no' => '8J-LEG-' . date('His'),
        ':nm' => '8J-VERIFY-legacy-thin ' . date('His'),
    ]
);
$legId = (int)$fbLeg['last_id'];

$now = $wa->directAwardUnjustified();
$found = null;
foreach ($now as $a) if ((int)$a['project_id'] === $legId) { $found = $a; break; }
ok($found !== null, 'legacy thin row appears in alerts');
ok(is_array($found) && $found['category'] === 'DIRECT_AWARD_UNJUSTIFIED', 'category = DIRECT_AWARD_UNJUSTIFIED');
ok(is_array($found) && $found['severity'] === 'HIGH', 'severity = HIGH');
ok(is_array($found) && stripos((string)$found['summary'], 'placeholder') !== false, 'summary mentions placeholder');

/* ── 6. Real-justification project does NOT trigger alert ──────────── */
h('6. Real-justification project skipped by alert probe');
$nowGood = $wa->directAwardUnjustified();
$foundGood = false;
foreach ($nowGood as $a) if ((int)$a['project_id'] === $goodId) { $foundGood = true; break; }
ok(!$foundGood, 'real-justification project not flagged');

/* ── 7. Wiring surface ─────────────────────────────────────────────── */
h('7. Wiring surface');
$pmBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'models' . DS . 'Projects.php');
ok(strpos($pmBody, 'too short') !== false, 'Projects::create has too-short guard');
ok(strpos($pmBody, 'placeholder') !== false, 'Projects::create has placeholder guard');
$waBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'models' . DS . 'WorksAlerts.php');
ok(strpos($waBody, 'directAwardUnjustified')  !== false, 'WorksAlerts has directAwardUnjustified probe');
ok(strpos($waBody, 'DIRECT_AWARD_UNJUSTIFIED') !== false, 'WorksAlerts emits DIRECT_AWARD_UNJUSTIFIED category');
$vBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'Projects' . DS . 'view.php');
ok(strpos($vBody, 'Direct Award · commercial justification on file') !== false, 'Projects/view has direct-award chip');
$aBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksAlerts' . DS . 'index.php');
ok(strpos($aBody, 'DIRECT_AWARD_UNJUSTIFIED') !== false, 'WorksAlerts view has chip mapping');
$rBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksProjectReport' . DS . 'view.php');
ok(strpos($rBody, 'DIRECT_AWARD_UNJUSTIFIED') !== false, 'Project report alert strip has chip mapping');

/* ── Z. Restore ────────────────────────────────────────────────────── */
h('Z. Restore');
$db->insertUpdate("DELETE FROM c_projects WHERE c0d IN (:g, :l) OR project_name LIKE '8J-VERIFY-%'",
    [':g' => $goodId, ':l' => $legId]);
p('anchor state restored');

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