<?php

namespace AutoAdmin\Bundle\Tests\Functional;

use AutoAdmin\Bundle\BrickBuilder\Brick\BrickConfigManager;
use AutoAdmin\Bundle\BrickBuilder\Brick\BrickHandlerManagerInterface;
use AutoAdmin\Bundle\BrickBuilder\Form\ColumnPatternType;
use AutoAdmin\Bundle\BrickBuilder\Storage\ObjectSettingFormat;
use AutoAdmin\Bundle\Content\Context\SiteContext;
use AutoAdmin\Bundle\Content\Duplicator\BrickDuplicator;
use AutoAdmin\Bundle\Content\Duplicator\Manager\DuplicatorManager;
use AutoAdmin\Bundle\Content\Entity\Domain;
use AutoAdmin\Bundle\Content\Entity\Locale;
use AutoAdmin\Bundle\Content\Entity\Site;
use AutoAdmin\Bundle\Content\Site\SiteContextHelper;
use AutoAdmin\Bundle\Media\Model\FileInterface;
use AutoAdmin\Bundle\Tests\Functional\BrickBuilder\Brick\Handler\EntityCollectionBrickHandler;
use AutoAdmin\Bundle\Tests\Functional\BrickBuilder\Brick\Handler\EntityDefaultBrickHandler;
use AutoAdmin\Bundle\Tests\Functional\BrickBuilder\Brick\Handler\EntityDuplicateBrickHandler;
use AutoAdmin\Bundle\Tests\Functional\BrickBuilder\Brick\Handler\EntityKeepBrickHandler;
use AutoAdmin\Bundle\Tests\Functional\BrickBuilder\Brick\Handler\FileBrickHandler;
use AutoAdmin\Bundle\Tests\Functional\BrickBuilder\Brick\Handler\ScalarCollectionBrickHandler;
use AutoAdmin\Bundle\Tests\Functional\Entity\Brick;
use AutoAdmin\Bundle\Tests\Functional\Entity\Page;
use AutoAdmin\Bundle\Tests\Functional\Entity\PageTranslation;
use AutoAdmin\Bundle\Tests\Functional\Model\BrickModel;
use AutoAdmin\Bundle\Tests\Functional\Model\RegexModel;
use DateTime;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

abstract class AbstractDuplicationTest extends KernelTestCase
{
    protected const MOCKED_FILE_NAME = 'file-name.png';

    protected DuplicatorManager $duplicatorManager;
    protected ?Site $site = null;
    protected ?Locale $localeFR = null;
    protected ?Locale $localeEN = null;
    protected ?SiteContext $siteContext = null;

    protected function setUp(): void
    {
        self::bootKernel();

        $this->duplicatorManager = self::getContainer()->get(DuplicatorManager::class);

        $this->loadHandlers();

        $this->loadBaseContext();

        $objectSettingFormat = $this->getMockBuilder(ObjectSettingFormat::class)
            ->disableOriginalConstructor()
            ->getMock();

        $objectSettingFormat->method('stringToObject')->willReturnCallback(function ($stringObject) {
            [$class, $identifier] = explode('#', $stringObject);

            $object = new $class();
            $object->setId($identifier);
            if ($object instanceof FileInterface) {
                $object->setFileName(self::MOCKED_FILE_NAME);
            }

            return $object;
        });

        $reflectionClass = new \ReflectionClass($this->duplicatorManager);
        $reflectionProperty = $reflectionClass->getProperty('duplicators');
        $duplicators = [];
        foreach ($reflectionProperty->getValue($this->duplicatorManager) as $duplicator) {
            if ($duplicator instanceof BrickDuplicator) {
                $reflectionBrickClass = new \ReflectionClass($duplicator);
                $reflectionBrickProperty = $reflectionBrickClass->getProperty('brickFormat');
                $reflectionBrickProperty->setValue($duplicator, $objectSettingFormat);
            }

            $duplicators[] = $duplicator;
        }

        $reflectionProperty->setValue($this->duplicatorManager, $duplicators);

        self::getContainer()->get(SiteContextHelper::class)->loadContext($this->siteContext);
    }

    /**
     * @dataProvider provideEntitiesForDuplication
     */
    public function testDuplication($originalEntity, $expectedProperties): void
    {
        $duplicatedEntity = $this->duplicatorManager->duplicate($originalEntity);

        $this->assertNotNull($duplicatedEntity);
        $this->assertNotSame($originalEntity, $duplicatedEntity);

        $this->handleExpectedProperties($duplicatedEntity, $expectedProperties);
    }

    protected function baseExpectations(array $extra = []): array
    {
        return array_merge(
            ['id' => null, 'createdAt' => new DateTime(), 'updatedAt' => new DateTime()],
            $extra
        );
    }

    protected function expectedTranslation(Locale $locale, array $extra = []): array
    {
        return $this->baseExpectations(array_merge(['locale' => $locale], $extra));
    }

    private function buildBrickTree(array $config, callable $makeNode): mixed
    {
        $rows = array_map(function ($row) use ($makeNode) {
            $columns = array_map(function ($column) use ($makeNode) {
                $items = array_map(
                    fn ($item) => $makeNode($item['type'], $item['settings'], null),
                    $column
                );
                return $makeNode('brick.column', [
                    'center' => true,
                    'css_classes' => 'column',
                    'wrapper' => true,
                ], $items);
            }, $row);
            return $makeNode('brick.row', [
                'center' => true,
                'css_classes' => 'row',
                'wrapper' => true,
                'column_pattern' => ColumnPatternType::PATTERN_COL1_100,
            ], $columns);
        }, $config);

        $section = $makeNode('brick.section', [
            'center' => true,
            'css_classes' => 'section',
            'wrapper' => true,
            'full_width' => true,
            'margin_top' => 1,
            'margin_bottom' => 1,
            'padding_top' => 1,
            'padding_bottom' => 1,
        ], $rows);

        return $makeNode('brick.root', [
            'center' => true,
            'css_classes' => 'root',
            'wrapper' => true,
        ], [$section]);
    }

    protected function createBrick(array $config): Brick
    {
        return $this->buildBrickTree($config, function (string $type, array $settings, ?array $children) {
            $brick = new Brick();
            $brick->setId(123);
            $brick->setCreatedAt(new DateTime('2022-01-01 00:00:00'));
            $brick->setUpdatedAt(new DateTime('2022-01-01 00:00:00'));
            $brick->setType($type);
            $brick->setEnabled(true);
            $brick->setSettings($settings);
            foreach ($children ?? [] as $child) {
                $brick->addChild($child);
            }
            return $brick;
        });
    }

    protected function expectedBrick(array $config): BrickModel
    {
        return $this->buildBrickTree($config, fn (string $type, array $settings, ?array $children) => new BrickModel(
            id: null,
            type: $type,
            enabled: true,
            settings: $settings,
            createdAt: new DateTime(),
            updatedAt: new DateTime(),
            children: $children,
        ));
    }

    protected function createBrickPage(array $frItem, array $enItem): Page
    {
        $frTranslation = $this->createPageTranslation($this->localeFR);
        $frTranslation->setBrick($this->createBrick([[[$frItem]]]));

        $enTranslation = $this->createPageTranslation($this->localeEN);
        $enTranslation->setBrick($this->createBrick([[[$enItem]]]));

        return $this->createPage([$frTranslation, $enTranslation]);
    }

    protected function expectedSingleItemBrick(array $item): BrickModel
    {
        return $this->expectedBrick([[[$item]]]);
    }

    protected function createPage(array $translations = []): Page
    {
        $page = new Page();
        $page->setId(123);
        $page->setCreatedAt(new DateTime('2022-01-01 00:00:00'));
        $page->setUpdatedAt(new DateTime('2022-01-01 00:00:00'));

        foreach ($translations as $translation) {
            $page->addTranslation($translation);
        }

        return $page;
    }

    protected function createPageTranslation(Locale $locale): PageTranslation
    {
        $pageTranslation = new PageTranslation();
        $pageTranslation->setId(123);
        $pageTranslation->setCreatedAt(new DateTime('2022-01-01 00:00:00'));
        $pageTranslation->setUpdatedAt(new DateTime('2022-01-01 00:00:00'));
        $pageTranslation->setLocale($locale);

        return $pageTranslation;
    }

    protected function loadBaseContext(): void
    {
        $this->localeFR = new Locale();
        $this->localeFR->setCode('fr');

        $this->localeEN = new Locale();
        $this->localeEN->setCode('en');

        $domain = new Domain();
        $domain->setDomain('test.fr');
        $domain->addLocale($this->localeFR);
        $domain->addLocale($this->localeEN);

        $this->site = new Site();
        $this->site->setEnabled(true);
        $this->site->setCreatedAt(new DateTime('2022-01-01 00:00:00'));
        $this->site->setUpdatedAt(new DateTime('2022-01-01 00:00:00'));
        $this->site->setTitle('Test');
        $this->site->setSlug('test');
        $this->site->addDomain($domain);

        $this->siteContext = new SiteContext();
        $this->siteContext->setSite($this->site);
        $this->siteContext->setLocale($this->localeFR);
    }

    protected function loadEntities(): void
    {
        $this->loadBaseContext();
    }

    protected function loadHandlers(): void
    {
        $handlers = [
            new ScalarCollectionBrickHandler(),
            new EntityDefaultBrickHandler(),
            new EntityKeepBrickHandler(),
            new EntityDuplicateBrickHandler(),
            new FileBrickHandler(),
            new EntityCollectionBrickHandler(),
        ];

        // suboptimal, but ok for now
        BrickConfigManager::configureHandlers($handlers);

        $brickHandlerManager = self::getContainer()->get(BrickHandlerManagerInterface::class);

        $reflectionClass = new \ReflectionClass($brickHandlerManager);
        $reflectionMethod = $reflectionClass->getMethod('add');

        foreach ($handlers as $handler) {
            $reflectionMethod->invokeArgs($brickHandlerManager, [
                get_class($handler)::BRICK_TYPE,
                $handler
            ]);
        }
    }

    protected function handleExpectedProperties(object|null $duplicatedEntity, array $expectedProperties): void
    {
        if ($duplicatedEntity === null) {
            return;
        }

        if ($duplicatedEntity instanceof Brick) {
            if ($duplicatedEntity->getType() === EntityDuplicateBrickHandler::BRICK_TYPE) {
                $this->handleExpectedProperties(
                    $duplicatedEntity->getSettings()['entity'],
                    $expectedProperties['settings']['entity']
                );

                $expectedProperties['settings'] = $duplicatedEntity->getSettings();
            } elseif ($duplicatedEntity->getType() === FileBrickHandler::BRICK_TYPE) {
                $this->handleExpectedProperties(
                    $duplicatedEntity->getSettings()['file'],
                    $expectedProperties['settings']['file']
                );

                $expectedProperties['settings'] = $duplicatedEntity->getSettings();
            }
        }

        $propertyAccessor = self::getContainer()->get('property_accessor');
        foreach ($expectedProperties as $property => $value) {
            $duplicatedValue = $propertyAccessor->getValue($duplicatedEntity, $property);

            if ($value instanceof DateTimeInterface) {
                $this->assertInstanceOf(
                    DateTimeInterface::class,
                    $duplicatedValue,
                    "Bad type for property '$property' of entity " . get_class($duplicatedEntity)
                );
                $this->assertEqualsWithDelta(
                    $value->getTimestamp(),
                    $duplicatedValue->getTimestamp(),
                    30,
                    "Bad value for property '$property' of entity " . get_class($duplicatedEntity)
                );
            } elseif ($value instanceof RegexModel) {
                $this->assertMatchesRegularExpression(
                    $value->regex,
                    $duplicatedValue
                );
            } elseif ($duplicatedValue instanceof ArrayCollection) {
                $this->assertInstanceOf(ArrayCollection::class, $duplicatedValue);

                $expectedArray = $value instanceof ArrayCollection ? $value->toArray() : $value;
                $duplicatedArray = $duplicatedValue->toArray();

                $this->assertSameSize(
                    $expectedArray,
                    $duplicatedArray,
                    "The size of the collection '$property' does not match for entity " . get_class($duplicatedEntity)
                );

                foreach ($duplicatedArray as $key => $item) {
                    if (!is_string($key) && !is_int($key)) {
                        throw new \InvalidArgumentException(sprintf(
                            'The key of the collection item must be a string or an integer, "%s" given.',
                            gettype($key)
                        ));
                    }
                    if (isset($expectedArray[$key])) {
                        $this->handleExpectedProperties(
                            $item,
                            $expectedArray[$key]
                        );
                    }
                }
            } elseif (is_array($value) && is_object($duplicatedValue)) {
                $this->handleExpectedProperties($duplicatedValue, $value);
            } elseif ($property === 'mediaChangedAt') {
                $this->assertEqualsWithDelta(
                    $value,
                    $duplicatedValue,
                    30,
                    "Bad value for property '$property' of entity " . get_class($duplicatedEntity)
                );
            } elseif ($value instanceof BrickModel) {
                $this->handleExpectedProperties($duplicatedValue, $value->toArray());
            } else {
                $this->assertEquals($value, $duplicatedValue);
            }
        }
    }
}
