<?php

declare(strict_types=1);

namespace Tests\Framework\Reflection\Cache;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\Cache\AttributeCache;
use App\Framework\Reflection\Cache\ClassCache;
use App\Framework\Reflection\Cache\MethodCache;
use App\Framework\Reflection\Cache\ParameterCache;
use PHPUnit\Framework\TestCase;
use stdClass;

/**
 * Specific memory leak tests for ClassCache
 */
final class ClassCacheMemoryLeakTest extends TestCase
{
    private ClassCache $cache;

    protected function setUp(): void
    {
        $attributeCache = new AttributeCache();
        $parameterCache = new ParameterCache();
        $methodCache = new MethodCache($parameterCache, $attributeCache);
        $this->cache = new ClassCache($methodCache, $attributeCache);
    }

    /**
     * Test that ClassCache doesn't leak memory with repeated class access
     */
    public function test_class_cache_memory_stability(): void
    {
        $initialMemory = memory_get_usage(true);
        $testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class];

        // Simulate heavy usage
        for ($iteration = 0; $iteration < 100; $iteration++) {
            foreach ($testClasses as $className) {
                $classNameObj = ClassName::create($className);

                // Access different cached properties
                $this->cache->getNativeClass($classNameObj);
                $this->cache->getWrappedClass($classNameObj);
                $this->cache->isInstantiable($classNameObj);
                $this->cache->implementsInterface($classNameObj, 'JsonSerializable');
            }

            if ($iteration % 25 === 0) {
                gc_collect_cycles();
            }
        }

        $finalMemory = memory_get_usage(true);
        $memoryGrowth = $finalMemory - $initialMemory;
        $stats = $this->cache->getStats();

        // Memory growth should be bounded
        $this->assertLessThan(
            2 * 1024 * 1024,
            $memoryGrowth,
            "ClassCache grew by " . number_format($memoryGrowth) . " bytes"
        );

        // Statistics should show reasonable counts
        $counters = $stats->getCounters();
        $this->assertArrayHasKey('classes', $counters);
        $this->assertLessThanOrEqual(
            count($testClasses),
            $counters['classes'],
            "ClassCache should not cache more classes than accessed"
        );

        echo "\nClassCache Memory Growth: " . number_format($memoryGrowth) . " bytes\n";
        echo "Cached Classes: " . $counters['classes'] . "\n";
        echo "Total Cache Items: " . $stats->getTotalCount() . "\n";
    }

    /**
     * Test that cache eviction works if implemented
     */
    public function test_cache_eviction_prevents_unbounded_growth(): void
    {
        $initialStats = $this->cache->getStats();
        $testClasses = $this->generateManyTestClasses(500);

        // Fill cache beyond reasonable limits
        foreach ($testClasses as $className) {
            if (class_exists($className)) {
                $classNameObj = ClassName::create($className);
                $this->cache->getNativeClass($classNameObj);
            }
        }

        $finalStats = $this->cache->getStats();
        $cacheGrowth = $finalStats->getTotalCount() - $initialStats->getTotalCount();

        // If cache has size limits, it should not grow indefinitely
        if ($cacheGrowth > 100) {
            $this->addWarning("Cache grew by {$cacheGrowth} items. Consider implementing cache size limits.");
        }

        echo "\nCache growth: {$cacheGrowth} items\n";
    }

    /**
     * Test that cache flush completely clears memory
     */
    public function test_cache_flush_clears_all_data(): void
    {
        $testClasses = [stdClass::class, TestCase::class, \Exception::class];

        // Fill cache
        foreach ($testClasses as $className) {
            $classNameObj = ClassName::create($className);
            $this->cache->getNativeClass($classNameObj);
            $this->cache->getWrappedClass($classNameObj);
        }

        $statsBeforeFlush = $this->cache->getStats();
        $this->assertGreaterThan(0, $statsBeforeFlush->getTotalCount());

        // Flush cache
        $this->cache->flush();

        $statsAfterFlush = $this->cache->getStats();
        $this->assertEquals(
            0,
            $statsAfterFlush->getTotalCount(),
            "Cache should be empty after flush"
        );

        // Verify all cache types are cleared
        $counters = $statsAfterFlush->getCounters();
        foreach ($counters as $cacheType => $count) {
            $this->assertEquals(0, $count, "Cache type '{$cacheType}' should be empty after flush");
        }
    }

    /**
     * Test individual class cache forget functionality
     */
    public function test_individual_class_forget(): void
    {
        $className1 = stdClass::class;
        $className2 = TestCase::class;

        $classNameObj1 = ClassName::create($className1);
        $classNameObj2 = ClassName::create($className2);

        // Cache both classes
        $this->cache->getNativeClass($classNameObj1);
        $this->cache->getNativeClass($classNameObj2);

        $statsAfterCaching = $this->cache->getStats();
        $this->assertGreaterThanOrEqual(2, $statsAfterCaching->getCounter('classes'));

        // Forget one class
        $this->cache->forget($classNameObj1);

        $statsAfterForget = $this->cache->getStats();
        $this->assertLessThan(
            $statsAfterCaching->getTotalCount(),
            $statsAfterForget->getTotalCount(),
            "Total cache count should decrease after forgetting a class"
        );
    }

    /**
     * Test cache performance with many lookups
     */
    public function test_cache_performance_stability(): void
    {
        $testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class, \SplFileInfo::class];

        // Pre-fill cache
        foreach ($testClasses as $className) {
            $classNameObj = ClassName::create($className);
            $this->cache->getNativeClass($classNameObj);
        }

        $startTime = microtime(true);
        $memoryBefore = memory_get_usage(true);

        // Many cache hits
        for ($i = 0; $i < 1000; $i++) {
            $randomClass = $testClasses[array_rand($testClasses)];
            $classNameObj = ClassName::create($randomClass);
            $class = $this->cache->getNativeClass($classNameObj);
            $this->assertInstanceOf(\ReflectionClass::class, $class);
        }

        $endTime = microtime(true);
        $memoryAfter = memory_get_usage(true);

        $duration = $endTime - $startTime;
        $memoryGrowth = $memoryAfter - $memoryBefore;

        // Performance should be good
        $this->assertLessThan(0.1, $duration, "1000 cache hits took {$duration}s");

        // Memory should not grow significantly with cache hits
        $this->assertLessThan(
            100 * 1024,
            $memoryGrowth,
            "Memory grew by " . number_format($memoryGrowth) . " bytes during cache hits"
        );

        echo "\nCache hit performance: " . number_format(1000 / $duration, 0) . " hits/second\n";
        echo "Memory growth during hits: " . number_format($memoryGrowth) . " bytes\n";
    }

    /**
     * Generate many test class names (some may not exist)
     * @return array<string>
     */
    private function generateManyTestClasses(int $count): array
    {
        $classes = [];
        $baseClasses = [
            stdClass::class,
            TestCase::class,
            \Exception::class,
            \DateTime::class,
            \SplFileInfo::class,
            \ArrayObject::class,
            \RuntimeException::class,
            \InvalidArgumentException::class,
        ];

        for ($i = 0; $i < $count; $i++) {
            $classes[] = $baseClasses[$i % count($baseClasses)];
        }

        return $classes;
    }
}
