<?php

// Feature: wallet-logs-and-settings, Property 16: Disabled payments rejects all payment attempts

declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../includes/wallet_helpers.php';

/**
 * Task 11.2 — Property 16: Disabled payments rejects all payment attempts
 *
 * For any payment form submission when payments_enabled is 0 in wallet_settings,
 * the transaction must be rejected and the response must contain the message
 * "Wallet payments are currently disabled by the system administrator."
 * No wallet_transactions row must be inserted and no wallets.balance must be modified.
 *
 * Validates: Requirements 9.1
 */
class Property16DisabledPaymentsTest extends TestCase
{
    private \mysqli $con;

    /** @var array<int, array{student_id: int, original_balance: float}> */
    private array $testStudents = [];

    private const DISABLED_MSG = 'Wallet payments are currently disabled by the system administrator.';

    protected function setUp(): void
    {
        $host = defined('DB_SERVER') ? DB_SERVER : 'localhost';
        $user = defined('DB_USER')   ? DB_USER   : 'root';
        $pass = defined('DB_PASS')   ? DB_PASS   : '';
        $name = defined('DB_NAME')   ? DB_NAME   : 'sms';

        $this->con = new \mysqli($host, $user, $pass, $name);

        if ($this->con->connect_error) {
            $this->markTestSkipped('Cannot connect to DB: ' . $this->con->connect_error);
        }

        // Disable payments
        $this->con->query(
            "INSERT INTO wallet_settings (setting_key, setting_value, label)
             VALUES ('payments_enabled', '0', 'Enable Wallet Payments')
             ON DUPLICATE KEY UPDATE setting_value = '0'"
        );

        // Create test students with wallets for property testing
        for ($i = 0; $i < 3; $i++) {
            $studentno   = 'PBT16_' . uniqid();
            $studentName = 'PBT Student ' . $i;
            $balance     = (float) random_int(1000, 50000);

            $this->con->query(
                "INSERT INTO students (studentno, studentName, class, stream, section, demote_status)
                 VALUES ('{$studentno}', '{$studentName}', 'SS 1', 'Science', 'A', 0)"
            );
            $studentId = (int) $this->con->insert_id;

            $this->con->query(
                "INSERT INTO wallets (student_id, balance)
                 VALUES ({$studentId}, {$balance})
                 ON DUPLICATE KEY UPDATE balance = {$balance}"
            );

            $this->testStudents[] = [
                'student_id'       => $studentId,
                'original_balance' => $balance,
            ];
        }
    }

    protected function tearDown(): void
    {
        // Restore payments_enabled to 1
        $this->con->query(
            "UPDATE wallet_settings SET setting_value = '1' WHERE setting_key = 'payments_enabled'"
        );

        // Remove test students and their wallets
        foreach ($this->testStudents as $s) {
            $sid = $s['student_id'];
            $this->con->query("DELETE FROM wallets WHERE student_id = {$sid}");
            $this->con->query("DELETE FROM students WHERE id = {$sid}");
        }

        $this->con->close();
    }

    // -------------------------------------------------------------------------
    // Helper: simulate the enforcement logic from wallet_pay_fee.php
    // Returns the $msg string that would be set, or null if it would proceed.
    // -------------------------------------------------------------------------
    private function runEnforcementCheck(float $amount, int $studentId): string
    {
        $payments_enabled = (int) getWalletSetting($this->con, 'payments_enabled', 1);
        $min_balance      = (float) getWalletSetting($this->con, 'min_balance', 0);

        // Fetch current balance
        $stmt = $this->con->prepare("SELECT balance FROM wallets WHERE student_id = ?");
        $stmt->bind_param("i", $studentId);
        $stmt->execute();
        $stmt->bind_result($balance);
        $stmt->fetch();
        $stmt->close();
        $balance = (float) $balance;

        if (!$payments_enabled) {
            return "danger|Wallet payments are currently disabled by the system administrator.";
        }

        if ($balance - $amount < $min_balance) {
            return "danger|This payment would reduce the wallet balance below the minimum threshold of ₦" . number_format($min_balance, 2) . ".";
        }

        return ""; // would proceed
    }

    // -------------------------------------------------------------------------
    // Helper: count wallet_transactions rows for a student
    // -------------------------------------------------------------------------
    private function countTransactions(int $studentId): int
    {
        $stmt = $this->con->prepare(
            "SELECT COUNT(*) FROM wallet_transactions WHERE student_id = ?"
        );
        $stmt->bind_param("i", $studentId);
        $stmt->execute();
        $stmt->bind_result($count);
        $stmt->fetch();
        $stmt->close();
        return (int) $count;
    }

    // -------------------------------------------------------------------------
    // Helper: get current wallet balance for a student
    // -------------------------------------------------------------------------
    private function getBalance(int $studentId): float
    {
        $stmt = $this->con->prepare(
            "SELECT balance FROM wallets WHERE student_id = ?"
        );
        $stmt->bind_param("i", $studentId);
        $stmt->execute();
        $stmt->bind_result($balance);
        $stmt->fetch();
        $stmt->close();
        return (float) $balance;
    }

    // -------------------------------------------------------------------------
    // Property 16 — 100 iterations with random amounts and students
    // -------------------------------------------------------------------------

    /**
     * **Validates: Requirements 9.1**
     *
     * For any random amount and any student, when payments_enabled=0,
     * the enforcement logic must:
     *   1. Return a message containing the exact rejection string
     *   2. NOT insert any wallet_transactions row
     *   3. NOT modify wallets.balance
     */
    public function testDisabledPaymentsRejectsAllAttempts(): void
    {
        $iterations = 20;
        $studentCount = count($this->testStudents);

        // Edge-case amounts to include in the random pool
        $edgeCaseAmounts = [0.01, 1.0, 100.0, 999.99, 50000.0, 0.001];

        for ($i = 0; $i < $iterations; $i++) {
            // Pick a random student
            $studentData = $this->testStudents[$i % $studentCount];
            $studentId   = $studentData['student_id'];

            // Generate a random amount (mix of edge cases and random values)
            if ($i < count($edgeCaseAmounts)) {
                $amount = $edgeCaseAmounts[$i];
            } else {
                // Random float between 0.01 and 100000
                $amount = round(mt_rand(1, 10000000) / 100, 2);
            }

            // Record state before enforcement
            $txCountBefore  = $this->countTransactions($studentId);
            $balanceBefore  = $this->getBalance($studentId);

            // Run the enforcement check (simulates the wallet_pay_fee.php logic)
            $msg = $this->runEnforcementCheck($amount, $studentId);

            // 1. Message must be set (non-empty) — payment was rejected
            $this->assertNotEmpty(
                $msg,
                "Iteration {$i}: Expected rejection message for amount={$amount}, student={$studentId}, but got empty msg"
            );

            // 2. Message must contain the exact disabled payments string
            $this->assertStringContainsString(
                self::DISABLED_MSG,
                $msg,
                "Iteration {$i}: Rejection message does not contain the required disabled-payments text. Got: {$msg}"
            );

            // 3. No wallet_transactions row should have been inserted
            $txCountAfter = $this->countTransactions($studentId);
            $this->assertSame(
                $txCountBefore,
                $txCountAfter,
                "Iteration {$i}: wallet_transactions count changed for student={$studentId}, amount={$amount}"
            );

            // 4. wallets.balance must not have changed
            $balanceAfter = $this->getBalance($studentId);
            $this->assertSame(
                $balanceBefore,
                $balanceAfter,
                "Iteration {$i}: wallets.balance changed for student={$studentId}, amount={$amount}"
            );
        }
    }

    /**
     * **Validates: Requirements 9.1**
     *
     * Verify that the exact rejection message matches the spec requirement exactly.
     */
    public function testRejectionMessageMatchesSpecExactly(): void
    {
        $studentData = $this->testStudents[0];
        $msg = $this->runEnforcementCheck(500.0, $studentData['student_id']);

        $this->assertStringContainsString(
            'danger|',
            $msg,
            'Message must be prefixed with danger|'
        );

        $this->assertStringContainsString(
            self::DISABLED_MSG,
            $msg,
            'Rejection message must match the spec exactly'
        );
    }

    /**
     * **Validates: Requirements 9.1**
     *
     * Verify that with payments_enabled=0, the setting is read correctly
     * and the enforcement flag is falsy.
     */
    public function testPaymentsEnabledSettingIsZero(): void
    {
        $value = getWalletSetting($this->con, 'payments_enabled', 1);
        $this->assertSame(0, (int) $value, 'payments_enabled should be 0 in this test context');
    }

    /**
     * **Validates: Requirements 9.1**
     *
     * Zero-amount edge case: even a zero-amount payment attempt must be rejected
     * when payments_enabled=0.
     */
    public function testZeroAmountIsAlsoRejected(): void
    {
        $studentData = $this->testStudents[0];
        $msg = $this->runEnforcementCheck(0.0, $studentData['student_id']);

        $this->assertStringContainsString(
            self::DISABLED_MSG,
            $msg,
            'Zero-amount payment must also be rejected when payments are disabled'
        );
    }
}
