<?php

declare(strict_types=1);

namespace Tests\Framework\Console;

use App\Framework\Cache\Cache;
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\GeneralCache;
use App\Framework\Config\AppConfig;
use App\Framework\Console\ConsoleApplication;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\DemoCommand;
use App\Framework\Console\ExitCode;
use App\Framework\Context\ExecutionContext;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\DateTime\Timezone;
use App\Framework\DI\DefaultContainer;
use App\Framework\Discovery\Cache\DiscoveryCacheIdentifiers;
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
use App\Framework\Discovery\InitializerProcessor;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;
use App\Framework\Serializer\Php\PhpSerializer;
use App\Framework\Serializer\Php\PhpSerializerConfig;

describe('Console Discovery End-to-End Integration', function () {
    beforeEach(function () {
        // Create completely fresh environment 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();
        $this->pathProvider = new PathProvider('/home/michael/dev/michaelschiemer');

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

        // Register ReflectionProvider and ExecutionContext dependencies
        $reflectionProvider = new CachedReflectionProvider();
        $executionContext = ExecutionContext::detect();
        $this->container->singleton(ReflectionProvider::class, $reflectionProvider);
        $this->container->singleton(ExecutionContext::class, $executionContext);

        $this->container->singleton(InitializerProcessor::class, fn ($c) => new InitializerProcessor(
            $c,
            $c->get(ReflectionProvider::class),
            $c->get(ExecutionContext::class)
        ));
        $this->container->singleton(DemoCommand::class, fn () => new DemoCommand());

        // App config for testing
        $appConfig = new AppConfig(
            environment: 'testing',
            debug: true,
            timezone: Timezone::UTC,
            locale: 'en'
        );
        $this->container->singleton(AppConfig::class, $appConfig);

        // Clear all caches to ensure fresh discovery
        $this->cache->clear();

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

        // Create test output
        $this->output = new class () implements \App\Framework\Console\ConsoleOutputInterface {
            public array $capturedLines = [];

            public array $errorLines = [];

            public function write(string $message, null|\App\Framework\Console\ConsoleColor|\App\Framework\Console\ConsoleStyle $style = null): void
            {
                $this->capturedLines[] = $message;
            }

            public function writeLine(string $message = '', ?\App\Framework\Console\ConsoleColor $color = null): void
            {
                $this->capturedLines[] = $message;
            }

            public function writeError(string $message): void
            {
                $this->errorLines[] = $message;
                $this->capturedLines[] = "ERROR: $message";
            }

            public function writeSuccess(string $message): void
            {
                $this->capturedLines[] = "SUCCESS: $message";
            }

            public function writeWarning(string $message): void
            {
                $this->capturedLines[] = "WARNING: $message";
            }

            public function writeInfo(string $message): void
            {
                $this->capturedLines[] = "INFO: $message";
            }

            public function newLine(int $count = 1): void
            {
                for ($i = 0; $i < $count; $i++) {
                    $this->capturedLines[] = '';
                }
            }

            public function askQuestion(string $question, ?string $default = null): string
            {
                return $default ?? '';
            }

            public function confirm(string $question, bool $default = false): bool
            {
                return $default;
            }

            public function writeWindowTitle(string $title, int $mode = 0): void
            {
                // No-op for testing
            }
        };
    });

    it('performs complete console command discovery and execution flow', function () {
        // Step 1: Fresh discovery from scratch
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
        $registry = $bootstrapper->bootstrap();

        // Verify discovery found console commands
        $consoleCommands = $registry->attributes->get(ConsoleCommand::class);
        expect($consoleCommands)->not->toBeEmpty();
        expect(count($consoleCommands))->toBeGreaterThanOrEqual(6); // At least the demo commands

        // Step 2: Create console application
        $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);

        // Step 3: Verify command discovery worked
        $argv = ['test-console', 'help'];
        $exitCode = $app->run($argv);

        expect($exitCode)->toBe(ExitCode::SUCCESS->value);

        $output = implode(' ', $this->output->capturedLines);
        expect($output)->toContain('demo:hello');
        expect($output)->toContain('demo:colors');
        expect($output)->not->toContain('Keine Kommandos verfügbar');

        // Step 4: Execute specific command
        $this->output->capturedLines = []; // Clear output
        $argv = ['test-console', 'demo:hello', 'EndToEndTest'];
        $exitCode = $app->run($argv);

        expect($exitCode)->toBe(ExitCode::SUCCESS->value);

        $output = implode(' ', $this->output->capturedLines);
        expect($output)->toContain('Hallo, EndToEndTest!');
    });

    it('handles cache-miss scenario correctly', function () {
        // Ensure cache is completely empty
        $this->cache->clear();

        // Verify cache is empty for discovery
        $paths = [$this->pathProvider->getSourcePath()];
        $context = ExecutionContext::detect();
        $cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value);

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

        // Create console application (should trigger fresh discovery)
        $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);

        // Should work despite cache miss
        $argv = ['test-console', 'demo:hello'];
        $exitCode = $app->run($argv);

        expect($exitCode)->toBe(ExitCode::SUCCESS->value);

        // Verify cache is now populated
        $cachedItem = $this->cache->get($cacheKey);
        expect($cachedItem->isHit)->toBeTrue();
    });

    it('handles context-specific cache separation correctly', function () {
        // Test that different execution contexts have separate caches
        $paths = [$this->pathProvider->getSourcePath()];

        $consoleKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'console');
        $cliScriptKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'cli-script');

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

        // Perform discovery (will cache under current context)
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
        $registry = $bootstrapper->bootstrap();

        // One cache should be hit, the other should not
        $consoleCached = $this->cache->get($consoleKey);
        $cliScriptCached = $this->cache->get($cliScriptKey);

        // At least one should be hit (depending on current context)
        $anyCacheHit = $consoleCached->isHit || $cliScriptCached->isHit;
        expect($anyCacheHit)->toBeTrue();
    });

    it('discovers minimum expected number of commands', function () {
        // Fresh discovery to get actual count
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
        $registry = $bootstrapper->bootstrap();

        $consoleCommands = $registry->attributes->get(ConsoleCommand::class);
        $commandCount = count($consoleCommands);

        // Should find at least:
        // - 6 demo commands (hello, colors, interactive, menu, simple-menu, wizard)
        // - Various framework commands (db:migrate, db:status, etc.)
        expect($commandCount)->toBeGreaterThanOrEqual(10);

        // Verify specific demo commands are present
        $commandNames = [];
        foreach ($consoleCommands as $discovered) {
            $command = $discovered->createAttributeInstance();
            $commandNames[] = $command->name;
        }

        $expectedDemoCommands = [
            'demo:hello',
            'demo:colors',
            'demo:interactive',
            'demo:menu',
            'demo:simple-menu',
            'demo:wizard',
        ];

        foreach ($expectedDemoCommands as $expectedCommand) {
            expect($commandNames)->toContain($expectedCommand);
        }
    });

    it('handles no duplicate command names', function () {
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
        $registry = $bootstrapper->bootstrap();

        $consoleCommands = $registry->attributes->get(ConsoleCommand::class);

        $commandNames = [];
        $duplicates = [];

        foreach ($consoleCommands as $discovered) {
            $command = $discovered->createAttributeInstance();

            if (in_array($command->name, $commandNames)) {
                $duplicates[] = $command->name;
            } else {
                $commandNames[] = $command->name;
            }
        }

        // Should have no duplicate command names
        expect($duplicates)->toBeEmpty("Found duplicate commands: " . implode(', ', $duplicates));
    });

    it('recovers gracefully when fallback discovery is triggered', function () {
        // Simulate a scenario where initial discovery fails or finds no commands
        // by registering an empty discovery registry first
        $emptyRegistry = new \App\Framework\Discovery\Results\DiscoveryRegistry(
            attributes: new \App\Framework\Discovery\Results\AttributeRegistry([]),
            interfaces: new \App\Framework\Discovery\Results\InterfaceRegistry([]),
            templates: new \App\Framework\Discovery\Results\TemplateRegistry([])
        );

        $this->container->singleton(\App\Framework\Discovery\Results\DiscoveryRegistry::class, $emptyRegistry);

        // Create console application - should trigger fallback discovery
        $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);

        // Should still be able to execute commands after fallback
        $argv = ['test-console', 'demo:hello'];
        $exitCode = $app->run($argv);

        // May succeed if fallback worked, or fail gracefully if no commands found
        expect($exitCode)->toBeInt();
        expect($exitCode)->toBeGreaterThanOrEqual(0);
    });

    it('validates ConsoleCommand attribute syntax is correct throughout codebase', function () {
        // Perform discovery and verify all found commands have valid syntax
        $bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
        $registry = $bootstrapper->bootstrap();

        $consoleCommands = $registry->attributes->get(ConsoleCommand::class);

        foreach ($consoleCommands as $discovered) {
            // Should be able to create attribute instance without errors
            $command = $discovered->createAttributeInstance();

            expect($command)->toBeInstanceOf(ConsoleCommand::class);
            expect($command->name)->not->toBeEmpty();
            expect($command->description)->not->toBeEmpty();

            // Command name should follow expected format
            expect($command->name)->toMatch('/^[a-z]+:[a-z\-]+$/');
        }
    });

    it('completes full console session within reasonable time', function () {
        $startTime = microtime(true);

        // Complete flow: discovery -> application -> command execution
        $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);

        // Execute a command
        $argv = ['test-console', 'demo:hello', 'PerformanceTest'];
        $exitCode = $app->run($argv);

        $endTime = microtime(true);
        $duration = $endTime - $startTime;

        // Should complete within reasonable time (20 seconds for CI)
        expect($duration)->toBeLessThan(20.0);

        // Should still succeed
        expect($exitCode)->toBe(ExitCode::SUCCESS->value);

        $output = implode(' ', $this->output->capturedLines);
        expect($output)->toContain('Hallo, PerformanceTest!');
    });

    it('maintains memory usage within bounds during discovery and execution', function () {
        $initialMemory = memory_get_usage(true);

        // Perform complete console flow
        $app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);

        $argv = ['test-console', 'demo:colors'];
        $exitCode = $app->run($argv);

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

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

        expect($exitCode)->toBe(ExitCode::SUCCESS->value);
    });
});
