<?php
/**
 * Slice 8b-3 · Services PM Plan e2e (first consumer of the Work Templates engine).
 *
 *   1) Author a PM-scope template targeting 'assets' with 3 items (1 required CHECK).
 *   2) Create a services project + register 2 assets scoped to it.
 *   3) Create a PM plan on the project; add 2 items (2 assets × same template) MONTHLY.
 *   4) generateNextInstances(planId, horizon=60d):
 *      Both items' next_due (today) fall inside the horizon → 2 WorkInstances created.
 *      next_due for each item advances by 30 days (MONTHLY).
 *   5) Immediate second generate with horizon=60d → 0 new (the new next_due is 30d out;
 *      but WAIT: horizon 60d covers it, so it WILL generate again. Confirm: at 30d idempotency
 *      relies on same-scheduled_for dedup. Test with horizon=15d → 0 new (next_due beyond horizon).
 *   6) Third generate with horizon=60d → 2 more (next_due=30d, inside horizon).
 *   7) Verify WorkInstances have correct target_entity_type='assets', target_entity_id, and
 *      context_json carries plan/plan_item ids.
 *   8) FT (assignee) sees the instance in their queue via WorkInstances::myQueue.
 */
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',
    'WorkTemplates.php','WorkInstances.php','ServicePmPlans.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();
$wt = new WorkTemplates();
$wi = new WorkInstances();
$sp = new ServicePmPlans();

$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';
$fsId   = 7;   $fsNm   = 'Field Supervisor 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'] ?? '');

/* ── 1 ── Author PM template targeting assets ─────────────────────── */
h('1. Author PM template targeting assets (3 items)');
$stamp = date('YmdHis');
$tplId = $wt->create([
    'code' => 'PM_GEN_MONTHLY_' . $stamp, 'title' => 'Monthly PM · generator',
    'description' => 'Standard monthly PM for a branch generator.',
    'scope' => 'PM', 'target_entity_type' => 'assets', 'cadence' => 'MONTHLY',
    'expected_minutes' => 45, 'reviewer_role' => 'PROJECT MANAGER',
], $adminId, $adminNm);
$wt->addItem($tplId, ['section'=>'Exterior','prompt'=>'Enclosure locked?','input_type'=>'CHECK','is_required'=>1]);
$wt->addItem($tplId, ['section'=>'Power','prompt'=>'Battery voltage','input_type'=>'NUMBER','numeric_unit'=>'V']);
$wt->addItem($tplId, ['section'=>'Closeout','prompt'=>'Recommendations','input_type'=>'LONGTEXT']);
ok(count($wt->itemsForTemplate($tplId)) === 3, 'Template with 3 items created (id=' . $tplId . ')');

/* ── 2 ── Create services project + register 2 assets ─────────────── */
h('2. Drive Services pipeline to project + register 2 assets');
$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(), 60000000, 'TZS', 'PREP', 'OPEN', :u, :un, NOW())",
    [':tn' => 'T-8B3-' . $stamp, ':tt' => 'SLICE 8b-3 PM plan 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'), '', 60000000, '', '', $uidCrm, $unmCrm);
$tm->advanceStage($tid, 'AWARDED', 'a', $uidCrm, $unmCrm);
$tm->saveContractTerms($tid, ['contract_value'=>60000000], $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'];
p("Project $pid provisioned");

// Register 2 assets against the project. Uses whatever asset_category exists (first one).
$catRow = $db->mySelect("SELECT c0d FROM asset_categories WHERE deleted = 0 ORDER BY c0d LIMIT 1", []);
if (empty($catRow)) {
    $db->insertUpdate("INSERT INTO asset_categories (name, created_on) VALUES ('Generators', NOW())", []);
    $catRow = $db->mySelect("SELECT c0d FROM asset_categories ORDER BY c0d DESC LIMIT 1", []);
}
$catId = (int)$catRow[0]['c0d'];
$assetIds = [];
foreach (['GEN-001', 'GEN-002'] as $i => $no) {
    $seq = ((int)$db->mySelect("SELECT COUNT(*) n FROM assets WHERE deleted=0", [])[0]['n']) + 1;
    $assetNo = 'AST-' . date('Y') . '-' . str_pad((string)$seq, 4, '0', STR_PAD_LEFT);
    $fb = $db->insertUpdate(
        "INSERT INTO assets
            (asset_no, category_id, name, purchase_date, purchase_cost, project_id, status, created_by, created_on)
         VALUES (:no, :c, :n, CURDATE(), 12000000, :p, 'IN_SERVICE', :u, NOW())",
        [':no' => $assetNo, ':c' => $catId, ':n' => 'Branch generator ' . $no, ':p' => $pid, ':u' => $adminId]
    );
    $assetIds[] = (int)($fb['last_id'] ?? 0);
}
p("Assets registered: " . implode(', ', $assetIds));
ok(count($assetIds) === 2, '2 assets registered on project');

/* ── 3 ── Create PM plan + add 2 items ─────────────────────────────── */
h('3. Create PM plan and add 2 items (2 assets × same template · MONTHLY)');
$planId = $sp->create($pid, ['title' => 'Monthly generator PM · Vodacom sites', 'planned_start' => date('Y-m-d'), 'planned_end' => date('Y-m-d', strtotime('+365 days'))], $pcId, $pcNm);
$today = date('Y-m-d');
$item1 = $sp->addItem($planId, $assetIds[0], $tplId, 'MONTHLY', ['start_date' => $today, 'default_assignee_id' => $fsId, 'default_assignee_name' => $fsNm]);
$item2 = $sp->addItem($planId, $assetIds[1], $tplId, 'MONTHLY', ['start_date' => $today, 'default_assignee_id' => $fsId, 'default_assignee_name' => $fsNm]);
$items = $sp->itemsFor($planId);
p("Plan c0d=$planId · items=" . count($items));
ok(count($items) === 2, '2 plan items added');

/* ── 4 ── First generate horizon=60d → 2 instances created ─────────── */
h('4. generateNextInstances(60d) — expect 2 WorkInstances');
$sp->activate($planId, $pcId);
$created1 = $sp->generateNextInstances($planId, 60, $pcId, $pcNm);
p("Created: " . count($created1) . " instance ids: " . implode(', ', $created1));
ok(count($created1) === 2, '2 WorkInstances created');

// Confirm instances shape
foreach ($created1 as $iid) {
    $inst = $wi->findById($iid);
    ok($inst && $inst['target_entity_type'] === 'assets' && in_array((int)$inst['target_entity_id'], $assetIds, true),
       "  inst $iid targets an asset (id={$inst['target_entity_id']})");
    ok((int)$inst['template_id'] === $tplId && (int)$inst['template_version'] === 1,
       "  inst $iid stamped template_id=$tplId, template_version=1");
    ok((int)$inst['assignee_id'] === $fsId, "  inst $iid assigned to FS");
    $cx = json_decode((string)$inst['context_json'], true);
    ok(isset($cx['plan_id']) && (int)$cx['plan_id'] === $planId, "  inst $iid context_json carries plan_id");
}

/* ── 5 ── next_due advanced by 30 days on the plan items ──────────── */
h('5. Plan items next_due advanced by 30 days');
$itemsAfter = $sp->itemsFor($planId);
$expectedDue = date('Y-m-d', strtotime($today . ' +30 days'));
foreach ($itemsAfter as $it) p("  item {$it['c0d']} · next_due={$it['next_due']} (expected $expectedDue)");
ok($itemsAfter[0]['next_due'] === $expectedDue && $itemsAfter[1]['next_due'] === $expectedDue,
   'Both items next_due bumped to today+30d');

/* ── 6 ── Second generate horizon=15d → 0 new (next_due beyond horizon) ── */
h('6. generateNextInstances(15d) — expect 0 new (next_due 30d > horizon 15d)');
$created2 = $sp->generateNextInstances($planId, 15, $pcId, $pcNm);
p("Created: " . count($created2));
ok(count($created2) === 0, '0 new instances (idempotent within short horizon)');

/* ── 7 ── Third generate horizon=60d → 2 more ─────────────────────── */
h('7. generateNextInstances(60d) — next_due (30d) is inside horizon → 2 more');
$created3 = $sp->generateNextInstances($planId, 60, $pcId, $pcNm);
p("Created: " . count($created3));
ok(count($created3) === 2, '2 more WorkInstances created (next cycle)');

// Total instances on assets should now be 4
$totalInst = (int)$db->mySelect(
    "SELECT COUNT(*) n FROM work_instances WHERE template_id = :t AND target_entity_type='assets' AND target_entity_id IN (:a1, :a2)",
    [':t' => $tplId, ':a1' => $assetIds[0], ':a2' => $assetIds[1]]
)[0]['n'];
ok($totalInst === 4, "Total instances against these assets = 4 (2 cycles × 2 assets)");

/* ── 8 ── FS sees instances in their queue ─────────────────────────── */
h('8. FS sees the generated instances in myQueue');
$queue = $wi->myQueue($fsId);
$mine = array_filter($queue, fn($q) => (int)$q['template_id'] === $tplId);
p("FS queue rows for this template: " . count($mine));
ok(count($mine) >= 2, 'FS queue picks up the generated instances (mobile-ready)');

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