<?php

declare(strict_types=1);

namespace Tests\Framework\Queue\Performance;

use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Locks\DatabaseDistributedLock;
use App\Framework\Queue\Locks\LockKey;
use App\Framework\Queue\Locks\LockOwner;
use PHPUnit\Framework\TestCase;

final class DistributedLockPerformanceTest extends TestCase
{
    private DatabaseManager $database;

    private DatabaseDistributedLock $lockService;

    protected function setUp(): void
    {
        $this->database = $this->createTestDatabase();
        $this->lockService = new DatabaseDistributedLock($this->database);

        $this->cleanupTestData();
        PerformanceTestHelper::warmupDatabase($this->database->getConnection());
    }

    protected function tearDown(): void
    {
        $this->cleanupTestData();
    }

    public function testLockAcquisitionLatency(): void
    {
        $acquisitionTimes = [];
        $iterations = 1000;

        for ($i = 0; $i < $iterations; $i++) {
            $lockKey = new LockKey("test_lock_{$i}");
            $owner = new LockOwner("owner_{$i}");

            $time = PerformanceTestHelper::measureTime(function () use ($lockKey, $owner) {
                $acquired = $this->lockService->acquire($lockKey, $owner, 30);
                if ($acquired) {
                    $this->lockService->release($lockKey, $owner);
                }

                return $acquired;
            });

            $acquisitionTimes[] = $time;
        }

        $stats = PerformanceTestHelper::calculateStatistics($acquisitionTimes);

        // Validate performance benchmarks
        $this->assertLessThan(2.0, $stats['avg'], 'Average lock acquisition time exceeds 2ms');
        $this->assertLessThan(5.0, $stats['p95'], 'P95 lock acquisition time exceeds 5ms');
        $this->assertLessThan(10.0, $stats['p99'], 'P99 lock acquisition time exceeds 10ms');

        echo "\nLock Acquisition Latency Results:\n";
        echo "Iterations: {$iterations}\n";
        echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";

        PerformanceTestHelper::assertPerformance(
            $acquisitionTimes,
            2.0,
            5.0,
            'Lock acquisition'
        );
    }

    public function testLockReleaseLatency(): void
    {
        $releaseTimes = [];
        $iterations = 1000;

        // Pre-acquire locks
        $locks = [];
        for ($i = 0; $i < $iterations; $i++) {
            $lockKey = new LockKey("release_lock_{$i}");
            $owner = new LockOwner("owner_{$i}");

            $acquired = $this->lockService->acquire($lockKey, $owner, 60);
            if ($acquired) {
                $locks[] = ['key' => $lockKey, 'owner' => $owner];
            }
        }

        // Measure release times
        foreach ($locks as $lock) {
            $time = PerformanceTestHelper::measureTime(function () use ($lock) {
                return $this->lockService->release($lock['key'], $lock['owner']);
            });
            $releaseTimes[] = $time;
        }

        $stats = PerformanceTestHelper::calculateStatistics($releaseTimes);

        // Validate performance benchmarks
        $this->assertLessThan(1.5, $stats['avg'], 'Average lock release time exceeds 1.5ms');
        $this->assertLessThan(3.0, $stats['p95'], 'P95 lock release time exceeds 3ms');
        $this->assertLessThan(8.0, $stats['p99'], 'P99 lock release time exceeds 8ms');

        echo "\nLock Release Latency Results:\n";
        echo "Locks released: " . count($releaseTimes) . "\n";
        echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";

        PerformanceTestHelper::assertPerformance(
            $releaseTimes,
            1.5,
            3.0,
            'Lock release'
        );
    }

    public function testLockContentionPerformance(): void
    {
        $lockKey = new LockKey('contended_lock');
        $concurrentAttempts = 20;
        $attemptsPerWorker = 50;

        $allResults = [];
        $successCounts = [];
        $failureCounts = [];

        // Simulate concurrent lock acquisition attempts
        for ($worker = 0; $worker < $concurrentAttempts; $worker++) {
            $owner = new LockOwner("worker_{$worker}");
            $workerResults = [];
            $successes = 0;
            $failures = 0;

            for ($attempt = 0; $attempt < $attemptsPerWorker; $attempt++) {
                $result = PerformanceTestHelper::measureTimeWithResult(function () use ($lockKey, $owner) {
                    $acquired = $this->lockService->acquire($lockKey, $owner, 1); // 1 second timeout
                    if ($acquired) {
                        // Hold lock briefly then release
                        usleep(100); // 0.1ms
                        $this->lockService->release($lockKey, $owner);

                        return true;
                    }

                    return false;
                });

                $workerResults[] = $result['time_ms'];

                if ($result['result']) {
                    $successes++;
                } else {
                    $failures++;
                }

                // Brief pause between attempts
                usleep(50); // 0.05ms
            }

            $allResults = array_merge($allResults, $workerResults);
            $successCounts[$worker] = $successes;
            $failureCounts[$worker] = $failures;
        }

        $stats = PerformanceTestHelper::calculateStatistics($allResults);
        $totalSuccesses = array_sum($successCounts);
        $totalFailures = array_sum($failureCounts);
        $successRate = $totalSuccesses / ($totalSuccesses + $totalFailures) * 100;

        echo "\nLock Contention Performance Results:\n";
        echo "Concurrent workers: {$concurrentAttempts}\n";
        echo "Total attempts: " . ($totalSuccesses + $totalFailures) . "\n";
        echo "Successful acquisitions: {$totalSuccesses}\n";
        echo "Failed acquisitions: {$totalFailures}\n";
        echo "Success rate: {$successRate}%\n";
        echo "Attempt performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";

        // Under contention, some failures are expected but most should succeed
        $this->assertGreaterThan(50.0, $successRate, 'Lock success rate too low under contention');

        // Performance should degrade gracefully under contention
        $this->assertLessThan(50.0, $stats['avg'], 'Average lock time too high under contention');
        $this->assertLessThan(200.0, $stats['p95'], 'P95 lock time too high under contention');
    }

    public function testLockTimeoutPerformance(): void
    {
        $lockKey = new LockKey('timeout_lock');
        $owner1 = new LockOwner('owner_1');
        $owner2 = new LockOwner('owner_2');

        // First owner acquires the lock
        $acquired = $this->lockService->acquire($lockKey, $owner1, 60);
        $this->assertTrue($acquired, 'Initial lock acquisition should succeed');

        $timeoutResults = [];
        $iterations = 100;

        // Second owner repeatedly tries to acquire with short timeouts
        for ($i = 0; $i < $iterations; $i++) {
            $result = PerformanceTestHelper::measureTimeWithResult(function () use ($lockKey, $owner2) {
                return $this->lockService->acquire($lockKey, $owner2, 0.1); // 100ms timeout
            });

            $timeoutResults[] = $result['time_ms'];
            $this->assertFalse($result['result'], 'Lock acquisition should fail due to timeout');
        }

        // Release the lock
        $this->lockService->release($lockKey, $owner1);

        $stats = PerformanceTestHelper::calculateStatistics($timeoutResults);

        echo "\nLock Timeout Performance Results:\n";
        echo "Timeout attempts: {$iterations}\n";
        echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";

        // Timeout should be close to requested timeout (100ms) but not much longer
        $this->assertGreaterThan(90.0, $stats['avg'], 'Timeout too fast - should wait approximately 100ms');
        $this->assertLessThan(150.0, $stats['avg'], 'Timeout too slow - should be close to 100ms');

        // Timeouts should be consistent
        $this->assertLessThan(30.0, $stats['stddev'], 'Timeout timing too inconsistent');
    }

    public function testLockCleanupPerformance(): void
    {
        $expiredLockCount = 500;
        $validLockCount = 100;

        // Create expired locks
        $pdo = $this->database->getConnection();
        $expiredTime = (new \DateTimeImmutable())->modify('-1 hour');

        for ($i = 0; $i < $expiredLockCount; $i++) {
            $pdo->exec(sprintf(
                "INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('expired_%d', 'owner_%d', '%s', '%s')",
                $i,
                $i,
                $expiredTime->format('Y-m-d H:i:s'),
                $expiredTime->format('Y-m-d H:i:s')
            ));
        }

        // Create valid locks
        $validTime = (new \DateTimeImmutable())->modify('+1 hour');
        for ($i = 0; $i < $validLockCount; $i++) {
            $pdo->exec(sprintf(
                "INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('valid_%d', 'owner_%d', '%s', '%s')",
                $i,
                $i,
                $validTime->format('Y-m-d H:i:s'),
                $validTime->format('Y-m-d H:i:s')
            ));
        }

        // Measure cleanup performance
        $cleanupTime = PerformanceTestHelper::measureTime(function () {
            $this->lockService->cleanupExpiredLocks();
        });

        // Verify cleanup results
        $remaining = $pdo->query('SELECT COUNT(*) FROM distributed_locks')->fetchColumn();

        echo "\nLock Cleanup Performance Results:\n";
        echo "Expired locks created: {$expiredLockCount}\n";
        echo "Valid locks created: {$validLockCount}\n";
        echo "Locks remaining after cleanup: {$remaining}\n";
        echo "Cleanup time: {$cleanupTime}ms\n";

        $this->assertEquals($validLockCount, $remaining, 'Should only clean up expired locks');
        $this->assertLessThan(100.0, $cleanupTime, 'Lock cleanup should complete within 100ms');

        // Test cleanup performance with larger dataset
        $this->cleanupTestData();

        // Create many more expired locks
        $largeExpiredCount = 5000;
        for ($i = 0; $i < $largeExpiredCount; $i++) {
            $pdo->exec(sprintf(
                "INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('large_expired_%d', 'owner_%d', '%s', '%s')",
                $i,
                $i,
                $expiredTime->format('Y-m-d H:i:s'),
                $expiredTime->format('Y-m-d H:i:s')
            ));
        }

        $largeCleanupTime = PerformanceTestHelper::measureTime(function () {
            $this->lockService->cleanupExpiredLocks();
        });

        echo "Large cleanup ({$largeExpiredCount} locks): {$largeCleanupTime}ms\n";

        $this->assertLessThan(500.0, $largeCleanupTime, 'Large cleanup should complete within 500ms');
    }

    public function testHighThroughputLockOperations(): void
    {
        $operationsPerSecond = 500;
        $testDuration = 10; // seconds
        $totalOperations = $operationsPerSecond * $testDuration;

        echo "\nHigh Throughput Lock Operations Test:\n";
        echo "Target: {$operationsPerSecond} operations/second for {$testDuration} seconds\n";

        $loadResult = PerformanceTestHelper::simulateLoad(
            function ($index) {
                $lockKey = new LockKey("throughput_lock_{$index}");
                $owner = new LockOwner("owner_{$index}");

                // Acquire and immediately release
                $acquired = $this->lockService->acquire($lockKey, $owner, 5);
                if ($acquired) {
                    $this->lockService->release($lockKey, $owner);

                    return true;
                }

                return false;
            },
            $totalOperations,
            25, // Moderate concurrency
            $testDuration
        );

        $actualThroughput = $loadResult['throughput_ops_per_sec'];
        $operationTimes = array_column($loadResult['results'], 'time_ms');
        $stats = PerformanceTestHelper::calculateStatistics($operationTimes);

        $successfulOperations = count(array_filter(
            array_column($loadResult['results'], 'result'),
            fn ($result) => $result['result'] === true
        ));

        echo "Actual Throughput: {$actualThroughput} operations/second\n";
        echo "Successful operations: {$successfulOperations}\n";
        echo "Operation Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";

        // Should achieve at least 70% of target throughput
        $this->assertGreaterThan(
            $operationsPerSecond * 0.7,
            $actualThroughput,
            'High throughput lock operations below 70% of target'
        );

        // Most operations should succeed
        $successRate = $successfulOperations / $loadResult['operations_completed'] * 100;
        $this->assertGreaterThan(95.0, $successRate, 'Lock operation success rate too low');

        // Operation times should remain reasonable
        $this->assertLessThan(20.0, $stats['avg'], 'Average operation time too high under load');
        $this->assertLessThan(100.0, $stats['p95'], 'P95 operation time too high under load');
    }

    public function testLockRetryPerformance(): void
    {
        $lockKey = new LockKey('retry_lock');
        $owner1 = new LockOwner('blocking_owner');
        $owner2 = new LockOwner('retry_owner');

        // First owner acquires lock for a short time
        $this->lockService->acquire($lockKey, $owner1, 60);

        // Schedule lock release after 200ms
        $releaseTime = microtime(true) + 0.2;

        $retryAttempts = [];
        $maxRetries = 50;
        $retryDelay = 50; // 50ms between retries

        for ($i = 0; $i < $maxRetries; $i++) {
            $startTime = microtime(true);

            // Release the lock when it's time
            if ($startTime >= $releaseTime && $this->lockService->isLocked($lockKey)) {
                $this->lockService->release($lockKey, $owner1);
            }

            $result = PerformanceTestHelper::measureTimeWithResult(function () use ($lockKey, $owner2) {
                return $this->lockService->acquire($lockKey, $owner2, 0.01); // 10ms timeout
            });

            $retryAttempts[] = [
                'time_ms' => $result['time_ms'],
                'success' => $result['result'],
            ];

            if ($result['result']) {
                // Successfully acquired, release it and stop
                $this->lockService->release($lockKey, $owner2);

                break;
            }

            usleep($retryDelay * 1000); // Convert to microseconds
        }

        $retryTimes = array_column($retryAttempts, 'time_ms');
        $stats = PerformanceTestHelper::calculateStatistics($retryTimes);

        $successfulAttempt = array_search(true, array_column($retryAttempts, 'success'));
        $attemptsUntilSuccess = $successfulAttempt !== false ? $successfulAttempt + 1 : $maxRetries;

        echo "\nLock Retry Performance Results:\n";
        echo "Attempts until success: {$attemptsUntilSuccess}\n";
        echo "Retry performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";

        // Should eventually succeed
        $this->assertNotFalse($successfulAttempt, 'Lock retry should eventually succeed');

        // Retry attempts should be fast (mostly just timeout delays)
        $this->assertLessThan(20.0, $stats['avg'], 'Retry attempts taking too long');
    }

    private function createTestDatabase(): DatabaseManager
    {
        $pdo = new \PDO('sqlite::memory:');
        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

        $pdo->exec('
            CREATE TABLE distributed_locks (
                lock_key TEXT PRIMARY KEY,
                owner_id TEXT NOT NULL,
                acquired_at TEXT NOT NULL,
                expires_at TEXT NOT NULL
            )
        ');

        // Performance-optimized indexes
        $pdo->exec('CREATE INDEX idx_locks_expires ON distributed_locks(expires_at)');
        $pdo->exec('CREATE INDEX idx_locks_owner ON distributed_locks(owner_id)');

        return new DatabaseManager($pdo);
    }

    private function cleanupTestData(): void
    {
        $pdo = $this->database->getConnection();
        $pdo->exec('DELETE FROM distributed_locks');
    }
}
