<?php

declare(strict_types=1);

namespace NoahVet\Reef\Phpunit\Traits;

use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManagerInterface;

trait DatabaseTrait
{
    public ORMExecutor $fixtureExecutor;

    /**
     * @param array<class-string<FixtureInterface>> $fixtureFQCNs
     */
    public function loadData(array $fixtureFQCNs = []): void
    {
        $entityManager = self::getContainer()->get(EntityManagerInterface::class);

        $this->fixtureExecutor = new ORMExecutor(
            $entityManager,
            new ORMPurger($entityManager),
        );

        $this->addFixtures($fixtureFQCNs);
    }

    /**
     * @param array<class-string<FixtureInterface>> $fixtureFQCNs
     */
    protected function addFixtures(array $fixtureFQCNs): void
    {
        $resolvedFQCNs = $this->resolveDependencies($fixtureFQCNs);
        $fixtures = [];

        foreach ($resolvedFQCNs as $fixtureFQCN) {
            $fixture = self::getContainer()->get($fixtureFQCN);

            if (!$fixture instanceof FixtureInterface) {
                throw new \LogicException(\sprintf(
                    'Fixture "%s" must implement %s.',
                    $fixtureFQCN,
                    FixtureInterface::class,
                ));
            }

            $fixtures[] = $fixture;
        }

        if ([] !== $fixtures) {
            $this->fixtureExecutor->execute($fixtures, true);
        }
    }

    /**
     * Resolves fixture dependencies recursively and returns a flat list
     * of all fixtures in the correct order, without duplicates.
     *
     * @param array<class-string<FixtureInterface>> $fixtureFQCNs
     *
     * @return array<class-string<FixtureInterface>>
     */
    private function resolveDependencies(array $fixtureFQCNs): array
    {
        $resolved = [];
        $seen = [];

        foreach ($fixtureFQCNs as $fixtureFQCN) {
            $this->resolveDependenciesRecursive($fixtureFQCN, $resolved, $seen);
        }

        return $resolved;
    }

    /**
     * Recursively resolves dependencies for a single fixture.
     *
     * @param class-string<FixtureInterface>              $fixtureFQCN
     * @param array<class-string<FixtureInterface>>       &$resolved
     * @param array<class-string<FixtureInterface>, bool> &$seen
     */
    private function resolveDependenciesRecursive(string $fixtureFQCN, array &$resolved, array &$seen): void
    {
        // Avoid circular dependencies and duplicates
        if (isset($seen[$fixtureFQCN])) {
            return;
        }

        $seen[$fixtureFQCN] = true;

        $fixture = self::getContainer()->get($fixtureFQCN);

        if (!$fixture instanceof FixtureInterface) {
            throw new \LogicException(\sprintf(
                'Fixture "%s" must implement %s.',
                $fixtureFQCN,
                FixtureInterface::class,
            ));
        }

        // Check if fixture has dependencies
        if ($fixture instanceof DependentFixtureInterface) {
            $dependencies = $fixture->getDependencies();

            foreach ($dependencies as $dependencyFQCN) {
                $this->resolveDependenciesRecursive($dependencyFQCN, $resolved, $seen);
            }
        }

        // Add the fixture after its dependencies
        $resolved[] = $fixtureFQCN;
    }
}
