<?php

declare(strict_types=1);

namespace Tests\Framework\Discovery;

use App\Framework\Cache\Cache;
use App\Framework\Cache\GeneralCache;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\SystemClock;
use App\Framework\DI\DefaultContainer;
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;
use PHPUnit\Framework\TestCase;

/**
 * Memory leak test for Discovery system and related caches
 *
 * Tests:
 * - Discovery Cache growth over multiple runs
 * - ReflectionProvider cache size limits
 * - APCU cache growth monitoring
 * - Overall memory usage patterns
 */
final class MemoryLeakTest extends TestCase
{
    private DefaultContainer $container;

    private DiscoveryServiceBootstrapper $bootstrapper;

    private array $memoryBaseline = [];

    protected function setUp(): void
    {
        $this->container = new DefaultContainer();

        // Register required dependencies with concrete implementations
        $this->container->bind(
            \App\Framework\Cache\CacheDriver::class,
            \App\Framework\Cache\Driver\InMemoryCache::class
        );
        $this->container->bind(
            \App\Framework\Serializer\Serializer::class,
            \App\Framework\Serializer\Json\JsonSerializer::class
        );
        $this->container->bind(Cache::class, GeneralCache::class);
        $this->container->instance(PathProvider::class, new PathProvider('/var/www/html'));

        $this->bootstrapper = new DiscoveryServiceBootstrapper(
            $this->container,
            new SystemClock()
        );

        // Take memory baseline
        $this->memoryBaseline = $this->captureMemoryStats();
    }

    public function test_discovery_multiple_runs_memory_growth(): void
    {
        $iterations = 15;
        $memoryGrowthData = [];

        echo "\n=== Starting $iterations Discovery Runs ===\n";

        for ($i = 1; $i <= $iterations; $i++) {
            echo "\n=== Discovery Run $i/$iterations ===\n";

            $memoryBefore = memory_get_usage(true);
            $peakBefore = memory_get_peak_usage(true);

            // Run discovery
            $registry = $this->bootstrapper->bootstrap();

            $memoryAfter = memory_get_usage(true);
            $peakAfter = memory_get_peak_usage(true);

            // Capture detailed stats
            $memoryStats = $this->captureMemoryStats();
            $memoryStats['memory_before'] = $memoryBefore;
            $memoryStats['memory_after'] = $memoryAfter;
            $memoryStats['memory_diff'] = $memoryAfter - $memoryBefore;
            $memoryStats['peak_before'] = $peakBefore;
            $memoryStats['peak_after'] = $peakAfter;
            $memoryStats['peak_diff'] = $peakAfter - $peakBefore;

            $memoryGrowthData[$i] = $memoryStats;

            echo sprintf(
                "Run %d: Memory %s -> %s (diff: %s), Peak %s -> %s (diff: %s)\n",
                $i,
                $this->formatBytes($memoryBefore),
                $this->formatBytes($memoryAfter),
                $this->formatBytes($memoryStats['memory_diff']),
                $this->formatBytes($peakBefore),
                $this->formatBytes($peakAfter),
                $this->formatBytes($memoryStats['peak_diff'])
            );

            // Test ReflectionProvider stats if available
            if ($i % 5 === 0) {
                $this->logReflectionStats($i);
            }

            // Force cleanup but keep container
            unset($registry);
            gc_collect_cycles();

            // Check for immediate memory leak
            if ($memoryStats['memory_diff'] > 100 * 1024 * 1024) { // 100MB per run
                $this->fail("Run $i: Excessive memory usage detected: " . $this->formatBytes($memoryStats['memory_diff']));
            }
        }

        // Comprehensive analysis
        $this->analyzeMemoryGrowth($memoryGrowthData);
        $this->analyzeMemoryTrends($memoryGrowthData);
    }

    private function logReflectionStats(int $iteration): void
    {
        try {
            $reflectionProvider = $this->container->has(ReflectionProvider::class)
                ? $this->container->get(ReflectionProvider::class)
                : new CachedReflectionProvider();

            $stats = $reflectionProvider->getStats();
            echo "  ReflectionCache after run $iteration: " . json_encode($stats->toArray()['caches']['class']) . "\n";
        } catch (\Throwable $e) {
            echo "  ReflectionCache stats unavailable: " . $e->getMessage() . "\n";
        }
    }

    public function test_reflection_cache_size_limits(): void
    {
        echo "\n=== Testing ReflectionProvider Cache Limits ===\n";

        // Get or create ReflectionProvider
        $reflectionProvider = $this->container->has(ReflectionProvider::class)
            ? $this->container->get(ReflectionProvider::class)
            : new CachedReflectionProvider();

        $initialStats = $reflectionProvider->getStats();
        echo "Initial reflection stats: " . json_encode($initialStats->toArray()) . "\n";

        // Run discovery multiple times without clearing cache
        for ($i = 1; $i <= 3; $i++) {
            $this->bootstrapper->bootstrap();

            $stats = $reflectionProvider->getStats();
            echo "After discovery run $i: " . json_encode($stats->toArray()) . "\n";

            // Check if cache is growing unbounded
            $cacheSize = $stats->toArray()['total_cache_size'] ?? 0;
            if ($cacheSize > 50 * 1024 * 1024) { // 50MB limit
                $this->fail("ReflectionProvider cache exceeded 50MB: {$cacheSize} bytes");
            }
        }
    }

    public function test_apcu_cache_growth(): void
    {
        if (! extension_loaded('apcu') || ! apcu_enabled()) {
            $this->markTestSkipped('APCu not available');
        }

        echo "\n=== Testing APCu Cache Growth ===\n";

        $initialInfo = apcu_cache_info();
        echo "Initial APCu info: mem_size={$initialInfo['mem_size']}, num_entries={$initialInfo['num_entries']}\n";

        // Run discovery multiple times
        for ($i = 1; $i <= 3; $i++) {
            $this->bootstrapper->bootstrap();

            $info = apcu_cache_info();
            echo "After run $i: mem_size={$info['mem_size']}, num_entries={$info['num_entries']}\n";

            // Check for excessive growth
            if ($info['num_entries'] > $initialInfo['num_entries'] + 1000) {
                $this->fail("APCu entries grew excessively: {$info['num_entries']} entries");
            }
        }
    }

    public function test_cache_system_memory_usage(): void
    {
        echo "\n=== Testing Cache System Memory Usage ===\n";

        if (! $this->container->has(Cache::class)) {
            $this->markTestSkipped('Cache not available in container');
        }

        $cache = $this->container->get(Cache::class);

        // Monitor cache usage during discovery runs
        for ($i = 1; $i <= 3; $i++) {
            $memoryBefore = memory_get_usage(true);

            $this->bootstrapper->bootstrap();

            $memoryAfter = memory_get_usage(true);
            $memoryDiff = $memoryAfter - $memoryBefore;

            echo "Discovery run $i: Memory diff = " . $this->formatBytes($memoryDiff) . "\n";

            // Check for excessive memory usage
            if ($memoryDiff > 100 * 1024 * 1024) { // 100MB limit per run
                $this->fail("Single discovery run used excessive memory: " . $this->formatBytes($memoryDiff));
            }
        }
    }

    public function test_discovery_cache_invalidation(): void
    {
        echo "\n=== Testing Discovery Cache Invalidation ===\n";

        // Run discovery first time
        $registry1 = $this->bootstrapper->bootstrap();
        $initialMemory = memory_get_usage(true);

        // Run discovery second time (should use cache if enabled)
        $registry2 = $this->bootstrapper->bootstrap();
        $secondMemory = memory_get_usage(true);

        // Run discovery third time with cache clearing
        if ($this->container->has(Cache::class)) {
            $cache = $this->container->get(Cache::class);
            if (method_exists($cache, 'flush')) {
                $cache->flush();
            }
        }

        $registry3 = $this->bootstrapper->bootstrap();
        $thirdMemory = memory_get_usage(true);

        echo "First run: " . $this->formatBytes($initialMemory) . "\n";
        echo "Second run (cached): " . $this->formatBytes($secondMemory) . "\n";
        echo "Third run (cache cleared): " . $this->formatBytes($thirdMemory) . "\n";

        // Memory should not grow excessively between runs
        $maxGrowth = 20 * 1024 * 1024; // 20MB max growth
        if ($thirdMemory - $initialMemory > $maxGrowth) {
            $this->fail("Memory grew excessively between runs: " . $this->formatBytes($thirdMemory - $initialMemory));
        }
    }

    private function captureMemoryStats(): array
    {
        return [
            'php_memory_usage' => memory_get_usage(true),
            'php_memory_peak' => memory_get_peak_usage(true),
            'php_memory_limit' => $this->parseMemoryLimit(ini_get('memory_limit')),
            'timestamp' => microtime(true),
            'apcu_info' => extension_loaded('apcu') && apcu_enabled() ? apcu_cache_info() : null,
        ];
    }

    private function formatMemoryStats(array $stats): string
    {
        $usage = $this->formatBytes($stats['php_memory_usage']);
        $peak = $this->formatBytes($stats['php_memory_peak']);
        $limit = $this->formatBytes($stats['php_memory_limit']);

        $result = "Usage: $usage, Peak: $peak, Limit: $limit";

        if ($stats['apcu_info']) {
            $entries = $stats['apcu_info']['num_entries'];
            $size = $this->formatBytes($stats['apcu_info']['mem_size']);
            $result .= ", APCu: $entries entries, $size";
        }

        return $result;
    }

    private function analyzeMemoryGrowth(array $memoryData): void
    {
        echo "\n=== Memory Growth Analysis ===\n";

        $baseline = $memoryData[1]['php_memory_usage'];
        $maxAllowedGrowth = 50 * 1024 * 1024; // 50MB max total growth

        foreach ($memoryData as $iteration => $stats) {
            $growth = $stats['php_memory_usage'] - $baseline;
            $percentage = ($growth / $baseline) * 100;

            echo "Run $iteration: Growth = " . $this->formatBytes($growth) . " (" . round($percentage, 1) . "%)\n";

            if ($growth > $maxAllowedGrowth) {
                $this->fail("Memory growth exceeded limit at iteration $iteration: " . $this->formatBytes($growth));
            }
        }

        // Check for linear growth (memory leak indicator)
        if (count($memoryData) >= 3) {
            $growthRates = [];
            for ($i = 2; $i <= count($memoryData); $i++) {
                $growthRates[] = $memoryData[$i]['php_memory_usage'] - $memoryData[$i - 1]['php_memory_usage'];
            }

            $avgGrowthRate = array_sum($growthRates) / count($growthRates);
            echo "Average growth per iteration: " . $this->formatBytes($avgGrowthRate) . "\n";

            // If average growth > 10MB per iteration, likely memory leak
            if ($avgGrowthRate > 10 * 1024 * 1024) {
                $this->fail("Detected potential memory leak: average growth " . $this->formatBytes($avgGrowthRate) . " per iteration");
            }
        }
    }

    private function analyzeMemoryTrends(array $memoryData): void
    {
        echo "\n=== Detailed Memory Trend Analysis ===\n";

        $memoryDiffs = [];
        $peakDiffs = [];
        $cumulativeGrowth = 0;

        foreach ($memoryData as $iteration => $stats) {
            $memoryDiffs[] = $stats['memory_diff'];
            $peakDiffs[] = $stats['peak_diff'];
            $cumulativeGrowth += $stats['memory_diff'];

            if ($iteration % 3 === 0) {
                echo sprintf(
                    "Run %d: Cumulative growth: %s, Avg per run: %s\n",
                    $iteration,
                    $this->formatBytes($cumulativeGrowth),
                    $this->formatBytes($cumulativeGrowth / $iteration)
                );
            }
        }

        $avgMemoryDiff = array_sum($memoryDiffs) / count($memoryDiffs);
        $avgPeakDiff = array_sum($peakDiffs) / count($peakDiffs);
        $maxMemoryDiff = max($memoryDiffs);
        $minMemoryDiff = min($memoryDiffs);

        echo "\n=== Memory Pattern Summary ===\n";
        echo "Average memory per run: " . $this->formatBytes($avgMemoryDiff) . "\n";
        echo "Average peak per run: " . $this->formatBytes($avgPeakDiff) . "\n";
        echo "Max memory per run: " . $this->formatBytes($maxMemoryDiff) . "\n";
        echo "Min memory per run: " . $this->formatBytes($minMemoryDiff) . "\n";
        echo "Total cumulative growth: " . $this->formatBytes($cumulativeGrowth) . "\n";

        // Check for linear growth pattern (memory leak indicator)
        $growthTrend = $this->calculateGrowthTrend($memoryDiffs);
        echo "Growth trend: " . ($growthTrend > 0 ? "INCREASING" : "STABLE") . " ($growthTrend per run)\n";

        if ($growthTrend > 1024 * 1024) { // 1MB growth trend
            $this->fail("Detected linear memory growth trend: " . $this->formatBytes($growthTrend) . " per run");
        }

        if ($avgMemoryDiff > 50 * 1024 * 1024) { // 50MB average
            $this->fail("Average memory usage per run too high: " . $this->formatBytes($avgMemoryDiff));
        }
    }

    private function calculateGrowthTrend(array $values): float
    {
        $n = count($values);
        if ($n < 3) {
            return 0;
        }

        $sumX = 0;
        $sumY = 0;
        $sumXY = 0;
        $sumX2 = 0;

        for ($i = 0; $i < $n; $i++) {
            $x = $i + 1;
            $y = $values[$i];
            $sumX += $x;
            $sumY += $y;
            $sumXY += $x * $y;
            $sumX2 += $x * $x;
        }

        // Calculate slope (linear regression)
        $slope = ($n * $sumXY - $sumX * $sumY) / ($n * $sumX2 - $sumX * $sumX);

        return $slope;
    }

    private function formatBytes(int|float $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $unitIndex = 0;

        while ($bytes >= 1024 && $unitIndex < count($units) - 1) {
            $bytes /= 1024;
            $unitIndex++;
        }

        return round($bytes, 2) . ' ' . $units[$unitIndex];
    }

    private function parseMemoryLimit(string $memoryLimit): int
    {
        if ($memoryLimit === '-1') {
            return PHP_INT_MAX;
        }

        $unit = strtolower(substr($memoryLimit, -1));
        $value = (int) substr($memoryLimit, 0, -1);

        return match($unit) {
            'g' => $value * 1024 * 1024 * 1024,
            'm' => $value * 1024 * 1024,
            'k' => $value * 1024,
            default => (int) $memoryLimit,
        };
    }
}
