<?php
/**
 * Bug Condition Exploration Tests — portal/pay_tuition.php
 *
 * These tests are EXPECTED TO FAIL on unfixed code.
 * Failure confirms each bug exists. Do NOT fix the code or tests when they fail.
 *
 * Run: php portal/tests/test_bug_conditions.php
 *
 * Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5
 */

$pass = 0;
$fail = 0;

function test(string $name, bool $result, string $counterexample): void
{
    global $pass, $fail;
    if ($result) {
        echo "[PASS] $name\n";
        $pass++;
    } else {
        echo "[FAIL] $name\n";
        echo "       Counterexample: $counterexample\n";
        $fail++;
    }
}

// ---------------------------------------------------------------------------
// Bug 1 — Term Paid Query Mismatch
// ---------------------------------------------------------------------------
// The $paidQuery in pay_tuition.php filters by `student_id = '$id'` but the
// payment table stores the student identifier under the `studentno` column.
// This means the query never matches any rows and all $term_paid values come
// back as zero, making fully-paid terms appear unpaid.
//
// Test: read the source and assert the query uses `student_id` (confirming bug).
// Expected on unfixed code: FAIL — because `student_id` IS present (bug confirmed).
// After fix: PASS — because `student_id` will be replaced with `studentno`.
// ---------------------------------------------------------------------------

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

// The bug is that the query uses `student_id` instead of `studentno`.
// We assert the CORRECT behaviour: the query should NOT contain `student_id`
// in the paidQuery context. On unfixed code this assertion fails, confirming the bug.
$paidQuerySection = '';
// Use a broader match: from $paidQuery up to the closing "); of the query
if (preg_match('/\$paidQuery\s*=\s*mysqli_query\s*\(.*?GROUP BY Term[^)]*\)/s', $source, $m)) {
    $paidQuerySection = $m[0];
} elseif (preg_match('/\$paidQuery\s*=\s*mysqli_query\s*\([^;]+;/s', $source, $m)) {
    $paidQuerySection = $m[0];
} else {
    // Fallback: grab the raw text around $paidQuery
    $pos = strpos($source, '$paidQuery');
    if ($pos !== false) {
        $paidQuerySection = substr($source, $pos, 400);
    }
}

$bug1_present = (strpos($paidQuerySection, 'student_id') !== false);
// Assert the correct state: student_id should NOT be in paidQuery (it should use studentno).
// On unfixed code: $bug1_present = true → assertion false → FAIL (bug confirmed).
test(
    'Bug 1 — paidQuery uses studentno (not student_id)',
    !$bug1_present,
    'paidQuery filters by student_id instead of studentno — prior payments are invisible; ' .
    '$term_paid[\'First Term\'] returns 0 despite a prior ₦55,000 payment existing for studentno=ADM001'
);

// ---------------------------------------------------------------------------
// Bug 2 — $allInserted State Loss
// ---------------------------------------------------------------------------
// After the leftover block, the code unconditionally sets $allInserted = true,
// overwriting any false set inside the block when a refund/carry-over INSERT fails.
//
// Simulate: leftover block sets $allInserted = false (INSERT failure), then the
// buggy unconditional assignment resets it to true.
// Assert: $allInserted === false (correct behaviour).
// On unfixed code: $allInserted === true → FAIL (bug confirmed).
// ---------------------------------------------------------------------------

// Simulate the buggy code path exactly as written in pay_tuition.php:
// Inside leftover block a failure occurs:
$allInserted_sim = null; // not yet set

// Leftover block runs — refund INSERT fails:
$refundQueryResult = false; // simulates mysqli_query returning false
if (!$refundQueryResult) {
    $allInserted_sim = false;
}

// Buggy line: unconditional reset AFTER the leftover block (as in the source):
$allInserted_sim = true; // ← this is the bug — overwrites the false above

// Assert the CORRECT behaviour: $allInserted should still be false after a failure.
// On unfixed code: $allInserted_sim === true → assertion false → FAIL (bug confirmed).
test(
    'Bug 2 — $allInserted preserves false after leftover INSERT failure',
    $allInserted_sim === false,
    '$allInserted reset to true after leftover block failure — ' .
    'a failed refund INSERT sets $allInserted=false but the unconditional $allInserted=true ' .
    'on the next line overwrites it, causing the transaction to commit despite the failure'
);

// ---------------------------------------------------------------------------
// Bug 3 — Double exit;
// ---------------------------------------------------------------------------
// After the duplicate-payment redirect header, two consecutive `exit;` statements
// appear. The second is unreachable dead code.
//
// Test: count consecutive `exit;` occurrences after the duplicate redirect header.
// Assert count == 1 (correct). On unfixed code: count == 2 → FAIL (bug confirmed).
// ---------------------------------------------------------------------------

// Extract the duplicate-payment detection block from source
$exitCount = 0;
// Find the region around the duplicate redirect
if (preg_match(
    "/header\s*\(\s*'Location:\s*pay_tuition\.php'\s*\)\s*;((?:\s*exit\s*;)+)/",
    $source,
    $exitMatch
)) {
    // Count how many `exit;` appear consecutively after the header() call
    $exitBlock = $exitMatch[1];
    preg_match_all('/\bexit\s*;/', $exitBlock, $exitMatches);
    $exitCount = count($exitMatches[0]);
}

// Assert the CORRECT behaviour: exactly 1 exit; after the redirect.
// On unfixed code: $exitCount == 2 → assertion false → FAIL (bug confirmed).
test(
    'Bug 3 — exactly one exit; after duplicate-payment redirect',
    $exitCount === 1,
    "Two exit; statements found after duplicate redirect header — " .
    "second exit; is unreachable dead code (found $exitCount consecutive exit; statements)"
);

// ---------------------------------------------------------------------------
// Bug 4 — $amount Undefined in Disapprove Scope
// ---------------------------------------------------------------------------
// The disapprove block references $amount in the email body, but $amount is only
// assigned inside the submit block. In the disapprove scope it is undefined.
//
// Simulate: do NOT assign $amount (as the buggy disapprove block does), then
// build the email body string referencing $amount.
// Assert the string contains the expected amount value.
// On unfixed code: $amount is undefined/empty → FAIL (bug confirmed).
// ---------------------------------------------------------------------------

// Simulate the disapprove block WITHOUT assigning $amount (the bug):
$studentname_sim = 'Test Student';
// $amount is intentionally NOT assigned here — mirroring the buggy disapprove scope

// Suppress the undefined variable notice for the purpose of this test
$emailBody_sim = @"We regret to inform you that your payment of NGN $amount has been disapproved.";

// Assert the CORRECT behaviour: email body should contain the amount "55000".
// On unfixed code: $amount is undefined → empty string → assertion false → FAIL (bug confirmed).
// (In a real disapprove POST, $_POST['original_amount'] = '55000' would be available,
//  but the buggy code never reads it in the disapprove scope.)
test(
    'Bug 4 — $amount defined in disapprove scope (email shows correct amount)',
    strpos($emailBody_sim, '55000') !== false,
    '$amount undefined in disapprove block — email body shows ' .
    '"your payment of NGN  has been disapproved" (blank amount) ' .
    'because $amount is only assigned inside the submit block, not the disapprove block'
);

// ---------------------------------------------------------------------------
// Bug 5 — SQL Injection in Duplicate-Check Query
// ---------------------------------------------------------------------------
// The duplicate-check query interpolates $adno directly without escaping:
//   "SELECT id FROM payment WHERE studentno = '$adno' AND pay_id = '$pay_id' LIMIT 1"
// A crafted $adno can alter the SQL structure.
//
// Test: read the source and assert the duplicate-check query uses
// mysqli_real_escape_string (or a prepared statement) for $adno.
// On unfixed code: raw $adno is used → assertion false → FAIL (bug confirmed).
// ---------------------------------------------------------------------------

// Extract the duplicate-check query from source
$duplicateCheckSection = '';
if (preg_match('/\$duplicateCheck\s*=\s*mysqli_query\s*\(.*?LIMIT 1[^)]*\)/s', $source, $dm)) {
    $duplicateCheckSection = $dm[0];
}

// Check whether the query uses an escaped variable or prepared statement
$usesEscaping = (
    strpos($duplicateCheckSection, 'mysqli_real_escape_string') !== false ||
    strpos($duplicateCheckSection, '$safe_adno') !== false ||
    strpos($duplicateCheckSection, 'prepare(') !== false
);

// Assert the CORRECT behaviour: escaping must be used.
// On unfixed code: raw $adno is interpolated → $usesEscaping = false → FAIL (bug confirmed).
test(
    'Bug 5 — duplicate-check query escapes $adno (no SQL injection)',
    $usesEscaping,
    'Raw $adno interpolated in duplicate-check query — ' .
    'input "' . "' OR '1'='1" . '" would produce: ' .
    "SELECT id FROM payment WHERE studentno = '' OR '1'='1' AND pay_id = '...' LIMIT 1 " .
    '— returning all rows and bypassing the duplicate check'
);

// ---------------------------------------------------------------------------
// Summary
// ---------------------------------------------------------------------------
echo "\n";
echo "Results: $pass passed, $fail failed\n";
echo "\n";
if ($fail > 0) {
    echo "EXPECTED: $fail test(s) failed — this confirms the bugs exist in the unfixed code.\n";
    echo "These tests will PASS once the fixes are applied.\n";
} else {
    echo "All tests passed — bugs have been fixed.\n";
}
