<?php

namespace MintyPHP\Tests\Architecture;

use PHPUnit\Framework\TestCase;

/**
 * Enforces that every instance-based repository class has a co-located interface
 * and correctly implements it.
 *
 * Excluded from this contract (static-method-only classes, no instance interface possible):
 * - TenantCustomFieldDefinitionRepository
 * - TenantCustomFieldOptionRepository
 * - UserCustomFieldValueRepository
 * - UserCustomFieldValueOptionRepository
 * - PageRepository
 * - PageContentRepository
 *
 * Also excluded (utility, not a domain repository):
 * - RepoQuery
 */
class RepositoryInterfaceContractTest extends TestCase
{
    use ProjectFileAssertionSupport;

    /** @var list<string> */
    private const EXCLUDED_CLASSES = [
        'MintyPHP\\Repository\\Content\\PageContentRepository',
        'MintyPHP\\Repository\\Content\\PageRepository',
        'MintyPHP\\Repository\\CustomField\\TenantCustomFieldDefinitionRepository',
        'MintyPHP\\Repository\\CustomField\\TenantCustomFieldOptionRepository',
        'MintyPHP\\Repository\\CustomField\\UserCustomFieldValueRepository',
        'MintyPHP\\Repository\\CustomField\\UserCustomFieldValueOptionRepository',
        'MintyPHP\\Repository\\Support\\RepoQuery',
    ];

    /**
     * @return array<string, array{class: string, interfaceClass: string, interfaceFile: string}>
     */
    private function collectRepositoryPairs(): array
    {
        $root = $this->projectRootPath();
        $repoDir = $root . '/core/Repository';

        $pairs = [];

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($repoDir, \RecursiveDirectoryIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            /** @var \SplFileInfo $file */
            if ($file->getExtension() !== 'php') {
                continue;
            }

            $filename = $file->getFilename();

            // Only concrete repository files (not interfaces, not utilities)
            if (!str_ends_with($filename, 'Repository.php')) {
                continue;
            }
            if (str_ends_with($filename, 'RepositoryInterface.php')) {
                continue;
            }

            // Derive PSR-4 class name: core/Repository/Foo/Bar.php → MintyPHP\Repository\Foo\Bar
            $relativePath = ltrim(str_replace($root, '', $file->getPathname()), '/');
            $className = 'MintyPHP\\' . str_replace(['/', '.php'], ['\\', ''], substr($relativePath, strlen('core/')));

            if (in_array($className, self::EXCLUDED_CLASSES, true)) {
                continue;
            }

            $interfaceClass = $className . 'Interface';
            $interfaceFile = substr($relativePath, 0, -strlen('.php')) . 'Interface.php';

            $pairs[$className] = [
                'class' => $className,
                'interfaceClass' => $interfaceClass,
                'interfaceFile' => $interfaceFile,
            ];
        }

        ksort($pairs);
        return $pairs;
    }

    public function testEveryRepositoryHasACoLocatedInterface(): void
    {
        $pairs = $this->collectRepositoryPairs();
        $this->assertNotEmpty($pairs, 'No repository classes found — check path resolution.');

        $missing = [];
        foreach ($pairs as $className => $pair) {
            $root = $this->projectRootPath();
            if (!file_exists($root . '/' . $pair['interfaceFile'])) {
                $missing[] = $pair['interfaceFile'] . ' (for ' . $className . ')';
            }
        }

        $this->assertSame(
            [],
            $missing,
            "Missing interface files:\n" . implode("\n", $missing)
        );
    }

    public function testEveryRepositoryImplementsItsInterface(): void
    {
        $pairs = $this->collectRepositoryPairs();

        $violations = [];
        foreach ($pairs as $className => $pair) {
            if (!class_exists($className)) {
                $violations[] = $className . ' — class not found (autoload issue?)';
                continue;
            }

            $implemented = class_implements($className);
            if ($implemented === false || !in_array($pair['interfaceClass'], $implemented, true)) {
                $violations[] = $className . ' does not implement ' . $pair['interfaceClass'];
            }
        }

        $this->assertSame(
            [],
            $violations,
            "Repository interface contract violations:\n" . implode("\n", $violations)
        );
    }
}
