<?php
declare(strict_types=1);

namespace PhpList\Core\Tests\Unit\Composer;

use Composer\Package\PackageInterface;
use PhpList\Core\Composer\ModuleFinder;
use PhpList\Core\Composer\PackageRepository;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\ProphecySubjectInterface;
use Symfony\Component\Yaml\Yaml;

/**
 * Testcase.
 *
 * @author Oliver Klee <oliver@phplist.com>
 */
class ModuleFinderTest extends TestCase
{
    /**
     * @var string
     */
    const YAML_COMMENT = '# This file is autogenerated. Please do not edit.';

    /**
     * @var ModuleFinder
     */
    private $subject = null;

    /**
     * @var PackageRepository|ObjectProphecy
     */
    private $packageRepositoryProphecy = null;

    protected function setUp()
    {
        $this->subject = new ModuleFinder();

        $this->packageRepositoryProphecy = $this->prophesize(PackageRepository::class);

        /** @var PackageRepository|ProphecySubjectInterface $packageRepository */
        $packageRepository = $this->packageRepositoryProphecy->reveal();
        $this->subject->injectPackageRepository($packageRepository);
    }

    /**
     * @test
     */
    public function findBundleClassesForNoModulesReturnsEmptyArray()
    {
        $this->packageRepositoryProphecy->findModules()->willReturn([]);

        $result = $this->subject->findBundleClasses();

        static::assertSame([], $result);
    }

    /**
     * @return PackageInterface[][]
     */
    public function modulesWithoutBundlesDataProvider(): array
    {
        /** @var array[][] $extrasSets */
        $extrasSets = [
            'one module without/with empty extras' => [[]],
            'one module with extras for other stuff' => [['branch-alias' => ['dev-master' => '4.0.x-dev']]],
            'one module with empty "phplist/core" extras section' => [['phplist/core' => []]],
            'one module with empty bundles extras section' => [['phplist/core' => ['bundles' => []]]],
        ];

        return $this->buildMockPackagesWithModuleConfiguration($extrasSets);
    }

    /**
     * @param array[][] $extrasSets
     *
     * @return PackageInterface[][]
     */
    private function buildMockPackagesWithModuleConfiguration(array $extrasSets): array
    {
        $moduleSets = [];
        foreach ($extrasSets as $packageName => $extrasSet) {
            $moduleSet = $this->buildSingleMockPackageWithModuleConfiguration($extrasSet);
            $moduleSets[$packageName] = [$moduleSet];
        }

        return $moduleSets;
    }

    /**
     * @param array[] $extrasSet
     *
     * @return PackageInterface[]
     */
    private function buildSingleMockPackageWithModuleConfiguration(array $extrasSet): array
    {
        /** @var PackageInterface[] $moduleSet */
        $moduleSet = [];
        foreach ($extrasSet as $extras) {
            $moduleSet[] = $this->buildPackageProphecyWithExtras($extras, 'phplist/test');
        }

        return $moduleSet;
    }

    /**
     * @param array $extras
     * @param string $packageName
     *
     * @return PackageInterface|ProphecySubjectInterface
     */
    private function buildPackageProphecyWithExtras(array $extras, string $packageName): PackageInterface
    {
        /** @var PackageInterface|ObjectProphecy $packageProphecy */
        $packageProphecy = $this->prophesize(PackageInterface::class);
        $packageProphecy->getExtra()->willReturn($extras);
        $packageProphecy->getName()->willReturn($packageName);

        return $packageProphecy->reveal();
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @dataProvider modulesWithoutBundlesDataProvider
     */
    public function findBundleClassesForModulesWithoutBundlesReturnsEmptyArray(array $modules)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->findBundleClasses();

        static::assertSame([], $result);
    }

    /**
     * @return PackageInterface[][]
     */
    public function modulesWithInvalidBundlesDataProvider(): array
    {
        /** @var array[][] $extrasSets */
        $extrasSets = [
            'one module with core section as string' => [['phplist/core' => 'foo']],
            'one module with core section as int' => [['phplist/core' => 42]],
            'one module with core section as float' => [['phplist/core' => 3.14159]],
            'one module with core section as bool' => [['phplist/core' => true]],
            'one module with bundles section as string' => [['phplist/core' => ['bundles' => 'foo']]],
            'one module with bundles section as int' => [['phplist/core' => ['bundles' => 42]]],
            'one module with bundles section as float' => [['phplist/core' => ['bundles' => 3.14159]]],
            'one module with bundles section as bool' => [['phplist/core' => ['bundles' => true]]],
            'one module with one bundle class name as array' => [['phplist/core' => ['bundles' => [[]]]]],
            'one module with one bundle class name as int' => [['phplist/core' => ['bundles' => [42]]]],
            'one module with one bundle class name as float' => [['phplist/core' => ['bundles' => [3.14159]]]],
            'one module with one bundle class name as bool' => [['phplist/core' => ['bundles' => [true]]]],
            'one module with one bundle class name as null' => [['phplist/core' => ['bundles' => [null]]]],
        ];

        return $this->buildMockPackagesWithModuleConfiguration($extrasSets);
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @dataProvider modulesWithInvalidBundlesDataProvider
     */
    public function findBundleClassesForModulesWithInvalidBundlesConfigurationThrowsException(array $modules)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $this->expectException(\InvalidArgumentException::class);

        $this->subject->findBundleClasses();
    }

    /**
     * @return array[]
     */
    public function modulesWithBundlesDataProvider(): array
    {
        /** @var array[][] $dataSets */
        $dataSets = [
            'one module with one bundle' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'bundles' => ['Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'],
                        ],
                    ],
                ],
                ['phplist/foo' => ['Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle']],
            ],
            'one module with two bundles' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'bundles' => [
                                'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle',
                                'PhpList\\Core\\EmptyStartPageBundle\\PhpListEmptyStartPageBundle',
                            ],
                        ],
                    ],
                ],
                [
                    'phplist/foo' => [
                        'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle',
                        'PhpList\\Core\\EmptyStartPageBundle\\PhpListEmptyStartPageBundle',
                    ],
                ],
            ],
            'two module with one bundle each' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'bundles' => ['Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'],
                        ],
                    ],
                    'phplist/bar' => [
                        'phplist/core' => [
                            'bundles' => ['PhpList\\Core\\EmptyStartPageBundle\\PhpListEmptyStartPageBundle'],
                        ],
                    ],
                ],
                [
                    'phplist/foo' => ['Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'],
                    'phplist/bar' => ['PhpList\\Core\\EmptyStartPageBundle\\PhpListEmptyStartPageBundle'],
                ],
            ],
        ];

        $moduleSets = [];
        /** @var array[] $dataSet */
        foreach ($dataSets as $dataSetName => $dataSet) {
            /** @var string[][][] $extraSets */
            /** @var string[][] $expectedBundles */
            list($extraSets, $expectedBundles) = $dataSet;

            $testCases = [];
            foreach ($extraSets as $packageName => $extras) {
                $testCases[] = $this->buildPackageProphecyWithExtras($extras, $packageName);
            }
            $moduleSets[$dataSetName] = [$testCases, $expectedBundles];
        }

        return $moduleSets;
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @param string[][] $expectedBundles
     * @dataProvider modulesWithBundlesDataProvider
     */
    public function findBundleClassesForModulesWithBundlesReturnsBundleClassNames(
        array $modules,
        array $expectedBundles
    ) {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->findBundleClasses();

        static::assertSame($expectedBundles, $result);
    }

    /**
     * @test
     */
    public function createBundleConfigurationYamlForNoModulesReturnsCommentOnly()
    {
        $this->packageRepositoryProphecy->findModules()->willReturn([]);

        $result = $this->subject->createBundleConfigurationYaml();

        static::assertSame(static::YAML_COMMENT . "\n{  }", $result);
    }

    /**
     * @test
     * @param PackageInterface[][] $modules
     * @param array[] $bundles
     * @dataProvider modulesWithBundlesDataProvider
     */
    public function createBundleConfigurationYamlReturnsYamlForBundles(array $modules, array $bundles)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->createBundleConfigurationYaml();

        static::assertSame(static::YAML_COMMENT . "\n" . Yaml::dump($bundles), $result);
    }

    /**
     * @return PackageInterface[][]
     */
    public function modulesWithoutRoutesDataProvider(): array
    {
        /** @var array[][] $extrasSets */
        $extrasSets = [
            'one module without/with empty extras' => [[]],
            'one module with extras for other stuff' => [['branch-alias' => ['dev-master' => '4.0.x-dev']]],
            'one module with empty "phplist/core" extras section' => [['phplist/core' => []]],
            'one module with empty routes extras section' => [['phplist/core' => ['routes' => []]]],
        ];

        return $this->buildMockPackagesWithModuleConfiguration($extrasSets);
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @dataProvider modulesWithoutRoutesDataProvider
     */
    public function findRoutesForModulesWithoutRoutesReturnsEmptyArray(array $modules)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->findRoutes();

        static::assertSame([], $result);
    }

    /**
     * @return PackageInterface[][]
     */
    public function modulesWithInvalidRoutesDataProvider(): array
    {
        /** @var array[][] $extrasSets */
        $extrasSets = [
            'one module with core section as string' => [['phplist/core' => 'foo']],
            'one module with core section as int' => [['phplist/core' => 42]],
            'one module with core section as float' => [['phplist/core' => 3.14159]],
            'one module with core section as bool' => [['phplist/core' => true]],
            'one module with routes section as string' => [['phplist/core' => ['routes' => 'foo']]],
            'one module with routes section as int' => [['phplist/core' => ['routes' => 42]]],
            'one module with routes section as float' => [['phplist/core' => ['routes' => 3.14159]]],
            'one module with routes section as bool' => [['phplist/core' => ['routes' => true]]],
            'one module with one route class name as string' => [['phplist/core' => ['routes' => ['foo']]]],
            'one module with one route class name as int' => [['phplist/core' => ['routes' => [42]]]],
            'one module with one route class name as float' => [['phplist/core' => ['routes' => [3.14159]]]],
            'one module with one route class name as bool' => [['phplist/core' => ['routes' => [true]]]],
            'one module with one route class name as null' => [['phplist/core' => ['routes' => [null]]]],
        ];

        return $this->buildMockPackagesWithModuleConfiguration($extrasSets);
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @dataProvider modulesWithInvalidRoutesDataProvider
     */
    public function findRoutesClassesForModulesWithInvalidRoutesConfigurationThrowsException(array $modules)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $this->expectException(\InvalidArgumentException::class);

        $this->subject->findRoutes();
    }

    /**
     * @return array[][]
     */
    public function modulesWithRoutesDataProvider(): array
    {
        /** @var array[][] $dataSets */
        $dataSets = [
            'one module with one route' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'routes' => [
                                'homepage' => [
                                    'path' => '/',
                                    'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Default:index'],
                                ],
                            ],
                        ],
                    ],
                ],
                [
                    'phplist/foo.homepage' => [
                        'path' => '/',
                        'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Default:index'],
                    ],
                ],
            ],
            'one module with two routes' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'routes' => [
                                'homepage' => [
                                    'path' => '/',
                                    'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Default:index'],
                                ],
                                'blog' => [
                                    'path' => '/blog',
                                    'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Blog:index'],
                                ],
                            ],
                        ],
                    ],
                ],
                [
                    'phplist/foo.homepage' => [
                        'path' => '/',
                        'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Default:index'],
                    ],
                    'phplist/foo.blog' => [
                        'path' => '/blog',
                        'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Blog:index'],
                    ],
                ],
            ],
            'two module with one route each' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'routes' => [
                                'homepage' => [
                                    'path' => '/',
                                    'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Default:index'],
                                ],
                            ],
                        ],
                    ],
                    'phplist/bar' => [
                        'phplist/core' => [
                            'routes' => [
                                'blog' => [
                                    'path' => '/blog',
                                    'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Blog:index'],
                                ],
                            ],
                        ],
                    ],
                ],
                [
                    'phplist/foo.homepage' => [
                        'path' => '/',
                        'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Default:index'],
                    ],
                    'phplist/bar.blog' => [
                        'path' => '/blog',
                        'defaults' => ['_controller' => 'PhpListEmptyStartPageBundle:Blog:index'],
                    ],
                ],
            ],
        ];

        return $this->buildModuleSets($dataSets);
    }

    /**
     * @param array[][] $dataSets
     *
     * @return array[]
     */
    private function buildModuleSets(array $dataSets): array
    {
        $moduleSets = [];
        /** @var array[] $dataSet */
        foreach ($dataSets as $dataSetName => $dataSet) {
            /** @var string[][][] $extraSets */
            /** @var array[] $expectedRoutes */
            list($extraSets, $expectedRoutes) = $dataSet;

            $testCases = [];
            foreach ($extraSets as $packageName => $extras) {
                $testCases[] = $this->buildPackageProphecyWithExtras($extras, $packageName);
            }
            $moduleSets[$dataSetName] = [$testCases, $expectedRoutes];
        }

        return $moduleSets;
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @param array[] $expectedRoutes
     * @dataProvider modulesWithRoutesDataProvider
     */
    public function findRoutesForModulesWithRoutesReturnsRoutes(array $modules, array $expectedRoutes)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->findRoutes();

        static::assertSame($expectedRoutes, $result);
    }

    /**
     * @test
     */
    public function createRouteConfigurationYamlForNoModulesReturnsCommentOnly()
    {
        $this->packageRepositoryProphecy->findModules()->willReturn([]);

        $result = $this->subject->createRouteConfigurationYaml();

        static::assertSame(static::YAML_COMMENT . "\n{  }", $result);
    }

    /**
     * @test
     * @param PackageInterface[][] $modules
     * @param array[] $routes
     * @dataProvider modulesWithRoutesDataProvider
     */
    public function createRouteConfigurationYamlReturnsYamlForRoutes(array $modules, array $routes)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->createRouteConfigurationYaml();

        static::assertSame(static::YAML_COMMENT . "\n" . Yaml::dump($routes), $result);
    }

    /**
     * @return PackageInterface[][]
     */
    public function modulesWithoutConfigurationDataProvider(): array
    {
        /** @var array[][] $extrasSets */
        $extrasSets = [
            'without/with empty extras' => [[]],
            'with extras for other stuff' => [['branch-alias' => ['dev-master' => '4.0.x-dev']]],
            'with empty "phplist/core" extras section' => [['phplist/core' => []]],
            'with empty configuration extras section' => [['phplist/core' => ['configuration' => []]]],
        ];

        return $this->buildMockPackagesWithModuleConfiguration($extrasSets);
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @dataProvider modulesWithoutConfigurationDataProvider
     */
    public function findGeneralConfigurationForModulesWithoutConfigurationReturnsEmptyArray(array $modules)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->findGeneralConfiguration();

        static::assertSame([], $result);
    }

    /**
     * @return PackageInterface[][]
     */
    public function modulesWithInvalidConfigurationDataProvider(): array
    {
        /** @var array[][] $extrasSets */
        $extrasSets = [
            'core section as string' => [['phplist/core' => 'foo']],
            'core section as int' => [['phplist/core' => 42]],
            'core section as float' => [['phplist/core' => 3.14159]],
            'core section as bool' => [['phplist/core' => true]],
            'configuration section as string' => [['phplist/core' => ['configuration' => 'foo']]],
            'configuration section as int' => [['phplist/core' => ['configuration' => 42]]],
            'configuration section as float' => [['phplist/core' => ['configuration' => 3.14159]]],
            'configuration section as bool' => [['phplist/core' => ['configuration' => true]]],
        ];

        return $this->buildMockPackagesWithModuleConfiguration($extrasSets);
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @dataProvider modulesWithInvalidConfigurationDataProvider
     */
    public function findGeneralConfigurationForModulesWithInvalidConfigurationThrowsException(array $modules)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $this->expectException(\InvalidArgumentException::class);

        $this->subject->findGeneralConfiguration();
    }

    /**
     * @return array[][]
     */
    public function modulesWithConfigurationDataProvider(): array
    {
        /** @var array[][] $dataSets */
        $dataSets = [
            'one module with configuration' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'configuration' => ['foo' => 'bar'],
                        ],
                    ],
                ],
                ['foo' => 'bar'],
            ],
            'two modules non-overlapping configuration sets' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'configuration' => ['foo' => 'bar'],
                        ],
                    ],
                    'phplist/bar' => [
                        'phplist/core' => [
                            'configuration' => [
                                'foobar' => ['life' => 'everything'],
                            ],
                        ],
                    ],
                ],
                [
                    'foo' => 'bar',
                    'foobar' => ['life' => 'everything'],
                ],
            ],
            'two modules overlapping configuration sets' => [
                [
                    'phplist/foo' => [
                        'phplist/core' => [
                            'configuration' => [
                                'foo' => 'bar',
                                'foobar' => [1 => 'hello'],
                                'good' => 'code',
                            ],
                        ],
                    ],
                    'phplist/bar' => [
                        'phplist/core' => [
                            'configuration' => [
                                'foo' => 'bonjour',
                                'foobar' => [2 => 'world'],
                            ],
                        ],
                    ],
                ],
                [
                    'foo' => 'bonjour',
                    'foobar' => [1 => 'hello', 2 => 'world'],
                    'good' => 'code',
                ],
            ],
        ];

        return $this->buildModuleSets($dataSets);
    }

    /**
     * @test
     * @param PackageInterface[] $modules
     * @param array $expectedConfiguration
     * @dataProvider modulesWithConfigurationDataProvider
     */
    public function findGeneralConfigurationForModulesWithConfigurationReturnsConfiguration(
        array $modules,
        array $expectedConfiguration
    ) {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->findGeneralConfiguration();

        static::assertSame($expectedConfiguration, $result);
    }

    /**
     * @test
     */
    public function createGeneralConfigurationYamlForNoModulesReturnsCommentOnly()
    {
        $this->packageRepositoryProphecy->findModules()->willReturn([]);

        $result = $this->subject->createGeneralConfigurationYaml();

        static::assertSame(static::YAML_COMMENT . "\n{  }", $result);
    }

    /**
     * @test
     * @param PackageInterface[][] $modules
     * @param array[] $routes
     * @dataProvider modulesWithConfigurationDataProvider
     */
    public function createGeneralConfigurationYamlReturnsYamlForConfiguration(array $modules, array $routes)
    {
        $this->packageRepositoryProphecy->findModules()->willReturn($modules);

        $result = $this->subject->createGeneralConfigurationYaml();

        static::assertSame(static::YAML_COMMENT . "\n" . Yaml::dump($routes), $result);
    }
}
