<?php

declare(strict_types=1);

namespace Tests\Unit\Framework\DI;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Container;
use App\Framework\DI\ParameterResolver;
use App\Framework\Logging\Attributes\LogChannel as LogChannelAttribute;
use App\Framework\Logging\DefaultLogger;
use App\Framework\Logging\HasChannel;
use App\Framework\Logging\LogChannel;
use App\Framework\Logging\Logger;
use App\Framework\Logging\SupportsChannels;
use App\Framework\Reflection\CachedReflectionProvider;

// Test classes for parameter resolution
final class ServiceWithLogChannelAttribute
{
    public function __construct(
        #[LogChannelAttribute(LogChannel::CACHE)]
        public Logger $logger
    ) {
    }
}

final class ServiceWithMultipleLogChannels
{
    public function testMethod(
        #[LogChannelAttribute(LogChannel::SECURITY)]
        Logger $securityLogger,
        #[LogChannelAttribute(LogChannel::DATABASE)]
        Logger $dbLogger,
        #[LogChannelAttribute(LogChannel::CACHE)]
        Logger $cacheLogger
    ): void {
    }
}

final class ServiceWithOverrideTest
{
    public function __construct(
        #[LogChannelAttribute(LogChannel::CACHE)]
        public Logger $logger
    ) {
    }
}

final class ServiceWithTypedDependency
{
    public function __construct(public Logger $logger)
    {
    }
}

final class ServiceWithDefaultValue
{
    public function __construct(public string $name = 'default-value')
    {
    }
}

final class ServiceWithNullable
{
    public function __construct(public ?string $optionalParam)
    {
    }
}

final class ServiceWithRequiredParam
{
    public function __construct(public string $requiredParam)
    {
    }
}

final class ServiceWithMixedParams
{
    public function complexMethod(
        #[LogChannelAttribute(LogChannel::FRAMEWORK)]
        Logger $attributeLogger,
        Logger $overriddenParam,
        Logger $containerParam,
        string $defaultParam = 'default',
        ?string $nullableParam = null
    ): void {
    }
}

beforeEach(function () {
    $this->container = $this->createMock(Container::class);
    $this->reflectionProvider = new CachedReflectionProvider();
    $this->resolver = new ParameterResolver($this->container, $this->reflectionProvider);
});

describe('ParameterResolver', function () {
    it('resolves parameters with LogChannel attribute', function () {
        // Mock main logger - use real DefaultLogger for simplicity
        $mainLogger = new DefaultLogger();

        $this->container->expects($this->once())
            ->method('get')
            ->with(SupportsChannels::class)
            ->willReturn($mainLogger);

        $className = ClassName::create(ServiceWithLogChannelAttribute::class);
        $params = $this->resolver->resolveMethodParameters($className, '__construct');

        expect($params)->toHaveCount(1);
        expect($params[0])->toBeInstanceOf(Logger::class);
        expect($params[0])->toBeInstanceOf(HasChannel::class);
        expect($params[0]->channel)->toBe(LogChannel::CACHE);
    });

    it('resolves different channel loggers via attribute', function () {
        $mainLogger = new DefaultLogger();

        $this->container->expects($this->exactly(3))
            ->method('get')
            ->with(SupportsChannels::class)
            ->willReturn($mainLogger);

        $className = ClassName::create(ServiceWithMultipleLogChannels::class);
        $params = $this->resolver->resolveMethodParameters($className, 'testMethod');

        expect($params)->toHaveCount(3);

        // Verify each logger has correct channel
        expect($params[0])->toBeInstanceOf(Logger::class);
        expect($params[0])->toBeInstanceOf(HasChannel::class);
        expect($params[0]->channel)->toBe(LogChannel::SECURITY);

        expect($params[1])->toBeInstanceOf(Logger::class);
        expect($params[1])->toBeInstanceOf(HasChannel::class);
        expect($params[1]->channel)->toBe(LogChannel::DATABASE);

        expect($params[2])->toBeInstanceOf(Logger::class);
        expect($params[2])->toBeInstanceOf(HasChannel::class);
        expect($params[2]->channel)->toBe(LogChannel::CACHE);
    });

    it('resolves override values before attributes', function () {
        // Container should NOT be called because override takes priority
        $this->container->expects($this->never())
            ->method('get');

        $overrideLogger = $this->createMock(Logger::class);

        $className = ClassName::create(ServiceWithOverrideTest::class);
        $params = $this->resolver->resolveMethodParameters(
            $className,
            '__construct',
            ['logger' => $overrideLogger]
        );

        expect($params)->toHaveCount(1);
        expect($params[0])->toBe($overrideLogger);
    });

    it('resolves type-based injection when no attribute present', function () {
        $logger = $this->createMock(Logger::class);

        $this->container->expects($this->once())
            ->method('has')
            ->with(Logger::class)
            ->willReturn(true);

        $this->container->expects($this->once())
            ->method('get')
            ->with(Logger::class)
            ->willReturn($logger);

        $className = ClassName::create(ServiceWithTypedDependency::class);
        $params = $this->resolver->resolveMethodParameters($className, '__construct');

        expect($params)->toHaveCount(1);
        expect($params[0])->toBe($logger);
    });

    it('resolves default values when no injection possible', function () {
        $this->container->expects($this->never())
            ->method('get');

        $className = ClassName::create(ServiceWithDefaultValue::class);
        $params = $this->resolver->resolveMethodParameters($className, '__construct');

        expect($params)->toHaveCount(1);
        expect($params[0])->toBe('default-value');
    });

    it('resolves nullable parameters to null', function () {
        $this->container->expects($this->never())
            ->method('get');

        $className = ClassName::create(ServiceWithNullable::class);
        $params = $this->resolver->resolveMethodParameters($className, '__construct');

        expect($params)->toHaveCount(1);
        expect($params[0])->toBeNull();
    });

    it('throws exception when parameter cannot be resolved', function () {
        $this->container->method('has')->willReturn(false);

        $className = ClassName::create(ServiceWithRequiredParam::class);

        expect(fn () => $this->resolver->resolveMethodParameters($className, '__construct'))
            ->toThrow(\InvalidArgumentException::class, 'Cannot resolve parameter');
    });

    it('follows correct resolution priority order', function () {
        $mainLogger = new DefaultLogger();
        $overrideLogger = $this->createMock(Logger::class);
        $containerLogger = $this->createMock(Logger::class);

        $this->container->method('get')
            ->willReturnCallback(function ($type) use ($mainLogger, $containerLogger) {
                return match ($type) {
                    SupportsChannels::class => $mainLogger,
                    Logger::class => $containerLogger,
                    default => throw new \RuntimeException("Unexpected type: {$type}")
                };
            });

        $this->container->method('has')
            ->with(Logger::class)
            ->willReturn(true);

        $className = ClassName::create(ServiceWithMixedParams::class);
        $params = $this->resolver->resolveMethodParameters(
            $className,
            'complexMethod',
            ['overriddenParam' => $overrideLogger]
        );

        expect($params)->toHaveCount(5);

        // Attribute injection
        expect($params[0])->toBeInstanceOf(Logger::class);
        expect($params[0])->toBeInstanceOf(HasChannel::class);
        expect($params[0]->channel)->toBe(LogChannel::FRAMEWORK);

        // Override
        expect($params[1])->toBe($overrideLogger);

        // Container
        expect($params[2])->toBe($containerLogger);

        // Default value
        expect($params[3])->toBe('default');

        // Nullable
        expect($params[4])->toBeNull();
    });
});
