<?php
/**
 * Slice 8b-5 · Callout Playbook + SLA e2e.
 *
 *   1) Create playbook "STD_TELCO" with 4 SLA rows (P1..P4).
 *   2) Add 3 escalation steps for P1 (immediate FS · +2h PM · +4h Exec).
 *   3) Add 2 escalation steps for P2 (+1h FS · +3h PM).
 *   4) Try creating a duplicate-code playbook → BLOCKED.
 *   5) Provision a Services project (via Slice 3/5 pipeline).
 *   6) Attach playbook v1 to project → project_callout_config row.
 *   7) Verify activeForProject() returns v1.
 *   8) Create second playbook "PREMIUM_TELCO"; attach to same project.
 *      Old row soft-deleted with detached_on set; new row is now active.
 *   9) attachmentHistory() returns 2 rows (old detached + new active).
 *  10) Setting playbook inactive then attempting to attach → BLOCKED.
 *  11) SLA guard: upsertSla with response > resolution → BLOCKED.
 *  12) SLA upsert idempotency: second upsert on same severity updates, no duplicate row.
 */
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 ([
    'ApprovalChain.php','Tenders.php','TenderPricing.php','ProjectStarter.php','Notifications.php',
    'CalloutPlaybooks.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) { echo "\nFATAL: $why\n"; exit(1); }

$db = new DB();
$ac = new ApprovalChain();
$tm = new Tenders();
$cp = new CalloutPlaybooks();

$adminId = 1;  $adminNm = 'Admin';
$uidCrm = 100; $unmCrm = 'CRM Test User';       $ugCrm  = 'CRM';
$uidFm  = 3;   $unmFm  = 'Finance Manager Test'; $ugFm  = 'Finance Manager';
$uidExec= 2;   $unmExec= 'Executive Test';      $ugExec = 'Executive';
$accId  = 4;   $accNm  = 'Accountant Test';
$procId = 8;   $procNm = 'Procurement Test';
$pmId   = 5;   $pmNm   = 'Project Manager Test';
$pcRow  = $db->mySelect("SELECT c0d, full_name FROM users WHERE user_group_name='Projects Coordinator' AND status='ACTIVE' AND deleted=0 ORDER BY c0d LIMIT 1", []);
$pcId   = (int)($pcRow[0]['c0d'] ?? 0);
$pcNm   = (string)($pcRow[0]['full_name'] ?? '');

$stamp = date('YmdHis');

/* ── 1 ── Author playbook ──────────────────────────────────────────── */
h('1. Author playbook STD_TELCO with 4 SLAs');
$stdId = $cp->create([
    'code' => 'STD_TELCO_' . $stamp,
    'name' => 'Standard telco site SLA',
    'description' => 'Response + resolution targets for tier-2 telco branches.',
], $adminId, $adminNm);
p("Playbook c0d=$stdId");
$cp->upsertSla($stdId, 'P1', 2.0,   8.0, 'Site down · critical');
$cp->upsertSla($stdId, 'P2', 4.0,  24.0, 'Major degradation');
$cp->upsertSla($stdId, 'P3', 24.0, 72.0, 'Minor · scheduled workaround');
$cp->upsertSla($stdId, 'P4', 72.0, 336.0, 'Cosmetic · next visit');
$slas = $cp->slasFor($stdId);
p("SLAs: " . count($slas));
foreach ($slas as $s) p("  · {$s['severity']} · response {$s['response_target_hours']}h · resolution {$s['resolution_target_hours']}h");
ok(count($slas) === 4, '4 SLA rows (P1..P4)');
$byS = []; foreach ($slas as $s) $byS[$s['severity']] = $s;
ok(abs((float)$byS['P1']['response_target_hours'] - 2.0) < 0.01, 'P1 response = 2h');
ok(abs((float)$byS['P4']['resolution_target_hours'] - 336.0) < 0.01, 'P4 resolution = 336h (2 weeks)');

/* ── 2 ── Escalation steps for P1 ──────────────────────────────────── */
h('2. Add 3 escalation steps for P1');
$cp->addEscalation($stdId, 'P1', 1, 'Field Supervisor', 0.0, 'Dispatch immediately');
$cp->addEscalation($stdId, 'P1', 2, 'Project Manager',  2.0, 'Escalate to PM if not resolved');
$cp->addEscalation($stdId, 'P1', 3, 'Executive',        4.0, 'Client-visible escalation');
$p1Esc = $cp->escalationsFor($stdId, 'P1');
p("P1 escalation steps: " . count($p1Esc));
ok(count($p1Esc) === 3, '3 P1 escalation steps');
ok($p1Esc[0]['role'] === 'Field Supervisor' && $p1Esc[2]['role'] === 'Executive',
   'Ordered correctly by step_no');

/* ── 3 ── Escalation steps for P2 ──────────────────────────────────── */
h('3. Add 2 escalation steps for P2');
$cp->addEscalation($stdId, 'P2', 1, 'Field Supervisor', 1.0, '');
$cp->addEscalation($stdId, 'P2', 2, 'Project Manager',  3.0, '');
$p2Esc = $cp->escalationsFor($stdId, 'P2');
$allEsc = $cp->escalationsFor($stdId);
ok(count($p2Esc) === 2 && count($allEsc) === 5, '5 escalations total (3 P1 + 2 P2)');

/* ── 4 ── Duplicate-code guard ─────────────────────────────────────── */
h('4. Duplicate playbook code → BLOCKED');
try {
    $cp->create(['code' => 'STD_TELCO_' . $stamp, 'name' => 'Try dup'], $adminId, $adminNm);
    ok(false, 'Duplicate allowed');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'already exists') !== false, 'Blocked: ' . $e->getMessage());
}

/* ── 5 ── Provision services project ───────────────────────────────── */
h('5. Provision services project');
$fb = $db->insertUpdate(
    "INSERT INTO tender_tracker
        (tender_no, tender_title, source_type, buyer_name, project_type,
         date_received, estimated_value, currency, tender_stage, tender_status,
         created_by, created_by_name, created_on)
     VALUES (:tn, :tt, 'PRIVATE_CLIENT', 'Vodacom TZ', 'services',
         CURDATE(), 40000000, 'TZS', 'PREP', 'OPEN', :u, :un, NOW())",
    [':tn' => 'T-8B5-' . $stamp, ':tt' => 'SLICE 8b-5 playbook verify', ':u' => $uidCrm, ':un' => $unmCrm]
);
$tid = (int)($fb['last_id'] ?? 0);
foreach (['SUBMISSION','FOLLOW_UP','OPENING'] as $s) $tm->advanceStage($tid, $s, 'x', $uidCrm, $unmCrm);
$tm->recordResult($tid, 'WON', date('Y-m-d'), '', 40000000, '', '', $uidCrm, $unmCrm);
$tm->advanceStage($tid, 'AWARDED', 'a', $uidCrm, $unmCrm);
$tm->saveContractTerms($tid, ['contract_value'=>40000000], $uidFm, $ugFm, $unmFm);
$rC = $ac->submit(['chain_key'=>'TENDER_CONTRACT_APPROVAL','entity_type'=>'TENDER_CONTRACT','entity_id'=>$tid,'ref_label'=>'x','return_url'=>PROOT.'Tenders/view/'.$tid,'user_id'=>$uidCrm,'user_name'=>$unmCrm,'user_group'=>$ugCrm]);
$aidC = (int)$rC['approval_id'];
$ac->approve(['approval_id'=>$aidC,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,'comment'=>'','assignments'=>['ACCOUNTANT'=>['user_id'=>$accId,'user_name'=>$accNm],'PROCUREMENT'=>['user_id'=>$procId,'user_name'=>$procNm]]]);
$ac->approve(['approval_id'=>$aidC,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,'comment'=>'','assignments'=>['PROJECT_MANAGER'=>['user_id'=>$pmId,'user_name'=>$pmNm]]]);
$aRow = $ac->approvalById($aidC);
$stg = $ac->decodePayload((string)($aRow['step_payload_json'] ?? ''));
$tm->onContractApproved($tid, $uidExec, $unmExec, $stg['assignments'] ?? []);
$t = $tm->find($tid); $pid = (int)$t['linked_project_id'];
ok($pid > 0, 'Project provisioned (id=' . $pid . ')');

/* ── 6 ── Attach STD playbook ──────────────────────────────────────── */
h('6. Attach STD_TELCO to project');
$cfgId = $cp->attachToProject($pid, $stdId, $pmId, $pmNm, 'Standard tier-2 attachment');
$active = $cp->activeForProject($pid);
p("cfg c0d=$cfgId · active playbook = " . ($active['playbook_code'] ?? '?'));
ok((int)$active['playbook_id'] === $stdId, 'activeForProject returns STD_TELCO');
ok((int)$active['attached_by'] === $pmId, 'attached_by = PM');

/* ── 7 ── Idempotent same-attach ───────────────────────────────────── */
h('7. Re-attach same playbook → no-op (returns same cfg id)');
$cfgSame = $cp->attachToProject($pid, $stdId, $pmId, $pmNm);
ok($cfgSame === $cfgId, "Same cfg id returned (no new row)");
$cnt = (int)$db->mySelect("SELECT COUNT(*) n FROM project_callout_config WHERE project_id = :p", [':p' => $pid])[0]['n'];
ok($cnt === 1, 'Still exactly 1 row in project_callout_config');

/* ── 8 ── Author PREMIUM and switch ────────────────────────────────── */
h('8. Author PREMIUM_TELCO and switch project attachment');
$premId = $cp->create([
    'code' => 'PREMIUM_TELCO_' . $stamp,
    'name' => 'Premium telco site SLA',
    'description' => 'Tighter targets for HQ + high-value branches.',
], $adminId, $adminNm);
$cp->upsertSla($premId, 'P1', 1.0, 4.0, '');
$cp->upsertSla($premId, 'P2', 2.0, 12.0, '');

$newCfg = $cp->attachToProject($pid, $premId, $pmId, $pmNm, 'Upgraded contract → premium SLA');
$active = $cp->activeForProject($pid);
p("New active: " . ($active['playbook_code'] ?? '?') . " · cfg c0d=" . ($active['c0d'] ?? '?'));
ok((int)$active['playbook_id'] === $premId, 'PREMIUM now active');

// Old cfg should be soft-deleted with detached_on set
$oldCfg = $db->mySelect("SELECT deleted, detached_on, detached_by FROM project_callout_config WHERE c0d = :id", [':id' => $cfgId])[0];
p("Old cfg: deleted={$oldCfg['deleted']} · detached_on={$oldCfg['detached_on']} · detached_by={$oldCfg['detached_by']}");
ok((int)$oldCfg['deleted'] === 1 && !empty($oldCfg['detached_on']), 'Old cfg soft-deleted with detached_on');
ok((int)$oldCfg['detached_by'] === $pmId, 'detached_by = PM who reattached');

/* ── 9 ── attachmentHistory ───────────────────────────────────────── */
h('9. attachmentHistory returns 2 rows (old detached + new active)');
$hist = $cp->attachmentHistory($pid);
p("history rows: " . count($hist));
foreach ($hist as $x) p("  · {$x['playbook_code']} · attached {$x['attached_on']} · detached=" . ($x['detached_on'] ?? '—'));
ok(count($hist) === 2, '2 history rows visible');

/* ── 10 ── Cannot attach inactive playbook ─────────────────────────── */
h('10. Attach inactive playbook → BLOCKED');
$cp->setActive($premId, false, $adminId);
try {
    $cp->attachToProject($pid, $premId, $pmId, $pmNm);
    ok(false, 'Attach on inactive allowed');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'inactive') !== false, 'Blocked: ' . $e->getMessage());
}
$cp->setActive($premId, true, $adminId); // restore

/* ── 11 ── SLA sanity guard ────────────────────────────────────────── */
h('11. SLA response > resolution → BLOCKED');
try {
    $cp->upsertSla($stdId, 'P3', 100.0, 50.0, '');
    ok(false, 'Guard failed');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'exceed resolution') !== false, 'Blocked: ' . $e->getMessage());
}

/* ── 12 ── SLA upsert idempotency ─────────────────────────────────── */
h('12. Second SLA upsert on same severity updates, no duplicate row');
$before = (int)$db->mySelect("SELECT COUNT(*) n FROM callout_playbook_slas WHERE playbook_id = :p AND severity = 'P1' AND deleted = 0", [':p' => $stdId])[0]['n'];
$cp->upsertSla($stdId, 'P1', 3.0, 10.0, 'Revised targets');
$after  = (int)$db->mySelect("SELECT COUNT(*) n FROM callout_playbook_slas WHERE playbook_id = :p AND severity = 'P1' AND deleted = 0", [':p' => $stdId])[0]['n'];
$row = $db->mySelect("SELECT response_target_hours, resolution_target_hours, notes FROM callout_playbook_slas WHERE playbook_id = :p AND severity = 'P1' AND deleted = 0", [':p' => $stdId])[0];
p("row count before=$before · after=$after · new response={$row['response_target_hours']}h res={$row['resolution_target_hours']}h notes='{$row['notes']}'");
ok($before === 1 && $after === 1, 'Still 1 row after upsert');
ok(abs((float)$row['response_target_hours'] - 3.0) < 0.01 && $row['notes'] === 'Revised targets', 'Values updated');

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