<?php
/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

namespace Tests\Unit\Core\Form\IdentifiableObject\Builder;

use PHPUnit\Framework\TestCase;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Builder\FormBuilder;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Builder\FormBuilderInterface;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataProvider\FormDataProviderInterface;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\OptionProvider\FormOptionsProviderInterface;
use PrestaShop\PrestaShop\Core\Hook\HookDispatcherInterface;
use Symfony\Component\Form\FormBuilderInterface as SymfonyFormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormRegistryInterface;
use Symfony\Component\Form\ResolvedFormTypeInterface;

class FormBuilderTest extends TestCase
{
    public function testCanBeConstructed()
    {
        $builder = new FormBuilder(
            $this->createMock(FormFactoryInterface::class),
            $this->createMock(HookDispatcherInterface::class),
            $this->createMock(FormDataProviderInterface::class),
            'a',
            $this->createMockFormRegistryInterface('')
        );

        $this->assertInstanceOf(FormBuilderInterface::class, $builder);
    }

    public function testGetFormToCreateModel()
    {
        $formMock = $this->createMock(FormInterface::class);
        $formBuilderMock = $this->createSymfonyFormBuilderMock($formMock, 'Bbcd');
        $formFactoryMock = $this->createFormFactoryMock($formBuilderMock);

        $builder = new FormBuilder(
            $formFactoryMock,
            $this->createHookDispatcherMock($formBuilderMock, 'Bbcd'),
            $this->createDefaultDataProviderMock(),
            'a',
            $this->createMockFormRegistryInterface('Bbcd')
        );

        $form = $builder->getForm([], []);

        $this->assertEquals($formMock, $form);
    }

    public function testGetFormToEditModel()
    {
        $formMock = $this->createMock(FormInterface::class);
        $formBuilderMock = $this->createSymfonyFormBuilderMock($formMock, 'Abcd');
        $formFactoryMock = $this->createFormFactoryMock($formBuilderMock);

        $builder = new FormBuilder(
            $formFactoryMock,
            $this->createHookDispatcherMock($formBuilderMock, 'Abcd', 1),
            $this->createDataProviderMock(),
            'a',
            $this->createMockFormRegistryInterface('Abcd')
        );

        $form = $builder->getFormFor(1, [], []);

        $this->assertEquals($formMock, $form);
    }

    public function testGetFormWithOptionsToCreateModel()
    {
        $formMock = $this->createMock(FormInterface::class);
        $formBuilderMock = $this->createSymfonyFormBuilderMock($formMock, 'Bbcd');
        $formFactoryMock = $this->createFormFactoryMock($formBuilderMock);

        $options = ['option' => 'value'];
        $builder = new FormBuilder(
            $formFactoryMock,
            $this->createHookDispatcherMock($formBuilderMock, 'Bbcd', null, $options),
            $this->createDefaultDataProviderMock(),
            'a',
            $this->createMockFormRegistryInterface('Bbcd'),
            $this->createDefaultOptionProviderMock($options)
        );

        $form = $builder->getForm([], []);

        $this->assertEquals($formMock, $form);
    }

    public function testGetFormWithOptionsToEditModel()
    {
        $formMock = $this->createMock(FormInterface::class);
        $formBuilderMock = $this->createSymfonyFormBuilderMock($formMock, 'Abcd');
        $formFactoryMock = $this->createFormFactoryMock($formBuilderMock);

        $options = ['option' => 'value'];
        $builder = new FormBuilder(
            $formFactoryMock,
            $this->createHookDispatcherMock($formBuilderMock, 'Abcd', 1, $options),
            $this->createDataProviderMock(),
            'a',
            $this->createMockFormRegistryInterface('Abcd'),
            $this->createOptionProviderMock($options)
        );

        $form = $builder->getFormFor(1, [], []);

        $this->assertEquals($formMock, $form);
    }

    private function createMockFormRegistryInterface(string $formName): FormRegistryInterface
    {
        $mockResolvedFormTypeInterface = $this->createMock(ResolvedFormTypeInterface::class);
        $mockResolvedFormTypeInterface->method('getBlockPrefix')->willReturn($formName);

        $mockFormRegistryInterface = $this->createMock(FormRegistryInterface::class);
        $mockFormRegistryInterface->method('getType')->willReturn($mockResolvedFormTypeInterface);

        return $mockFormRegistryInterface;
    }

    /**
     * @param SymfonyFormBuilderInterface $formBuilder
     *
     * @return FormFactoryInterface
     */
    private function createFormFactoryMock(SymfonyFormBuilderInterface $formBuilder): FormFactoryInterface
    {
        $formFactoryMock = $this->createMock(FormFactoryInterface::class);

        $formFactoryMock
            ->method('createBuilder')
            ->will($this->returnValue($formBuilder));

        return $formFactoryMock;
    }

    /**
     * @param FormInterface $form
     *
     * @return SymfonyFormBuilderInterface
     */
    private function createSymfonyFormBuilderMock(FormInterface $form, string $formName): SymfonyFormBuilderInterface
    {
        $formBuilderMock = $this->createMock(SymfonyFormBuilderInterface::class);
        $formBuilderMock
            ->method('getForm')
            ->will($this->returnValue($form));
        $formBuilderMock
            ->method('getName')
            ->will($this->returnValue($formName));

        return $formBuilderMock;
    }

    /**
     * @param SymfonyFormBuilderInterface $formBuilder
     * @param string $formName
     * @param int|null $expectedId
     * @param array $expectedOptions
     *
     * @return HookDispatcherInterface
     */
    private function createHookDispatcherMock(
        SymfonyFormBuilderInterface $formBuilder,
        string $formName,
        ?int $expectedId = null,
        array $expectedOptions = []
    ): HookDispatcherInterface {
        if ($expectedId) {
            $hookNameEnd = 'FormDataProviderData';
            $hookOptions = [
                'data' => [],
                'options' => $expectedOptions,
                'id' => $expectedId,
            ];
        } else {
            $hookNameEnd = 'FormDataProviderDefaultData';
            $hookOptions = [
                'data' => [],
                'options' => $expectedOptions,
            ];
        }

        $hookDispatcherMock = $this->createMock(HookDispatcherInterface::class);

        $invokedCount = $this->exactly(2);
        $hookDispatcherMock->expects($invokedCount)
            ->method('dispatchWithParameters')
            ->willReturnCallback(function ($hookName, array $hookParameters) use ($invokedCount, $formName, $hookNameEnd, $hookOptions, $formBuilder, $expectedOptions, $expectedId) {
                if ($invokedCount->numberOfInvocations() === 1) {
                    $this->assertEquals('action' . $formName . $hookNameEnd, $hookName);
                    $this->assertEquals($hookOptions, $hookParameters);
                }

                if ($invokedCount->numberOfInvocations() === 2) {
                    $this->assertEquals('action' . $formName . 'FormBuilderModifier', $hookName);
                    $this->assertEquals([
                        'form_builder' => $formBuilder,
                        'data' => [],
                        'options' => $expectedOptions,
                        'id' => $expectedId,
                    ], $hookParameters);
                }
            });

        return $hookDispatcherMock;
    }

    /**
     * @return FormDataProviderInterface
     */
    private function createDataProviderMock(): FormDataProviderInterface
    {
        $dataProviderMock = $this->createMock(FormDataProviderInterface::class);
        $dataProviderMock
            ->expects($this->once())
            ->method('getData')
            ->will($this->returnValue([]));

        $dataProviderMock
            ->expects($this->never())
            ->method('getDefaultData');

        return $dataProviderMock;
    }

    /**
     * @return FormDataProviderInterface
     */
    private function createDefaultDataProviderMock(): FormDataProviderInterface
    {
        $dataProviderMock = $this->createMock(FormDataProviderInterface::class);
        $dataProviderMock
            ->expects($this->once())
            ->method('getDefaultData')
            ->will($this->returnValue([]));

        $dataProviderMock
            ->expects($this->never())
            ->method('getData');

        return $dataProviderMock;
    }

    /**
     * @param array $options
     *
     * @return FormOptionsProviderInterface
     */
    private function createDefaultOptionProviderMock(array $options): FormOptionsProviderInterface
    {
        $optionProviderMock = $this->createMock(FormOptionsProviderInterface::class);
        $optionProviderMock
            ->expects($this->once())
            ->method('getDefaultOptions')
            ->will($this->returnValue($options));

        $optionProviderMock
            ->expects($this->never())
            ->method('getOptions');

        return $optionProviderMock;
    }

    /**
     * @param array $options
     *
     * @return FormOptionsProviderInterface
     */
    private function createOptionProviderMock(array $options): FormOptionsProviderInterface
    {
        $optionProviderMock = $this->createMock(FormOptionsProviderInterface::class);
        $optionProviderMock
            ->expects($this->once())
            ->method('getOptions')
            ->will($this->returnValue($options));

        $optionProviderMock
            ->expects($this->never())
            ->method('getDefaultOptions')
        ;

        return $optionProviderMock;
    }
}
