<?php

namespace Tests\Feature\Security\Financial;

use App\Models\Account;
use App\Models\Transaction;
use App\Models\TransactionType;
use App\Models\User;
use App\Models\Organization;
use App\Models\Bank;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

/**
 * Transaction Integrity Tests
 *
 * Tests the database-level immutability and integrity of transactions:
 * - Transactions can only be created (INSERT) and read (SELECT)
 * - Transactions cannot be modified (UPDATE) or deleted (DELETE)
 * - Balance calculations use window functions correctly
 * - Concurrent transaction handling works properly
 * - Transaction validation rules are enforced
 *
 * CRITICAL FINANCIAL SECURITY: Transaction immutability is enforced at MySQL
 * user permission level. These tests verify that enforcement.
 *
 * @group security
 * @group critical
 * @group financial
 * @group transaction-integrity
 */
class TransactionIntegrityTest extends TestCase
{
    use RefreshDatabase;

    // ==========================================
    // DATABASE IMMUTABILITY TESTS
    // ==========================================

    /**
     * Test that transactions can be created (INSERT allowed)
     */
    public function test_transactions_can_be_created()
    {
        $user = User::factory()->create();
        $this->actingAs($user, 'web');

        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        $transaction = Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => 60, // 1 hour
            'description' => 'Test payment',
            'transaction_type_id' => 1, // Work
            'creator_user_id' => $user->id,
        ]);

        $this->assertDatabaseHas('transactions', [
            'id' => $transaction->id,
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => 60,
        ]);
    }

    /**
     * Test that transactions can be read (SELECT allowed)
     */
    public function test_transactions_can_be_read()
    {
        $user = User::factory()->create();
        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        $transaction = Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => 120, // 2 hours
            'description' => 'Test payment',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);

        $retrieved = Transaction::find($transaction->id);

        $this->assertNotNull($retrieved);
        $this->assertEquals(120, $retrieved->amount);
        $this->assertEquals('Test payment', $retrieved->description);
    }

    /**
     * Test that attempting to update transactions via Eloquent throws exception
     *
     * Note: This tests application-level protection. Database-level protection
     * (MySQL user permissions) should also prevent UPDATE at DB level.
     */
    public function test_transactions_cannot_be_updated_via_eloquent()
    {
        $user = User::factory()->create();
        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        $transaction = Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => 60,
            'description' => 'Original description',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);

        $originalAmount = $transaction->amount;
        $originalDescription = $transaction->description;

        // Try to modify the transaction
        $transaction->amount = 120;
        $transaction->description = 'Modified description';

        // If save() succeeds, verify data hasn't changed in database
        // If MySQL permissions are correctly set, save() should fail
        try {
            $transaction->save();

            // If save succeeded, check if data actually changed
            $transaction->refresh();

            // Transaction should remain unchanged (either save failed silently or DB rejected UPDATE)
            $this->assertEquals($originalAmount, $transaction->amount,
                'Transaction amount should not be modifiable');
            $this->assertEquals($originalDescription, $transaction->description,
                'Transaction description should not be modifiable');

        } catch (\Exception $e) {
            // Expected: UPDATE permission denied
            $this->assertTrue(true, 'Transaction update correctly prevented');
        }
    }

    /**
     * Test that attempting to delete transactions via Eloquent throws exception
     *
     * Note: This tests application-level protection. Database-level protection
     * (MySQL user permissions) should also prevent DELETE at DB level.
     */
    public function test_transactions_cannot_be_deleted_via_eloquent()
    {
        $user = User::factory()->create();
        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        $transaction = Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => 60,
            'description' => 'Test payment',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);

        $transactionId = $transaction->id;

        // Try to delete the transaction
        try {
            $transaction->delete();

            // If delete succeeded, verify transaction still exists
            $this->assertDatabaseHas('transactions', ['id' => $transactionId],
                'Transaction should not be deletable');

        } catch (\Exception $e) {
            // Expected: DELETE permission denied
            $this->assertTrue(true, 'Transaction deletion correctly prevented');
        }
    }

    /**
     * Test that raw SQL UPDATE is prevented by database permissions
     *
     * This test verifies the MySQL user permission restrictions.
     */
    public function test_raw_sql_update_is_prevented()
    {
        $user = User::factory()->create();
        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        $transaction = Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => 60,
            'description' => 'Original description',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);

        $originalAmount = $transaction->amount;

        // Try to update via raw SQL
        try {
            DB::statement('UPDATE transactions SET amount = ? WHERE id = ?', [120, $transaction->id]);

            // If update succeeded, verify data hasn't changed
            $transaction->refresh();
            $this->assertEquals($originalAmount, $transaction->amount,
                'Raw SQL UPDATE should not modify transaction');

        } catch (\Exception $e) {
            // Expected behavior: Permission denied
            $this->assertStringContainsString('UPDATE command denied', $e->getMessage());
        }
    }

    /**
     * Test that raw SQL DELETE is prevented by database permissions
     */
    public function test_raw_sql_delete_is_prevented()
    {
        $user = User::factory()->create();
        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        $transaction = Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => 60,
            'description' => 'Test payment',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);

        $transactionId = $transaction->id;

        // Try to delete via raw SQL
        try {
            DB::statement('DELETE FROM transactions WHERE id = ?', [$transactionId]);

            // If delete succeeded, verify transaction still exists
            $this->assertDatabaseHas('transactions', ['id' => $transactionId],
                'Raw SQL DELETE should not remove transaction');

        } catch (\Exception $e) {
            // Expected behavior: Permission denied
            $this->assertStringContainsString('DELETE command denied', $e->getMessage());
        }
    }

    // ==========================================
    // BALANCE CALCULATION INTEGRITY TESTS
    // ==========================================

    /**
     * Test that balance calculations are correct for single transaction
     */
    public function test_balance_calculation_single_transaction()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $account1 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user1->id,
        ]);
        $account2 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user2->id,
        ]);

        // Initial balances should be 0
        $this->assertEquals(0, $account1->balance);
        $this->assertEquals(0, $account2->balance);

        // Create transaction: user1 pays user2 60 minutes
        Transaction::create([
            'from_account_id' => $account1->id,
            'to_account_id' => $account2->id,
            'amount' => 60,
            'description' => 'Test payment',
            'transaction_type_id' => 1,
            'creator_user_id' => $user1->id,
        ]);

        // Clear cache to force recalculation
        \Cache::forget("account_balance_{$account1->id}");
        \Cache::forget("account_balance_{$account2->id}");

        // Refresh models
        $account1 = $account1->fresh();
        $account2 = $account2->fresh();

        // user1 should have -60, user2 should have +60
        $this->assertEquals(-60, $account1->balance);
        $this->assertEquals(60, $account2->balance);
    }

    /**
     * Test that balance calculations are correct for multiple transactions
     */
    public function test_balance_calculation_multiple_transactions()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $account1 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user1->id,
        ]);
        $account2 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user2->id,
        ]);

        // Transaction 1: user1 pays user2 60 minutes
        Transaction::create([
            'from_account_id' => $account1->id,
            'to_account_id' => $account2->id,
            'amount' => 60,
            'description' => 'Payment 1',
            'transaction_type_id' => 1,
            'creator_user_id' => $user1->id,
        ]);

        // Transaction 2: user2 pays user1 30 minutes
        Transaction::create([
            'from_account_id' => $account2->id,
            'to_account_id' => $account1->id,
            'amount' => 30,
            'description' => 'Payment 2',
            'transaction_type_id' => 1,
            'creator_user_id' => $user2->id,
        ]);

        // Transaction 3: user1 pays user2 15 minutes
        Transaction::create([
            'from_account_id' => $account1->id,
            'to_account_id' => $account2->id,
            'amount' => 15,
            'description' => 'Payment 3',
            'transaction_type_id' => 1,
            'creator_user_id' => $user1->id,
        ]);

        // Clear cache
        \Cache::forget("account_balance_{$account1->id}");
        \Cache::forget("account_balance_{$account2->id}");

        $account1 = $account1->fresh();
        $account2 = $account2->fresh();

        // user1: -60 + 30 - 15 = -45
        // user2: +60 - 30 + 15 = +45
        $this->assertEquals(-45, $account1->balance);
        $this->assertEquals(45, $account2->balance);
    }

    /**
     * Test that balance calculations handle concurrent transactions correctly
     */
    public function test_balance_calculation_with_concurrent_transactions()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();
        $user3 = User::factory()->create();

        $account1 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user1->id,
        ]);
        $account2 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user2->id,
        ]);
        $account3 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user3->id,
        ]);

        // Simulate multiple transactions happening "simultaneously"
        DB::transaction(function () use ($account1, $account2, $account3, $user1, $user2, $user3) {
            // Multiple transfers from account1
            Transaction::create([
                'from_account_id' => $account1->id,
                'to_account_id' => $account2->id,
                'amount' => 30,
                'description' => 'Concurrent payment 1',
                'transaction_type_id' => 1,
                'creator_user_id' => $user1->id,
            ]);

            Transaction::create([
                'from_account_id' => $account1->id,
                'to_account_id' => $account3->id,
                'amount' => 20,
                'description' => 'Concurrent payment 2',
                'transaction_type_id' => 1,
                'creator_user_id' => $user1->id,
            ]);

            Transaction::create([
                'from_account_id' => $account2->id,
                'to_account_id' => $account1->id,
                'amount' => 10,
                'description' => 'Concurrent payment 3',
                'transaction_type_id' => 1,
                'creator_user_id' => $user2->id,
            ]);
        });

        // Clear cache
        \Cache::forget("account_balance_{$account1->id}");
        \Cache::forget("account_balance_{$account2->id}");
        \Cache::forget("account_balance_{$account3->id}");

        $account1 = $account1->fresh();
        $account2 = $account2->fresh();
        $account3 = $account3->fresh();

        // account1: -30 - 20 + 10 = -40
        // account2: +30 - 10 = +20
        // account3: +20 = +20
        $this->assertEquals(-40, $account1->balance);
        $this->assertEquals(20, $account2->balance);
        $this->assertEquals(20, $account3->balance);

        // Sum of all balances should be 0 (zero-sum system)
        $totalBalance = $account1->balance + $account2->balance + $account3->balance;
        $this->assertEquals(0, $totalBalance, 'Total balance in system should always be zero');
    }

    // ==========================================
    // TRANSACTION VALIDATION TESTS
    // ==========================================

    /**
     * Test that transactions require valid from_account_id
     */
    public function test_transaction_requires_valid_from_account()
    {
        $user = User::factory()->create();
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        $this->expectException(\Exception::class);

        Transaction::create([
            'from_account_id' => 99999, // Non-existent account
            'to_account_id' => $toAccount->id,
            'amount' => 60,
            'description' => 'Test payment',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);
    }

    /**
     * Test that transactions require valid to_account_id
     */
    public function test_transaction_requires_valid_to_account()
    {
        $user = User::factory()->create();
        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);

        $this->expectException(\Exception::class);

        Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => 99999, // Non-existent account
            'amount' => 60,
            'description' => 'Test payment',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);
    }

    /**
     * Test that transactions require positive amount
     */
    public function test_transaction_requires_positive_amount()
    {
        $user = User::factory()->create();
        $fromAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);
        $toAccount = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => User::factory()->create()->id,
        ]);

        // Negative amounts should be rejected
        $this->expectException(\Exception::class);

        Transaction::create([
            'from_account_id' => $fromAccount->id,
            'to_account_id' => $toAccount->id,
            'amount' => -60, // Negative amount
            'description' => 'Test payment',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);
    }

    /**
     * Test that from_account and to_account must be different
     */
    public function test_transaction_requires_different_accounts()
    {
        $user = User::factory()->create();
        $account = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user->id,
        ]);

        $this->expectException(\Exception::class);

        Transaction::create([
            'from_account_id' => $account->id,
            'to_account_id' => $account->id, // Same as from_account
            'amount' => 60,
            'description' => 'Self-payment test',
            'transaction_type_id' => 1,
            'creator_user_id' => $user->id,
        ]);
    }

    // ==========================================
    // ZERO-SUM INTEGRITY TESTS
    // ==========================================

    /**
     * Test that the system maintains zero-sum integrity
     *
     * In a timebanking system, the sum of all account balances
     * should always equal zero (money is neither created nor destroyed
     * except through special currency creation/removal transactions).
     */
    public function test_system_maintains_zero_sum_integrity()
    {
        // Create multiple users and accounts
        $users = User::factory()->count(5)->create();
        $accounts = $users->map(function ($user) {
            return Account::factory()->create([
                'accountable_type' => User::class,
                'accountable_id' => $user->id,
            ]);
        });

        // Create random transactions between accounts
        for ($i = 0; $i < 10; $i++) {
            $fromAccount = $accounts->random();
            $toAccount = $accounts->where('id', '!=', $fromAccount->id)->random();

            Transaction::create([
                'from_account_id' => $fromAccount->id,
                'to_account_id' => $toAccount->id,
                'amount' => rand(10, 100),
                'description' => "Random transaction {$i}",
                'transaction_type_id' => 1,
                'creator_user_id' => $users->random()->id,
            ]);
        }

        // Clear all balance caches
        foreach ($accounts as $account) {
            \Cache::forget("account_balance_{$account->id}");
        }

        // Calculate sum of all balances
        $totalBalance = $accounts->sum(function ($account) {
            return $account->fresh()->balance;
        });

        // Should be exactly 0 (zero-sum system)
        $this->assertEquals(0, $totalBalance,
            'System should maintain zero-sum integrity: sum of all balances must be 0');
    }

    /**
     * Test that transaction creation maintains database consistency
     */
    public function test_transaction_creation_maintains_consistency()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $account1 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user1->id,
        ]);
        $account2 = Account::factory()->create([
            'accountable_type' => User::class,
            'accountable_id' => $user2->id,
        ]);

        $transactionCountBefore = Transaction::count();

        // Create transaction in database transaction
        DB::transaction(function () use ($account1, $account2, $user1) {
            Transaction::create([
                'from_account_id' => $account1->id,
                'to_account_id' => $account2->id,
                'amount' => 60,
                'description' => 'Consistency test',
                'transaction_type_id' => 1,
                'creator_user_id' => $user1->id,
            ]);
        });

        $transactionCountAfter = Transaction::count();

        // Verify transaction was created
        $this->assertEquals($transactionCountBefore + 1, $transactionCountAfter);

        // Verify transaction is retrievable
        $transaction = Transaction::latest()->first();
        $this->assertNotNull($transaction);
        $this->assertEquals($account1->id, $transaction->from_account_id);
        $this->assertEquals($account2->id, $transaction->to_account_id);
        $this->assertEquals(60, $transaction->amount);
    }
}
