<?php
/**
 * Preservation Property Tests — Tuition Payment Allocation Fix
 *
 * Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
 *
 * Property 2: Preservation — Existing Allocation and Approval Behaviour
 *
 * PURPOSE: Verify that for all inputs where the five bug conditions do NOT hold,
 * the fixed code produces the same allocation result as the expected correct behaviour.
 *
 * These tests MUST PASS on the fixed code — they confirm no regressions were introduced.
 *
 * Run via CLI: php portal/tests/TuitionAllocationPreservationTest.php
 */

// ── Bootstrap ─────────────────────────────────────────────────────────────────
$root = dirname(__DIR__); // portal/
require_once "{$root}/includes/dbconnection.php";

if (!isset($con) || mysqli_connect_errno()) {
    echo "FATAL: Could not connect to database.\n";
    exit(1);
}

// ── Helpers ───────────────────────────────────────────────────────────────────
$pass = 0;
$fail = 0;

function assert_equal(string $label, $expected, $actual): void
{
    global $pass, $fail;
    if ($expected === $actual) {
        echo "[PASS] $label\n";
        echo "       expected=" . json_encode($expected) . ", actual=" . json_encode($actual) . "\n";
        $pass++;
    } else {
        echo "[FAIL] $label\n";
        echo "       expected=" . json_encode($expected) . ", actual=" . json_encode($actual) . "\n";
        $fail++;
    }
}

function assert_true(string $label, bool $condition, string $detail = ''): void
{
    global $pass, $fail;
    if ($condition) {
        echo "[PASS] $label\n";
        if ($detail) echo "       $detail\n";
        $pass++;
    } else {
        echo "[FAIL] $label\n";
        if ($detail) echo "       $detail\n";
        $fail++;
    }
}

/**
 * Core allocation logic extracted from pay_tuition.php for unit testing.
 * Mirrors the fixed allocation loop exactly.
 *
 * @param float  $amount          Payment amount to allocate
 * @param array  $term_fees       ['First Term' => float, 'Second Term' => float, 'Third Term' => float]
 * @param array  $term_paid       ['First Term' => float, 'Second Term' => float, 'Third Term' => float]
 * @param array  $allocation_order Terms to iterate through in order
 * @return array ['inserts' => [['term'=>string,'amount'=>float],...], 'remaining' => float]
 */
function runAllocationLogic(float $amount, array $term_fees, array $term_paid, array $allocation_order): array
{
    $term_inserts = [];
    $remaining = $amount;

    foreach ($allocation_order as $term_name) {
        $needed = $term_fees[$term_name] - $term_paid[$term_name];
        if ($needed <= 0) continue;

        $allocate = min($remaining, $needed);
        if ($allocate <= 0) break;

        $term_inserts[] = ['term' => $term_name, 'amount' => $allocate];
        $remaining -= $allocate;
    }

    return ['inserts' => $term_inserts, 'remaining' => $remaining];
}

echo "\n=== Preservation Property Tests — Tuition Payment Allocation ===\n\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.1 — Student with zero payment history: allocation starts from First Term
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.1: Zero prior payments — allocation starts from First Term ---\n";

$term_fees   = ['First Term' => 55000.0, 'Second Term' => 55000.0, 'Third Term' => 55000.0];
$term_paid   = ['First Term' => 0.0,     'Second Term' => 0.0,     'Third Term' => 0.0];
$order       = ['First Term', 'Second Term', 'Third Term'];
$result      = runAllocationLogic(55000.0, $term_fees, $term_paid, $order);

assert_equal('First insert is First Term', 'First Term', $result['inserts'][0]['term'] ?? null);
assert_equal('First insert amount = 55000', 55000.0, $result['inserts'][0]['amount'] ?? null);
assert_equal('Remaining after full First Term payment = 0', 0.0, $result['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.1 — Full session payment (3 terms) allocates across all three terms
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.1: Full session payment — allocates across all three terms ---\n";

$result = runAllocationLogic(165000.0, $term_fees, $term_paid, $order);

assert_equal('Three inserts produced', 3, count($result['inserts']));
assert_equal('Insert 1 = First Term 55000',  ['term' => 'First Term',  'amount' => 55000.0], $result['inserts'][0]);
assert_equal('Insert 2 = Second Term 55000', ['term' => 'Second Term', 'amount' => 55000.0], $result['inserts'][1]);
assert_equal('Insert 3 = Third Term 55000',  ['term' => 'Third Term',  'amount' => 55000.0], $result['inserts'][2]);
assert_equal('No remaining after full session', 0.0, $result['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.2 — Partial payment: only remaining balance allocated to partially-paid term
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.2: Partial prior payment — only balance allocated to that term ---\n";

$term_paid_partial = ['First Term' => 30000.0, 'Second Term' => 0.0, 'Third Term' => 0.0];
$result = runAllocationLogic(25000.0, $term_fees, $term_paid_partial, $order);

assert_equal('One insert produced', 1, count($result['inserts']));
assert_equal('Insert is First Term', 'First Term', $result['inserts'][0]['term']);
assert_equal('Insert amount = 25000 (balance only)', 25000.0, $result['inserts'][0]['amount']);
assert_equal('No remaining', 0.0, $result['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.2 — Payment that completes one term and spills into next
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.2: Payment completes partial term and spills into next ---\n";

$term_paid_partial2 = ['First Term' => 30000.0, 'Second Term' => 0.0, 'Third Term' => 0.0];
$result = runAllocationLogic(80000.0, $term_fees, $term_paid_partial2, $order);

assert_equal('Two inserts produced', 2, count($result['inserts']));
assert_equal('Insert 1 = First Term 25000 (balance)', 25000.0, $result['inserts'][0]['amount']);
assert_equal('Insert 2 = Second Term 55000', 55000.0, $result['inserts'][1]['amount']);
assert_equal('No remaining', 0.0, $result['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.2 — Fully paid term is skipped (the primary bug fix — preservation side)
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.2: Fully paid First Term is skipped, payment goes to Second Term ---\n";

$term_paid_full_first = ['First Term' => 55000.0, 'Second Term' => 0.0, 'Third Term' => 0.0];
$result = runAllocationLogic(55000.0, $term_fees, $term_paid_full_first, $order);

assert_equal('One insert produced', 1, count($result['inserts']));
assert_equal('Insert is Second Term (First Term skipped)', 'Second Term', $result['inserts'][0]['term']);
assert_equal('Insert amount = 55000', 55000.0, $result['inserts'][0]['amount']);
assert_equal('No remaining', 0.0, $result['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.3 — Final year (SS 3 / JSS 3): Second Term = secondFee + thirdFee, no Third Term
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.3: Final year student — Second Term combines both fees, no Third Term ---\n";

$secondTermFee = 55000.0;
$thirdTermFee  = 55000.0;
$term_fees_fy  = [
    'First Term'  => 60000.0,
    'Second Term' => $secondTermFee + $thirdTermFee, // 110000
    'Third Term'  => 0.0,
];
$order_fy    = ['First Term', 'Second Term']; // Final year: no Third Term in order
$term_paid_0 = ['First Term' => 0.0, 'Second Term' => 0.0, 'Third Term' => 0.0];

assert_equal('Second Term fee = secondFee + thirdFee', 110000.0, $term_fees_fy['Second Term']);
assert_equal('Third Term fee = 0 for final year', 0.0, $term_fees_fy['Third Term']);

$result = runAllocationLogic(170000.0, $term_fees_fy, $term_paid_0, $order_fy);

assert_equal('Two inserts (First + Second only)', 2, count($result['inserts']));
assert_equal('Insert 1 = First Term 60000', 60000.0, $result['inserts'][0]['amount']);
assert_equal('Insert 2 = Second Term 110000', 110000.0, $result['inserts'][1]['amount']);
assert_equal('No remaining', 0.0, $result['remaining']);

// Verify Third Term never appears in inserts
$insertedTerms = array_column($result['inserts'], 'term');
assert_true('Third Term never allocated for final year', !in_array('Third Term', $insertedTerms),
    'Third Term not in inserts: ' . json_encode($insertedTerms));
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.4 — Leftover after all terms paid: remaining > 0
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.4: Excess payment produces leftover (remaining > 0) ---\n";

$result = runAllocationLogic(200000.0, $term_fees, $term_paid, $order);

assert_equal('Three inserts (all terms filled)', 3, count($result['inserts']));
assert_equal('Remaining = 35000 (excess)', 35000.0, $result['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.4 — Final year excess: leftover after combined Second Term fully paid
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.4: Final year excess — leftover after combined Second Term ---\n";

$result_fy = runAllocationLogic(200000.0, $term_fees_fy, $term_paid_0, $order_fy);

assert_equal('Two inserts for final year', 2, count($result_fy['inserts']));
assert_equal('Remaining = 30000 (excess for refund)', 30000.0, $result_fy['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.5 — $allInserted stays true when no INSERT fails (leftover block succeeds)
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.5: \$allInserted remains true when all inserts succeed ---\n";

// Simulate the $allInserted flag logic: initialised before leftover block,
// only set to false if an insert fails. When all succeed, it stays true.
$allInserted = true; // initialised BEFORE leftover block (Bug 2 fix)

// Simulate leftover block where all inserts succeed (return true)
$mockInsertResults = [true, true, true];
foreach ($mockInsertResults as $mockResult) {
    if (!$mockResult) {
        $allInserted = false;
        break;
    }
}

assert_true('$allInserted = true when all leftover inserts succeed', $allInserted === true,
    '$allInserted=' . ($allInserted ? 'true' : 'false'));
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.5 — $allInserted correctly set to false when a leftover insert fails
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.5: \$allInserted = false when a leftover insert fails (Bug 2 fix) ---\n";

$allInserted = true; // initialised BEFORE leftover block

// Simulate leftover block where second insert fails
$mockInsertResults2 = [true, false, true];
foreach ($mockInsertResults2 as $mockResult) {
    if (!$mockResult) {
        $allInserted = false;
        break;
    }
}

// With Bug 2 fixed: $allInserted = true is NOT placed after this block,
// so the false is preserved.
assert_true('$allInserted = false when leftover insert fails', $allInserted === false,
    '$allInserted=' . ($allInserted ? 'true' : 'false'));
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.6 — SS 1 special First Term fee is used when class = SS 1
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.6: SS 1 special First Term fee applied correctly ---\n";

$firstTermSS1Fee = 65000.0; // special fee for SS 1
$firstTermFee    = 55000.0; // standard fee
$class           = 'SS 1';

$resolvedFirstTermFee = ($class === 'SS 1') ? $firstTermSS1Fee : $firstTermFee;
assert_equal('SS 1 uses special First Term fee', 65000.0, $resolvedFirstTermFee);

$term_fees_ss1 = [
    'First Term'  => $resolvedFirstTermFee,
    'Second Term' => 55000.0,
    'Third Term'  => 55000.0,
];
$result_ss1 = runAllocationLogic(65000.0, $term_fees_ss1, $term_paid, $order);

assert_equal('SS 1 First Term insert = 65000', 65000.0, $result_ss1['inserts'][0]['amount'] ?? null);
assert_equal('No remaining after SS 1 First Term', 0.0, $result_ss1['remaining']);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Req 3.6 — Non-SS1 class uses standard First Term fee
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Req 3.6: Non-SS1 class uses standard First Term fee ---\n";

$class_other = 'JSS 1';
$resolvedFee = ($class_other === 'SS 1') ? $firstTermSS1Fee : $firstTermFee;
assert_equal('Non-SS1 uses standard First Term fee', 55000.0, $resolvedFee);
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Bug 5 fix preservation — mysqli_real_escape_string does not alter safe inputs
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Bug 5 preservation: safe inputs unchanged after escaping ---\n";

$safeInputs = [
    'ADM2024001',
    'John Doe',
    '2024/2025',
    'SS 2',
    'Day',
    'Full Term',
    'Bank Transfer',
    'School Fees',
    'No',
];

foreach ($safeInputs as $input) {
    $escaped = mysqli_real_escape_string($con, $input);
    assert_equal("Safe input unchanged: '$input'", $input, $escaped);
}
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Bug 3 fix preservation — single exit after duplicate redirect (source check)
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Bug 3 preservation: only one exit; after duplicate-payment redirect ---\n";

$source = file_get_contents(dirname(__DIR__) . '/pay_tuition.php');

// Find the duplicate-payment detection block
$dupBlockStart = strpos($source, 'Duplicate payment detected');
assert_true('Duplicate payment block found in source', $dupBlockStart !== false);

if ($dupBlockStart !== false) {
    // Extract ~300 chars after the flash_alert line to check exit count
    $snippet = substr($source, $dupBlockStart, 300);
    $exitCount = substr_count($snippet, 'exit;');
    assert_equal('Exactly one exit; in duplicate-payment block', 1, $exitCount);
}
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Bug 4 fix preservation — $amount defined in disapprove scope
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Bug 4 preservation: \$amount assigned in disapprove block ---\n";

// Find the disapprove block in source and verify $amount is assigned before email body
$disapprovePos = strpos($source, "isset(\$_POST['disapprove'])");
assert_true('Disapprove block found in source', $disapprovePos !== false);

if ($disapprovePos !== false) {
    $disapproveSnippet = substr($source, $disapprovePos, 500);
    // Widen snippet to 1500 chars to capture $mail->Body which appears after SMTP config
    $disapproveSnippet = substr($source, $disapprovePos, 1500);
    $amountAssignPos   = strpos($disapproveSnippet, '$amount');
    $emailBodyPos      = strpos($disapproveSnippet, 'mail->Body');
    assert_true(
        '$amount assigned before $mail->Body in disapprove block',
        $amountAssignPos !== false && $emailBodyPos !== false && $amountAssignPos < $emailBodyPos,
        "amount_pos=$amountAssignPos, body_pos=" . json_encode($emailBodyPos)
    );
}
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Bug 1 fix preservation — paidQuery uses studentno column
// ─────────────────────────────────────────────────────────────────────────────
echo "--- Bug 1 preservation: paidQuery filters by studentno, not student_id ---\n";

$paidQueryPos = strpos($source, 'SUM(amount) as paid FROM payment');
assert_true('paidQuery found in source', $paidQueryPos !== false);

if ($paidQueryPos !== false) {
    $paidSnippet = substr($source, $paidQueryPos - 50, 200);
    assert_true(
        'paidQuery uses studentno column',
        strpos($paidSnippet, 'studentno') !== false,
        "Snippet: " . trim($paidSnippet)
    );
    assert_true(
        'paidQuery does NOT use student_id column',
        strpos($paidSnippet, 'student_id') === false,
        "Snippet: " . trim($paidSnippet)
    );
}
echo "\n";

// ─────────────────────────────────────────────────────────────────────────────
// Summary
// ─────────────────────────────────────────────────────────────────────────────
echo "=== Results: $pass passed, $fail failed ===\n";

if ($fail > 0) {
    echo "\nFAIL — One or more preservation properties violated. Regression detected.\n";
    exit(1);
} else {
    echo "\nPASS — All preservation assertions passed. No regressions detected.\n";
    exit(0);
}
