<?php

declare(strict_types=1);

use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Exceptions\CyclicDependencyException;

beforeEach(function () {
    $this->container = new DefaultContainer();
});

afterEach(function () {
    $this->container->flush();
});

it('registers itself', function () {
    expect($this->container->has(Container::class))->toBeTrue();
    expect($this->container->has(DefaultContainer::class))->toBeTrue();
    expect($this->container->get(Container::class))->toBe($this->container);
    expect($this->container->get(DefaultContainer::class))->toBe($this->container);
});

it('creates simple class', function () {
    $instance = $this->container->get(SimpleTestClass::class);

    expect($instance)->toBeInstanceOf(SimpleTestClass::class);
});

it('caches instances for same class', function () {
    // Note: DefaultContainer caches instances even for non-singletons
    $instance1 = $this->container->get(SimpleTestClass::class);
    $instance2 = $this->container->get(SimpleTestClass::class);

    expect($instance1)->toBe($instance2);
});

it('binds string class', function () {
    $this->container->bind(TestInterface::class, ConcreteTestClass::class);
    $instance = $this->container->get(TestInterface::class);

    expect($instance)->toBeInstanceOf(ConcreteTestClass::class);
});

it('binds callable', function () {
    $this->container->bind(TestInterface::class, fn () => new ConcreteTestClass('from-callable'));
    $instance = $this->container->get(TestInterface::class);

    expect($instance)->toBeInstanceOf(ConcreteTestClass::class);
    expect($instance->value)->toBe('from-callable');
});

it('binds object', function () {
    $object = new ConcreteTestClass('bound-object');
    $this->container->bind(TestInterface::class, $object);
    $instance = $this->container->get(TestInterface::class);

    expect($instance)->toBe($object);
    expect($instance->value)->toBe('bound-object');
});

it('returns same instance for singleton', function () {
    $this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));

    $instance1 = $this->container->get(TestInterface::class);
    $instance2 = $this->container->get(TestInterface::class);

    expect($instance1)->toBe($instance2);
    expect($instance1->value)->toBe('singleton');
});

it('calls callable only once for singleton', function () {
    $callCount = 0;
    $this->container->singleton(TestInterface::class, function () use (&$callCount) {
        $callCount++;

        return new ConcreteTestClass("call-{$callCount}");
    });

    $instance1 = $this->container->get(TestInterface::class);
    $instance2 = $this->container->get(TestInterface::class);

    expect($instance1)->toBe($instance2);
    expect($callCount)->toBe(1);
    expect($instance1->value)->toBe('call-1');
});

it('stores instance directly', function () {
    $object = new ConcreteTestClass('instance');
    $this->container->instance(TestInterface::class, $object);

    $instance = $this->container->get(TestInterface::class);

    expect($instance)->toBe($object);
});

it('has returns true for existing class', function () {
    expect($this->container->has(SimpleTestClass::class))->toBeTrue();
});

it('has returns true for bound class', function () {
    $this->container->bind(TestInterface::class, ConcreteTestClass::class);

    expect($this->container->has(TestInterface::class))->toBeTrue();
});

it('has returns false for non-existent class', function () {
    expect($this->container->has('NonExistentClass'))->toBeFalse();
});

it('forgets binding', function () {
    $this->container->bind(TestInterface::class, ConcreteTestClass::class);
    expect($this->container->has(TestInterface::class))->toBeTrue();

    $this->container->forget(TestInterface::class);

    expect($this->container->has(TestInterface::class))->toBeFalse();
});

it('forgets singleton and creates new instance', function () {
    $this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));
    $instance1 = $this->container->get(TestInterface::class);

    $this->container->forget(TestInterface::class);

    // After forget, the binding is gone
    expect($this->container->has(TestInterface::class))->toBeFalse();
});

it('resolves dependencies', function () {
    $this->container->bind(TestInterface::class, ConcreteTestClass::class);
    $instance = $this->container->get(ClassWithDependency::class);

    expect($instance)->toBeInstanceOf(ClassWithDependency::class);
    expect($instance->dependency)->toBeInstanceOf(ConcreteTestClass::class);
});

it('throws exception for cyclic dependency', function () {
    $this->container->bind(CyclicA::class, CyclicA::class);
    $this->container->bind(CyclicB::class, CyclicB::class);

    $this->container->get(CyclicA::class);
})->throws(CyclicDependencyException::class, 'Zyklische Abhängigkeit entdeckt');

it('returns registered services', function () {
    $this->container->bind(TestInterface::class, ConcreteTestClass::class);
    $this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));

    $services = $this->container->getRegisteredServices();

    expect($services)->toContain(TestInterface::class);
    expect($services)->toContain(Container::class);
    expect($services)->toContain(DefaultContainer::class);
});

it('flushes all bindings and instances', function () {
    $this->container->bind(TestInterface::class, ConcreteTestClass::class);
    $this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));
    $instance = $this->container->get(TestInterface::class);

    $this->container->flush();

    // Container should re-register itself after flush
    expect($this->container->has(Container::class))->toBeTrue();
    expect($this->container->has(DefaultContainer::class))->toBeTrue();

    // Other bindings should be gone
    expect($this->container->has(TestInterface::class))->toBeFalse();

    // New instance should be different after flush
    $newInstance = $this->container->get(SimpleTestClass::class);
    expect($newInstance)->toBeInstanceOf(SimpleTestClass::class);
});

it('has method invoker available', function () {
    expect($this->container->invoker)->not->toBeNull();
    expect($this->container->invoker)->toBeInstanceOf(\App\Framework\DI\MethodInvoker::class);
});

it('resolves complex dependency chain', function () {
    $this->container->bind(TestInterface::class, ConcreteTestClass::class);
    $instance = $this->container->get(ComplexDependencyClass::class);

    expect($instance)->toBeInstanceOf(ComplexDependencyClass::class);
    expect($instance->classWithDep)->toBeInstanceOf(ClassWithDependency::class);
    expect($instance->classWithDep->dependency)->toBeInstanceOf(ConcreteTestClass::class);
    expect($instance->simple)->toBeInstanceOf(SimpleTestClass::class);
});

// Test helper classes
interface TestInterface
{
    //
}

class SimpleTestClass
{
    public function __construct(public string $value = 'default')
    {
    }
}

class ConcreteTestClass implements TestInterface
{
    public function __construct(public string $value = 'default')
    {
    }
}

class ClassWithDependency
{
    public function __construct(public TestInterface $dependency)
    {
    }
}

class ComplexDependencyClass
{
    public function __construct(
        public ClassWithDependency $classWithDep,
        public SimpleTestClass $simple
    ) {
    }
}

// Cyclic dependency test classes
class CyclicA
{
    public function __construct(public CyclicB $b)
    {
    }
}

class CyclicB
{
    public function __construct(public CyclicA $a)
    {
    }
}
