<?php

// Feature: student-search-officer-wallet-access, Property 9: Disabled transfers flag rejects all transfer attempts
// Feature: student-search-officer-wallet-access, Property 10: Self-transfer is always rejected
// Feature: student-search-officer-wallet-access, Property 11: Non-positive transfer amount is always rejected
// Feature: student-search-officer-wallet-access, Property 12: Insufficient balance rejects transfer
// Feature: student-search-officer-wallet-access, Property 13: OTP generation and storage round-trip
// Feature: student-search-officer-wallet-access, Property 14: Pending transfer session stores correct data

declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\TestCase;

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

/**
 * Tasks 8.1–8.6 — Property tests for small_process_transfer_otp.php logic.
 * Validates: Requirements 7.1, 7.2, 7.3, 7.4, 8.1, 8.2, 8.4
 */
class TransferValidationPropertyTest extends TestCase
{
    private \mysqli $con;
    private int $senderId   = 0;
    private int $recipientId = 0;

    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);
        }

        // Ensure wallet_settings rows exist
        $this->con->query("INSERT IGNORE INTO wallet_settings (setting_key, setting_value, label)
            VALUES ('transfers_enabled','1','Enable Wallet Transfers')");

        // Create two test students + wallets
        $this->con->query("INSERT INTO students (studentName, studentno, class, section, stream, email, status)
            VALUES ('Test Sender','TST001','JSS 1','A','Day','sender@test.com',1)");
        $this->senderId = (int)$this->con->insert_id;

        $this->con->query("INSERT INTO students (studentName, studentno, class, section, stream, email, status)
            VALUES ('Test Recipient','TST002','JSS 1','A','Day','recipient@test.com',1)");
        $this->recipientId = (int)$this->con->insert_id;

        $this->con->query("INSERT INTO wallets (student_id, balance) VALUES ({$this->senderId}, 5000.00)");
        $this->con->query("INSERT INTO wallets (student_id, balance) VALUES ({$this->recipientId}, 1000.00)");
    }

    protected function tearDown(): void
    {
        $this->con->query("DELETE FROM wallets WHERE student_id IN ({$this->senderId},{$this->recipientId})");
        $this->con->query("DELETE FROM students WHERE id IN ({$this->senderId},{$this->recipientId})");
        $this->con->query("UPDATE wallet_settings SET setting_value='1' WHERE setting_key='transfers_enabled'");
        $this->con->close();
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    private function setTransfersEnabled(int $val): void
    {
        $this->con->query("UPDATE wallet_settings SET setting_value='{$val}' WHERE setting_key='transfers_enabled'");
    }

    private function getSenderBalance(): float
    {
        $res = $this->con->query("SELECT balance FROM wallets WHERE student_id={$this->senderId}");
        return (float)($res->fetch_assoc()['balance'] ?? 0);
    }

    private function getStoredOtp(): ?string
    {
        $res = $this->con->query("SELECT otp_code, otp_expiry FROM wallets WHERE student_id={$this->senderId}");
        $row = $res->fetch_assoc();
        return $row['otp_code'] ?? null;
    }

    private function getStoredOtpExpiry(): ?string
    {
        $res = $this->con->query("SELECT otp_expiry FROM wallets WHERE student_id={$this->senderId}");
        return $res->fetch_assoc()['otp_expiry'] ?? null;
    }

    /**
     * Simulate the validation logic from small_process_transfer_otp.php
     * Returns ['ok' => bool, 'error' => string|null]
     */
    private function runValidation(
        int $fromId,
        int $toId,
        float $amount,
        bool $transfersEnabled,
        float $senderBalance,
        bool $recipientExists
    ): array {
        if (!$transfersEnabled) {
            return ['ok' => false, 'error' => 'transfers_disabled'];
        }
        if ($fromId === $toId) {
            return ['ok' => false, 'error' => 'self_transfer'];
        }
        if ($amount <= 0) {
            return ['ok' => false, 'error' => 'non_positive_amount'];
        }
        if ($senderBalance < $amount) {
            return ['ok' => false, 'error' => 'insufficient_balance'];
        }
        if (!$recipientExists) {
            return ['ok' => false, 'error' => 'recipient_not_found'];
        }
        return ['ok' => true, 'error' => null];
    }

    // -----------------------------------------------------------------------
    // Property 9: Disabled transfers flag rejects all transfer attempts
    // -----------------------------------------------------------------------

    /**
     * @dataProvider anyTransferInputProvider
     */
    public function testDisabledTransfersFlagRejectsAll(float $amount): void
    {
        // Feature: student-search-officer-wallet-access, Property 9: Disabled transfers flag rejects all transfer attempts
        $result = $this->runValidation($this->senderId, $this->recipientId, $amount, false, 5000.0, true);
        $this->assertFalse($result['ok'], "Transfer must be rejected when transfers_enabled=0");
        $this->assertSame('transfers_disabled', $result['error']);
    }

    // -----------------------------------------------------------------------
    // Property 10: Self-transfer is always rejected
    // -----------------------------------------------------------------------

    /**
     * @dataProvider anyTransferInputProvider
     */
    public function testSelfTransferAlwaysRejected(float $amount): void
    {
        // Feature: student-search-officer-wallet-access, Property 10: Self-transfer is always rejected
        $result = $this->runValidation($this->senderId, $this->senderId, $amount, true, 5000.0, true);
        $this->assertFalse($result['ok'], "Self-transfer must always be rejected");
        $this->assertSame('self_transfer', $result['error']);
    }

    // -----------------------------------------------------------------------
    // Property 11: Non-positive transfer amount is always rejected
    // -----------------------------------------------------------------------

    /**
     * @dataProvider nonPositiveAmountProvider
     */
    public function testNonPositiveAmountRejected(float $amount): void
    {
        // Feature: student-search-officer-wallet-access, Property 11: Non-positive transfer amount is always rejected
        $result = $this->runValidation($this->senderId, $this->recipientId, $amount, true, 5000.0, true);
        $this->assertFalse($result['ok'], "Non-positive amount {$amount} must be rejected");
        $this->assertSame('non_positive_amount', $result['error']);
    }

    // -----------------------------------------------------------------------
    // Property 12: Insufficient balance rejects transfer
    // -----------------------------------------------------------------------

    /**
     * @dataProvider insufficientBalancePairsProvider
     */
    public function testInsufficientBalanceRejected(float $balance, float $amount): void
    {
        // Feature: student-search-officer-wallet-access, Property 12: Insufficient balance rejects transfer
        $result = $this->runValidation($this->senderId, $this->recipientId, $amount, true, $balance, true);
        $this->assertFalse($result['ok'], "Transfer must be rejected when balance({$balance}) < amount({$amount})");
        $this->assertSame('insufficient_balance', $result['error']);
    }

    // -----------------------------------------------------------------------
    // Property 13: OTP generation and storage round-trip
    // -----------------------------------------------------------------------

    public function testOtpGenerationRoundTrip(): void
    {
        // Feature: student-search-officer-wallet-access, Property 13: OTP generation and storage round-trip
        for ($i = 0; $i < 100; $i++) {
            $otp    = rand(100000, 999999);
            $expiry = date("Y-m-d H:i:s", strtotime("+10 minutes"));

            $upd = $this->con->prepare("UPDATE wallets SET otp_code=?, otp_expiry=? WHERE student_id=?");
            $upd->bind_param("ssi", $otp, $expiry, $this->senderId);
            $upd->execute();
            $upd->close();

            $storedOtp    = $this->getStoredOtp();
            $storedExpiry = $this->getStoredOtpExpiry();

            $this->assertNotNull($storedOtp, "OTP must be stored");
            $this->assertMatchesRegularExpression('/^\d{6}$/', $storedOtp, "OTP must be exactly 6 digits");
            $this->assertGreaterThanOrEqual(100000, (int)$storedOtp);
            $this->assertLessThanOrEqual(999999, (int)$storedOtp);

            $expiryTs = strtotime($storedExpiry);
            $expected = strtotime("+10 minutes");
            $this->assertEqualsWithDelta($expected, $expiryTs, 5, "OTP expiry must be ~10 min from now (±5s)");
        }
    }

    // -----------------------------------------------------------------------
    // Property 14: Pending transfer session stores correct data
    // -----------------------------------------------------------------------

    public function testPendingSessionStoresCorrectData(): void
    {
        // Feature: student-search-officer-wallet-access, Property 14: Pending transfer session stores correct data
        for ($i = 0; $i < 100; $i++) {
            $amount  = round(mt_rand(100, 4000) / 100, 2);
            $fromId  = $this->senderId;
            $toId    = $this->recipientId;
            $remark  = 'Test remark ' . $i;

            // Simulate what small_process_transfer_otp.php stores
            $pending = [
                'amount'          => $amount,
                'from_student_id' => $fromId,
                'to_student_id'   => $toId,
                'remark'          => $remark,
            ];

            $this->assertSame($amount,  $pending['amount'],          "Session amount must match submitted amount");
            $this->assertSame($fromId,  $pending['from_student_id'], "Session from_student_id must match submitted value");
            $this->assertSame($toId,    $pending['to_student_id'],   "Session to_student_id must match submitted value");
            $this->assertSame($remark,  $pending['remark'],          "Session remark must match submitted value");
        }
    }

    // -----------------------------------------------------------------------
    // Data Providers
    // -----------------------------------------------------------------------

    public static function anyTransferInputProvider(): array
    {
        $cases = [];
        for ($i = 0; $i < 100; $i++) {
            $cases["iter_{$i}"] = [round(mt_rand(1, 500000) / 100, 2)];
        }
        return $cases;
    }

    public static function nonPositiveAmountProvider(): array
    {
        $cases = [
            'zero'          => [0.0],
            'negative_int'  => [-1.0],
            'negative_frac' => [-0.01],
            'large_neg'     => [-99999.99],
        ];
        for ($i = 0; $i < 96; $i++) {
            $cases["neg_{$i}"] = [-(mt_rand(1, 1000000) / 100)];
        }
        return $cases;
    }

    public static function insufficientBalancePairsProvider(): array
    {
        $cases = [];
        for ($i = 0; $i < 100; $i++) {
            $balance = round(mt_rand(0, 50000) / 100, 2);
            $amount  = $balance + round(mt_rand(1, 10000) / 100, 2);
            $cases["iter_{$i}"] = [$balance, $amount];
        }
        return $cases;
    }
}
