<?php
/**
 * Slice 8c-CustomerRetention · Customer retention pool · end-to-end.
 *
 * Piggybacks on the IPC verify path: prime the anchor project (customer_id + crm_status
 * + BOQ.retention_pct = 5%), draft an IPC, certify one BOQ line, approve → IPC becomes
 * APPROVED with retention_amount stamped. Then:
 *
 * 1) Baseline poolFor (before we add) — capture.
 * 2) Approve the IPC → total_held rises by retention_amount.
 * 3) Guard: recordRelease over pool balance is rejected.
 * 4) Record 50% release → total_released grows; in_pool falls by half.
 * 5) Record remaining 50% → in_pool = 0.
 * 6) Second full release attempt rejected (in_pool exhausted).
 * 7) deleteRelease returns the amount to the pool.
 * 8) DLP end estimate = last IPC date + BOQ.dlp_days.
 * 9) Perms + surface + wiring.
 * Z) Restore project + BOQ state.
 */
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 (['WorksIpcs.php','WorksBoq.php','WorksRetention.php','WorksCustomerRetention.php','Sales.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();
$wi  = new WorksIpcs();
$wr  = new WorksRetention();
$wcr = new WorksCustomerRetention();
$wb  = new WorksBoq();

$pcId = 100; $pcNm = 'Verify PC'; $pcG = 'Projects Coordinator';
$fmId = 3;   $fmNm = 'Verify FM';

/* ── 0. Anchor + backup ─────────────────────────────────────────────── */
h('0. Pick anchor works project + backup state');
$anchor = $db->mySelect(
    "SELECT p.c0d, p.project_no, p.project_customer_id, p.crm_status
       FROM c_projects p JOIN works_boqs b ON b.project_id = p.c0d AND b.deleted = 0
      WHERE p.project_type='works' AND p.deleted=0 ORDER BY p.c0d ASC LIMIT 1",
    []
);
if (empty($anchor)) boom('No works project with a BOQ. Run slice8cSpine_e2e_verify.php first.');
$prjId = (int)$anchor[0]['c0d'];
$prevCustId = (int)$anchor[0]['project_customer_id'];
$prevCrm    = (string)$anchor[0]['crm_status'];
$prevTerms  = $wr->termsFor($prjId) ?: ['pct' => 0.0, 'dlp_days' => 365];
p("Project #$prjId · customer_id=$prevCustId · crm_status=$prevCrm · retention_pct={$prevTerms['pct']}");

/* ── 0b. Prime project ────────────────────────────────────────────── */
h('0b. Prime project for invoicing + retention');
$db->insertUpdate(
    "UPDATE c_projects SET project_customer_id = 1, crm_status = 'APPROVED' WHERE c0d = :p",
    [':p' => $prjId]
);
$wr->setTerms($prjId, 5.00, 90);
p('customer_id=1 · crm_status=APPROVED · retention_pct=5% · DLP=90');

/* ── 0c. Purge prior CustRet-verify artefacts ─────────────────────── */
h('0c. Purge prior artefacts');
$db->insertUpdate(
    "DELETE FROM works_customer_retention_releases WHERE project_id = :p AND notes LIKE 'CustRetVerify %'",
    [':p' => $prjId]
);
$oldIpcs = $db->mySelect(
    "SELECT c0d, sale_id FROM works_ipcs WHERE project_id = :p AND notes LIKE 'CustRetVerify %'",
    [':p' => $prjId]
) ?: [];
foreach ($oldIpcs as $oi) {
    $id = (int)$oi['c0d']; $sid = (int)$oi['sale_id'];
    $db->insertUpdate("DELETE FROM works_ipc_lines WHERE ipc_id = :i", [':i' => $id]);
    $db->insertUpdate("DELETE FROM works_ipcs WHERE c0d = :i", [':i' => $id]);
    if ($sid > 0) {
        $db->insertUpdate("DELETE FROM sales_items WHERE sale_id = :s", [':s' => $sid]);
        $db->insertUpdate("DELETE FROM sales      WHERE c0d     = :s", [':s' => $sid]);
    }
}
// stray DRAFT
$stray = $db->mySelect("SELECT c0d FROM works_ipcs WHERE project_id = :p AND status IN ('DRAFT','SUBMITTED')", [':p' => $prjId]) ?: [];
foreach ($stray as $s) {
    $db->insertUpdate("DELETE FROM works_ipc_lines WHERE ipc_id = :i", [':i' => (int)$s['c0d']]);
    $db->insertUpdate("DELETE FROM works_ipcs WHERE c0d = :i", [':i' => (int)$s['c0d']]);
}
p("Purged " . count($oldIpcs) . " verify IPCs + " . count($stray) . " stray drafts");

/* ── 0d. Baseline pool before we add ────────────────────────────────── */
h('0d. Baseline pool');
$base = $wcr->poolFor($prjId);
p(sprintf('baseline held=%s released=%s in_pool=%s',
    number_format($base['total_held']), number_format($base['total_released']), number_format($base['in_pool'])));

/* ── 1. Draft + certify + approve an IPC ──────────────────────────── */
h('1. Draft + approve an IPC (retention captured)');
$dr = $wi->draft($prjId, ['notes' => 'CustRetVerify ' . date('YmdHis')], $pcId, $pcNm);
if (!$dr['ok']) boom('draft IPC failed: ' . ($dr['msg'] ?? '?'));
$ipcId = (int)$dr['id'];
$lines = $wi->linesFor($ipcId);
$line0 = $lines[0];
$thisQty = min(100, (float)$line0['contract_qty']);
$rate    = (float)$line0['contract_rate'];
$gross   = round($thisQty * $rate, 2);
$retAmt  = round($gross * 5 / 100, 2);
$wi->setLineQty($ipcId, (int)$line0['c0d'], $thisQty);
$appRes = $wi->approve($ipcId, $pcId, $pcNm, $pcG);
if (!$appRes['ok']) boom('approve failed: ' . ($appRes['msg'] ?? '?'));
p("IPC {$dr['ipc_no']} · gross=" . number_format($gross) . " · retention=" . number_format($retAmt));

/* ── 2. Pool grew by retention_amount ────────────────────────────── */
h('2. poolFor picks up the retention amount');
$p1 = $wcr->poolFor($prjId);
p(sprintf('now held=%s released=%s in_pool=%s (ipcs=%d)',
    number_format($p1['total_held']), number_format($p1['total_released']), number_format($p1['in_pool']), $p1['count_ipcs']));
ok(abs($p1['total_held']     - ($base['total_held']     + $retAmt)) < 0.01, 'total_held grew by IPC retention');
ok(abs($p1['in_pool']        - ($base['in_pool']        + $retAmt)) < 0.01, 'in_pool grew by IPC retention');
ok((int)$p1['count_ipcs']    >= (int)$base['count_ipcs'] + 1,                'count_ipcs +1');

/* ── 3. Guard: recordRelease over pool → rejected ─────────────────── */
h('3. Over-pool release rejected');
$over = $wcr->recordRelease($prjId, $p1['in_pool'] + 1, date('Y-m-d'), 'CustRetVerify over', 0, $fmId, $fmNm);
ok(!$over['ok'] && stripos($over['msg'], 'exceeds in-pool') !== false, 'over-release rejected');

/* ── 4. 50% release ────────────────────────────────────────────────── */
h('4. Record 50% release');
$half = round($retAmt / 2, 2);
$rel1 = $wcr->recordRelease($prjId, $half, date('Y-m-d'), 'CustRetVerify half at PC', 0, $fmId, $fmNm);
ok($rel1['ok'], 'first release recorded');
$rel1Id = (int)$rel1['id'];

$p2 = $wcr->poolFor($prjId);
ok(abs($p2['total_released'] - ($base['total_released'] + $half)) < 0.01, 'total_released grew by half');
ok(abs($p2['in_pool']        - ($base['in_pool']        + $retAmt - $half)) < 0.01, 'in_pool decreased by half');

/* ── 5. Remaining 50% ─────────────────────────────────────────────── */
h('5. Record remaining 50%');
$remaining = round($retAmt - $half, 2);
$rel2 = $wcr->recordRelease($prjId, $remaining, date('Y-m-d'), 'CustRetVerify final at DLP', 0, $fmId, $fmNm);
ok($rel2['ok'], 'second release recorded');
$rel2Id = (int)$rel2['id'];

$p3 = $wcr->poolFor($prjId);
p(sprintf('after 2 releases: held=%s released=%s in_pool=%s',
    number_format($p3['total_held']), number_format($p3['total_released']), number_format($p3['in_pool'])));
ok(abs($p3['in_pool']        - $base['in_pool']) < 0.01,                     'in_pool restored to baseline');
ok(abs($p3['total_released'] - ($base['total_released'] + $retAmt)) < 0.01, 'total_released += IPC retention');

/* ── 6. Release exceeding current pool balance is rejected ───────── */
h('6. Over-pool release still rejected');
$curBefore = $wcr->poolFor($prjId);
$noMore    = $wcr->recordRelease($prjId, (float)$curBefore['in_pool'] + 1, date('Y-m-d'), 'CustRetVerify overflow', 0, $fmId, $fmNm);
ok(!$noMore['ok'] && stripos($noMore['msg'], 'exceeds in-pool') !== false, 'in_pool + 1 rejected');

/* ── 7. deleteRelease returns amount to pool ─────────────────────── */
h('7. deleteRelease returns amount to pool');
$before = $wcr->poolFor($prjId);
$delRes = $wcr->deleteRelease($rel2Id, $fmId);
ok($delRes['ok'], 'delete accepted');
$after  = $wcr->poolFor($prjId);
ok(abs($after['in_pool'] - ($before['in_pool'] + $remaining)) < 0.01, 'in_pool grew back by removed amount');

/* ── 8. DLP end estimate ─────────────────────────────────────────── */
h('8. DLP end estimate');
$pool = $wcr->poolFor($prjId);
$expEnd = date('Y-m-d', strtotime((string)$pool['last_ipc_date'] . ' +90 days'));
ok($pool['dlp_end_estimate'] === $expEnd, "dlp_end_estimate = last_ipc + 90d = $expEnd");
ok(is_int($pool['days_to_dlp']) || is_float($pool['days_to_dlp']), 'days_to_dlp numeric');

/* ── 9. Perms + surface + wiring ─────────────────────────────────── */
h('9. Perms + surface + wiring');
foreach (['Projects Coordinator','Project Manager','Field Supervisor','Procurement','Finance Manager','Accountant','Executive','Administrator'] as $role) {
    $rr = $db->mySelect("SELECT c0d FROM user_groups WHERE user_group_name = :n", [':n' => $role]);
    $gid = (int)($rr[0]['c0d'] ?? 0);
    $r = $db->mySelect(
        "SELECT 1 FROM user_groups_perms WHERE user_group_id = :g AND controller_allowed = 'WorksCustomerRetention' AND action_allowed = 'pool' LIMIT 1",
        [':g' => $gid]
    );
    ok(!empty($r), "$role · WorksCustomerRetention/pool");
}
foreach (['Finance Manager','Administrator'] as $role) {
    $rr = $db->mySelect("SELECT c0d FROM user_groups WHERE user_group_name = :n", [':n' => $role]);
    $gid = (int)($rr[0]['c0d'] ?? 0);
    foreach (['recordReleaseAj','deleteReleaseAj'] as $act) {
        $r = $db->mySelect(
            "SELECT 1 FROM user_groups_perms WHERE user_group_id = :g AND controller_allowed = 'WorksCustomerRetention' AND action_allowed = :a LIMIT 1",
            [':g' => $gid, ':a' => $act]
        );
        ok(!empty($r), "$role · WorksCustomerRetention/$act");
    }
}
ok(is_file(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksCustomerRetention' . DS . 'pool.php'), 'pool view exists');
$ipcListBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksIpcs' . DS . 'list.php');
ok(strpos($ipcListBody, 'WorksCustomerRetention/pool/') !== false, 'IPCs list links to CustomerRetention/pool');
$rptBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksProjectReport' . DS . 'view.php');
ok(strpos($rptBody, 'WorksCustomerRetention/pool/') !== false, 'project report links to CustomerRetention/pool');

/* ── Z. Restore state ────────────────────────────────────────────── */
h('Z. Restore anchor state');
// Clean the release/IPC we made in this run so poolFor for future verifies stays sane
$db->insertUpdate("DELETE FROM works_customer_retention_releases WHERE project_id = :p AND notes LIKE 'CustRetVerify %'", [':p' => $prjId]);
$db->insertUpdate("DELETE FROM works_ipc_lines WHERE ipc_id = :i", [':i' => $ipcId]);
$saleId = (int)($wi->findById($ipcId)['sale_id'] ?? 0);
$db->insertUpdate("DELETE FROM works_ipcs WHERE c0d = :i", [':i' => $ipcId]);
if ($saleId > 0) {
    $db->insertUpdate("DELETE FROM sales_items WHERE sale_id = :s", [':s' => $saleId]);
    $db->insertUpdate("DELETE FROM sales      WHERE c0d     = :s", [':s' => $saleId]);
}
$db->insertUpdate(
    "UPDATE c_projects SET project_customer_id = :cid, crm_status = :cs WHERE c0d = :p",
    [':cid' => $prevCustId, ':cs' => $prevCrm, ':p' => $prjId]
);
$wr->setTerms($prjId, (float)$prevTerms['pct'], (int)$prevTerms['dlp_days']);
p("Restored: customer_id=$prevCustId · crm_status=$prevCrm · retention_pct={$prevTerms['pct']} · dlp={$prevTerms['dlp_days']}");

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