<?php

namespace App\Tests\Shared;

use App\Shared\Tenant\GlobalTables;
use App\Tests\ApiTestCase;
use Doctrine\ORM\EntityManagerInterface;

/**
 * ضد رگرسیون برای فازهای بعدی: entity جدیدی که فردا اضافه شود و کسی محیطش را
 * تعیین نکند، اینجا قرمز می‌شود — نه ماه‌ها بعد با یک نشت داده.
 *
 * شکست این تست یعنی «این کلاس طبقه‌بندی نشده»، نه «تست خراب است».
 */
class TenantSchemaCoverageTest extends ApiTestCase
{
    /** @return class-string[] */
    private function allEntityClasses(): array
    {
        $em = static::getContainer()->get(EntityManagerInterface::class);

        return array_map(
            static fn($meta) => $meta->getName(),
            $em->getMetadataFactory()->getAllMetadata(),
        );
    }

    private function isTenantOwned(string $class): bool
    {
        $em = static::getContainer()->get(EntityManagerInterface::class);

        return $em->getClassMetadata($class)->hasField('entityType');
    }

    /**
     * هر entity دقیقاً یکی از چهار وضعیت را دارد: جفت tenant، سراسری، فرزند
     * aggregate، یا بدهی ثبت‌شده.
     */
    public function testEveryEntityIsClassified(): void
    {
        $unclassified = array_values(array_filter(
            $this->allEntityClasses(),
            fn(string $class) => !$this->isTenantOwned($class)
                && !isset(GlobalTables::ENTITIES[$class])
                && !isset(GlobalTables::AGGREGATE_CHILDREN[$class])
                && !isset(GlobalTables::DEFERRED[$class]),
        ));

        self::assertSame([], $unclassified, sprintf(
            "این entityها نه جفت tenant دارند و نه در GlobalTables ثبت شده‌اند:\n%s",
            implode("\n", $unclassified),
        ));
    }

    /** هیچ کلاسی نباید هم‌زمان در دو دستهٔ GlobalTables باشد. */
    public function testClassificationsDoNotOverlap(): void
    {
        $buckets = [
            'ENTITIES'           => array_keys(GlobalTables::ENTITIES),
            'AGGREGATE_CHILDREN' => array_keys(GlobalTables::AGGREGATE_CHILDREN),
            'DEFERRED'           => array_keys(GlobalTables::DEFERRED),
        ];

        foreach ($buckets as $name => $classes) {
            foreach ($buckets as $otherName => $otherClasses) {
                if ($name === $otherName) {
                    continue;
                }
                self::assertSame(
                    [],
                    array_values(array_intersect($classes, $otherClasses)),
                    "{$name} و {$otherName} کلاس مشترک دارند",
                );
            }
        }
    }

    /**
     * هر فرزند aggregate باید زنجیره‌ای به یک ریشهٔ tenant-دار داشته باشد. بدون این،
     * ثبت‌کردنش در whitelist فقط تست را ساکت می‌کند بی‌آنکه محیطی وجود داشته باشد.
     */
    public function testEveryAggregateChildReachesATenantOwnedRoot(): void
    {
        foreach (array_keys(GlobalTables::AGGREGATE_CHILDREN) as $child) {
            $chain   = [$child];
            $current = $child;

            while (isset(GlobalTables::AGGREGATE_CHILDREN[$current])) {
                $current = GlobalTables::AGGREGATE_CHILDREN[$current];
                self::assertNotContains($current, $chain, 'زنجیرهٔ aggregate حلقه دارد: ' . implode(' → ', $chain));
                $chain[] = $current;
            }

            self::assertTrue(
                $this->isTenantOwned($current),
                sprintf('ریشهٔ %s باید جفت tenant داشته باشد: %s', $child, implode(' → ', $chain)),
            );
        }
    }

    /** فرزندِ خودش نباید جفت tenant داشته باشد — وگرنه دو منبع حقیقت می‌شود. */
    public function testAggregateChildrenDoNotCarryTheirOwnTenant(): void
    {
        foreach (array_keys(GlobalTables::AGGREGATE_CHILDREN) as $child) {
            self::assertFalse(
                $this->isTenantOwned($child),
                "{$child} هم جفت tenant دارد هم فرزند aggregate ثبت شده — یکی را بردار",
            );
        }
    }

    /** هر ثبت باید کلاس موجود و دلیل ناتهی داشته باشد. */
    public function testEveryClassificationIsRealAndJustified(): void
    {
        foreach ([GlobalTables::ENTITIES, GlobalTables::DEFERRED] as $bucket) {
            foreach ($bucket as $class => $reason) {
                self::assertTrue(class_exists($class), "کلاس ثبت‌شده وجود ندارد: {$class}");
                self::assertNotSame('', trim($reason), "دلیل {$class} خالی است");
            }
        }

        foreach (GlobalTables::AGGREGATE_CHILDREN as $child => $root) {
            self::assertTrue(class_exists($child), "کلاس ثبت‌شده وجود ندارد: {$child}");
            self::assertTrue(class_exists($root), "ریشهٔ ثبت‌شده وجود ندارد: {$root}");
        }
    }

    /**
     * فاز ۶ بدهی را صفر کرد. از «رشد نکن» به «صفر بمان»: افزودن دوباره یعنی جدولی
     * بیرون از هر تضمینی مانده، و باید تصمیم آگاهانه باشد نه یک ردشدنِ بی‌صدا.
     */
    public function testThereIsNoUnclassifiedDebtLeft(): void
    {
        self::assertSame(
            [],
            GlobalTables::DEFERRED,
            'بدهی طبقه‌بندی باید صفر بماند؛ هر کلاس یا جفت محیط می‌گیرد یا با دلیل سراسری/فرزند aggregate می‌شود',
        );
    }
}
