<?php

declare(strict_types=1);

namespace Tests\Framework\Discovery;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\GeneralCache;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Context\ExecutionContext;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\Discovery\Cache\DiscoveryCacheIdentifiers;
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Serializer\Php\PhpSerializer;
use App\Framework\Serializer\Php\PhpSerializerConfig;

describe('DiscoveryService Integration with Console Commands', function () {
    beforeEach(function () {
        // Create fresh container and dependencies for each test
        $this->container = new DefaultContainer();
        $cacheDriver = new InMemoryCache();
        $serializer = new PhpSerializer(PhpSerializerConfig::safe());
        $this->cache = new GeneralCache($cacheDriver, $serializer);
        $this->clock = new SystemClock();
        // Use correct base path for Docker environment
        $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
        $this->pathProvider = new PathProvider($basePath);

        // Register dependencies in container
        $this->container->singleton(Cache::class, $this->cache);
        $this->container->singleton(Clock::class, $this->clock);
        $this->container->singleton(PathProvider::class, $this->pathProvider);

        // Clear cache between tests
        $this->cache->clear();

        // Clear any opcache
        if (function_exists('opcache_reset')) {
            opcache_reset();
        }
    });

    it('properly handles different execution contexts in discovery', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        // Test console context discovery
        $consoleContext = ExecutionContext::forConsole();
        $paths = [$this->pathProvider->getSourcePath()];

        // Perform discovery
        $registry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null);

        expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
        expect($registry->isEmpty())->toBeFalse();

        // Should have discovered ConsoleCommand attributes
        $consoleCommands = $registry->attributes->get(ConsoleCommand::class);
        expect($consoleCommands)->not->toBeEmpty();
    });

    it('finds the correct number of console commands after fresh discovery', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        // Perform fresh discovery
        $registry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null);

        // Get console commands
        $consoleCommands = $registry->attributes->get(ConsoleCommand::class);

        // Should find a reasonable number of commands (at least the demo commands)
        expect(count($consoleCommands))->toBeGreaterThanOrEqual(6); // Demo commands
        expect(count($consoleCommands))->toBeLessThan(100); // Reasonable upper bound

        // Should include demo commands
        $commandNames = [];
        foreach ($consoleCommands as $discovered) {
            $command = $discovered->createAttributeInstance();
            $commandNames[] = $command->name;
        }

        expect($commandNames)->toContain('demo:hello');
        expect($commandNames)->toContain('demo:colors');
        expect($commandNames)->toContain('demo:interactive');
    });

    it('caches discovery results properly', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->cache);

        // First discovery should cache results
        $registry1 = $bootstrapper->bootstrap();

        // Check that cache contains discovery results
        $paths = [$this->pathProvider->getSourcePath()];
        $context = ExecutionContext::detect();
        $cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value);

        $cachedItem = $this->cache->get($cacheKey);
        expect($cachedItem->isHit)->toBeTrue();

        // Second discovery should use cache
        $registry2 = $bootstrapper->bootstrap();

        // Results should be equivalent
        expect($registry1->isEmpty())->toBe($registry2->isEmpty());

        $commands1 = $registry1->attributes->get(ConsoleCommand::class);
        $commands2 = $registry2->attributes->get(ConsoleCommand::class);
        expect(count($commands1))->toBe(count($commands2));
    });

    it('handles cache misses gracefully with fallback discovery', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        // Ensure cache is empty
        $this->cache->clear();

        // Bootstrap should perform fresh discovery when cache is empty
        $registry = $bootstrapper->bootstrap();

        expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
        expect($registry->isEmpty())->toBeFalse();

        // Should have found console commands even without cache
        $consoleCommands = $registry->attributes->get(ConsoleCommand::class);
        expect($consoleCommands)->not->toBeEmpty();
    });

    it('separates cache by execution context', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
        $paths = [$this->pathProvider->getSourcePath()];

        // Create cache keys for different contexts
        $consoleKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'console');
        $cliScriptKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'cli-script');

        // Keys should be different
        expect($consoleKey->toString())->not->toBe($cliScriptKey->toString());

        // Cache data for console context
        $consoleRegistry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null);

        // Check that only console cache is populated
        $consoleCached = $this->cache->get($consoleKey);
        $cliScriptCached = $this->cache->get($cliScriptKey);

        expect($consoleCached->isHit)->toBeTrue();
        expect($cliScriptCached->isHit)->toBeFalse();
    });

    it('handles invalid cached data gracefully', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
        $paths = [$this->pathProvider->getSourcePath()];
        $context = ExecutionContext::detect();

        // Put invalid data in cache
        $cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value);
        $invalidItem = CacheItem::forSetting($cacheKey, 'invalid_data');
        $this->cache->set($invalidItem);

        // Bootstrap should handle invalid cache gracefully and perform fresh discovery
        $registry = $bootstrapper->bootstrap();

        expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
        expect($registry->isEmpty())->toBeFalse();
    });

    it('processes initializers after discovery', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        // Bootstrap should process initializers
        $registry = $bootstrapper->bootstrap();

        // Registry should be registered in container
        expect($this->container->has(DiscoveryRegistry::class))->toBeTrue();
        $containerRegistry = $this->container->get(DiscoveryRegistry::class);
        expect($containerRegistry)->toBe($registry);
    });

    it('detects when discovery is required', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        // Initially should require discovery
        expect($bootstrapper->isDiscoveryRequired())->toBeTrue();

        // After bootstrap should still work
        $bootstrapper->bootstrap();

        // The method should still be callable
        $isRequired = $bootstrapper->isDiscoveryRequired();
        expect($isRequired)->toBeOfType('boolean');
    });

    it('performs incremental discovery when service is available', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        // First bootstrap
        $registry1 = $bootstrapper->bootstrap();

        // Incremental bootstrap should work
        $registry2 = $bootstrapper->incrementalBootstrap();

        expect($registry2)->toBeInstanceOf(DiscoveryRegistry::class);

        // Should have registered new registry in container
        expect($this->container->has(DiscoveryRegistry::class))->toBeTrue();
    });

    it('falls back to full bootstrap when incremental is not possible', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        // Incremental without previous bootstrap should fall back to full bootstrap
        $registry = $bootstrapper->incrementalBootstrap();

        expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
        expect($registry->isEmpty())->toBeFalse();
    });
});

describe('DiscoveryService Performance and Error Handling', function () {
    beforeEach(function () {
        $this->container = new DefaultContainer();
        $cacheDriver = new InMemoryCache();
        $serializer = new PhpSerializer(PhpSerializerConfig::safe());
        $this->cache = new GeneralCache($cacheDriver, $serializer);
        $this->clock = new SystemClock();
        // Use correct base path for Docker environment
        $basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
        $this->pathProvider = new PathProvider($basePath);

        $this->container->singleton(Cache::class, $this->cache);
        $this->container->singleton(Clock::class, $this->clock);
        $this->container->singleton(PathProvider::class, $this->pathProvider);
    });

    it('completes discovery within reasonable time', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        $startTime = microtime(true);
        $registry = $bootstrapper->bootstrap();
        $endTime = microtime(true);

        $duration = $endTime - $startTime;

        // Discovery should complete within 30 seconds (generous for CI)
        expect($duration)->toBeLessThan(30.0);

        // Should still produce valid results
        expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
        expect($registry->isEmpty())->toBeFalse();
    });

    it('handles discovery errors gracefully', function () {
        // Create bootstrapper with invalid path
        $invalidPathProvider = new PathProvider('/nonexistent/path');
        $this->container->instance(PathProvider::class, $invalidPathProvider);

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

        // Should not throw exception, but may produce empty or partial results
        $registry = $bootstrapper->bootstrap();

        expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
        // May be empty due to invalid path, but should not crash
    });

    it('maintains memory usage within reasonable bounds', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);

        $initialMemory = memory_get_usage(true);

        $registry = $bootstrapper->bootstrap();

        $finalMemory = memory_get_usage(true);
        $memoryIncrease = $finalMemory - $initialMemory;

        // Memory increase should be reasonable (less than 50MB)
        expect($memoryIncrease)->toBeLessThan(50 * 1024 * 1024);

        // Should still produce valid results
        expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
    });
});
