<?php
/**
 * Slice 8b-T · Work Templates foundation verify.
 *
 *   1) Author a PM-scope template with 3 items (CHECK required, NUMBER, TEXT).
 *   2) Create an instance targeting a c_project; confirm frozen template_version.
 *   3) Assignee saves 2 results (one CHECK OK, one NUMBER); submit without the required text → BLOCKED.
 *   4) Save the missing required item → submit succeeds.
 *   5) Reviewer rejects with reason → status REJECTED; assignee reopens → DRAFT.
 *   6) Resubmit → approve; status APPROVED (terminal).
 *   7) Version-fork test: create a new instance on v1 (still in flight), fork → v2 exists,
 *      v1 in-flight instance still queries against its frozen version, edits blocked on v1.
 *   8) Result upsert test: save the same item twice → only 1 row (UNIQUE key holds).
 *   9) Attachment sha256 dedup test.
 */
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 (['WorkTemplates.php','WorkInstances.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();
$wt = new WorkTemplates();
$wi = new WorkInstances();

$adminId = 1; $adminNm = 'Admin';
$fsId = 7; $fsNm = 'Field Supervisor Test';
$pmId = 5; $pmNm = 'Project Manager Test';

/* 1 — Author template */
h('1. Author PM template with 3 items');
$stamp = date('YmdHis');
$tplId = $wt->create([
    'code' => 'PM_GEN_5KVA_' . $stamp, 'title' => 'Monthly PM · 5kVA generator',
    'description' => 'Standard monthly PM checklist for branch generators.',
    'scope' => 'PM', 'target_entity_type' => 'c_projects', 'cadence' => 'MONTHLY',
    'expected_minutes' => 45, 'reviewer_role' => 'PROJECT MANAGER',
], $adminId, $adminNm);
p("Template c0d=$tplId");
$i1 = $wt->addItem($tplId, ['section'=>'Exterior','prompt'=>'Enclosure locked and undamaged?','input_type'=>'CHECK','is_required'=>1]);
$i2 = $wt->addItem($tplId, ['section'=>'Power','prompt'=>'Battery voltage','input_type'=>'NUMBER','is_required'=>0,'numeric_unit'=>'V','numeric_min'=>10,'numeric_max'=>15]);
$i3 = $wt->addItem($tplId, ['section'=>'Closeout','prompt'=>'Any recommendations for site owner','input_type'=>'LONGTEXT','is_required'=>1]);
$items = $wt->itemsForTemplate($tplId);
ok(count($items) === 3, count($items) . ' items authored');

/* 2 — Create instance targeting a project */
h('2. Create instance targeting c_projects#1');
$instId = $wi->create($tplId, 'c_projects', 1, [
    'ref_label' => 'Monthly PM · Site 01',
    'scheduled_for' => date('Y-m-d'),
    'assignee_id' => $fsId, 'assignee_name' => $fsNm,
], $pmId, $pmNm);
$inst = $wi->findById($instId);
p("Instance c0d={$inst['c0d']} · template_version={$inst['template_version']} · assignee={$inst['assignee_name']}");
ok((int)$inst['template_version'] === 1, 'Frozen at template_version=1');
ok($inst['status'] === 'DRAFT', 'Status DRAFT');

/* 3 — Save 2 results, try to submit missing required */
h('3. Save CHECK + NUMBER results → submit without required LONGTEXT → BLOCKED');
$wi->setResult($instId, $i1, ['value' => 'OK', 'note' => 'Padlock intact'], $fsId);
$wi->setResult($instId, $i2, ['value' => 12.6], $fsId);
try {
    $wi->submit($instId, $fsId, $fsNm);
    ok(false, 'Submit allowed despite missing required');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'Required items missing') !== false, "Blocked: " . $e->getMessage());
}

/* 4 — Fill required, submit */
h('4. Save required LONGTEXT → submit succeeds');
$wi->setResult($instId, $i3, ['value' => 'Consider oil top-up mid-cycle.'], $fsId);
$wi->submit($instId, $fsId, $fsNm);
$after = $wi->findById($instId);
ok($after['status'] === 'SUBMITTED' && !empty($after['submitted_at']), "Status = SUBMITTED · submitted_at set");

/* 5 — Reject → reopen */
h('5. Reviewer rejects with reason → assignee reopens');
$wi->reject($instId, $pmId, $pmNm, 'Please recheck battery — reading seems low.');
$rej = $wi->findById($instId);
ok($rej['status'] === 'REJECTED' && $rej['review_notes'] === 'Please recheck battery — reading seems low.', 'REJECTED with review_notes');
$wi->reopen($instId, $fsId);
$reo = $wi->findById($instId);
ok($reo['status'] === 'DRAFT', 'Reopened → DRAFT');

/* 6 — Resubmit + approve */
h('6. Resubmit + approve');
$wi->submit($instId, $fsId, $fsNm);
$wi->approve($instId, $pmId, $pmNm, 'OK verified');
$fin = $wi->findById($instId);
ok($fin['status'] === 'APPROVED' && !empty($fin['reviewed_at']), 'APPROVED · reviewed_at set');

/* 7 — Version fork protects in-flight instance */
h('7. Version fork: v1 has an APPROVED instance; fork → v2; item edits blocked on v2? no — on v1 that has live instances yes.');
// Make a NEW instance on v1 in DRAFT so v1 has a live instance
$liveInstId = $wi->create($tplId, 'c_projects', 1, ['ref_label'=>'PM · site02','assignee_id'=>$fsId,'assignee_name'=>$fsNm], $pmId, $pmNm);
try { $wt->addItem($tplId, ['prompt'=>'Extra?','input_type'=>'CHECK']); ok(false, 'v1 item add allowed despite live instance'); }
catch (\Throwable $e) { ok(strpos($e->getMessage(), 'live instances') !== false, "v1 item add blocked: " . $e->getMessage()); }
$newVerId = $wt->forkNewVersion($tplId, $adminId, $adminNm);
$v2 = $wt->findById($newVerId);
p("v2 c0d=$newVerId, version={$v2['version']}, is_active={$v2['is_active']}");
ok((int)$v2['version'] === 2 && (int)$v2['is_active'] === 1, 'v2 created as active');
$v1Now = $wt->findById($tplId);
ok((int)$v1Now['is_active'] === 0, 'v1 auto-retired');
// v2 has NO live instances yet → item add allowed
$v2ItemId = $wt->addItem($newVerId, ['section'=>'Exterior','prompt'=>'New safety check','input_type'=>'CHECK','is_required'=>1]);
ok($v2ItemId > 0, 'Item added to v2');
// Live v1 instance still points at v1
$liveInstAfter = $wi->findById($liveInstId);
ok((int)$liveInstAfter['template_version'] === 1, 'Live v1 instance still frozen at version=1');
$liveItems = $wt->itemsForTemplate((int)$liveInstAfter['template_id']);
ok(count($liveItems) === 3, "Live v1 instance sees v1's original 3 items (not v2's extra one)");

/* 8 — Result upsert dedup */
h('8. Result upsert: second save on same item does NOT create a second row');
$wi->setResult($liveInstId, $items[0]['c0d'], ['value'=>'OK'], $fsId);
$wi->setResult($liveInstId, $items[0]['c0d'], ['value'=>'NOTOK','note'=>'reopened lock'], $fsId);
$rc = (int)$db->mySelect("SELECT COUNT(*) n FROM work_instance_results WHERE instance_id = :i AND template_item_id = :t", [':i'=>$liveInstId, ':t'=>$items[0]['c0d']])[0]['n'];
$val = $db->mySelect("SELECT value_text, note FROM work_instance_results WHERE instance_id = :i AND template_item_id = :t", [':i'=>$liveInstId, ':t'=>$items[0]['c0d']])[0];
p("row count for that item: $rc · latest value_text={$val['value_text']} note='{$val['note']}'");
ok($rc === 1 && $val['value_text'] === 'NOTOK' && $val['note'] === 'reopened lock', 'Upsert kept 1 row with latest value');

/* 9 — Attachment sha256 dedup */
h('9. Attachment upload: duplicate sha256 returns the existing row');
$sha = str_repeat('ab', 32); // 64 hex chars
$a1 = $wi->addAttachment($liveInstId, ['sha256'=>$sha, 'file_path'=>'/uploads/a.jpg', 'mime_type'=>'image/jpeg', 'size_bytes'=>1024, 'phase'=>'GENERAL'], $fsId);
$a2 = $wi->addAttachment($liveInstId, ['sha256'=>$sha, 'file_path'=>'/uploads/a-dup.jpg', 'mime_type'=>'image/jpeg', 'size_bytes'=>1024, 'phase'=>'GENERAL'], $fsId);
p("attachment ids: first=$a1 · second=$a2");
ok($a1 === $a2, 'Second upload returned same c0d — dedup successful');
$att = $wi->attachmentsFor($liveInstId);
ok(count($att) === 1, 'Only 1 attachment row for the instance');

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