<?php
/**
 * Copyright 2020 Adobe
 * All Rights Reserved.
 *
 * NOTICE: Adobe permits you to use, modify, and distribute this file in
 * accordance with the terms of the Adobe license agreement accompanying
 * it.
 */
declare(strict_types=1);

namespace Magento\Mray\Integration;

use Magento\Mray\Api\MagentoReleaseIndex;
use PHPUnit\Framework\TestCase;

class MagentoReleaseIndexTest extends TestCase
{
    /**
     * @dataProvider projectsProvider
     * @param string $path
     * @param string $version
     * @param array $modules
     * @param array $dictionary
     * @param array $phpCode
     * @param array $api
     * @param array $virtualTypes
     * @param array $notApi
     * @param array $notExisting
     * @param array $dbSchema
     * @throws \JsonException
     */
    public function testProject(
        string $path,
        string $version,
        array $modules,
        array $dictionary,
        array $phpCode,
        array $api,
        array $virtualTypes,
        array $notApi,
        array $notExisting,
        array $dbSchema
    ): void {
        $result = (new MagentoReleaseIndex())->execute($path);
        $this->assertEquals($version, $result->getVersion());

        $components = $result->getComponents()->exportData();
        $this->assertArrayHasKey('modules', $components);

        $this->assertEquals(
            array_map(
                function ($module) {
                    return $this->getHash($module);
                },
                $modules
            ),
            array_keys($components['modules'])
        );

        $dictionaryData = $result->getDictionary()->exportData();
        foreach ($dictionary as $dictionaryReference) {
            $this->assertContains(
                $this->getReference($dictionaryReference),
                $dictionaryData,
                $dictionaryReference . ' should be present in dictionary.'
            );
        }

        $phpCodeData = $result->getPhpCode()->exportData();
        foreach ($phpCode as $phpCodeReference) {
            $this->assertArrayHasKey(
                $this->getHash($this->getReference($phpCodeReference)),
                $phpCodeData,
                $phpCodeReference . ' should be present in PHP code index.'
            );
        }

        $apiData = $result->getApi()->exportData();
        list($numberOfApiEntries, $numberOfNonApiEntries) = $this->getNumberOfApiAndNonApiEntries($apiData);

        foreach ($api as $reference => $data) {
            $encodedReference = $this->getHash($this->getReference($reference));
            $this->assertArrayHasKey(
                $encodedReference,
                $apiData,
                $reference . ' should be present in API index.'
            );
            $this->assertEquals(
                $this->getDataHash($data),
                $apiData[$encodedReference],
                'Incorrect API index data for ' . $reference
            );
        }

        $virtualTypesData = $result->getVirtualTypes()->exportData();

        $this->assertEquals(
            $virtualTypes,
            $virtualTypesData,
            count($virtualTypes) . 'Virtual Types should be identified '
        );

        $expectedNumberOfApiEntries = count($api);
        $this->assertEquals(
            $expectedNumberOfApiEntries,
            $numberOfApiEntries,
            sprintf('Expected %d API entries but got %d', $expectedNumberOfApiEntries, $numberOfApiEntries)
        );

        foreach ($notApi as $nonApiReference) {
            $encodedReference = $this->getHash($this->getReference($nonApiReference));
            $this->assertArrayHasKey(
                $encodedReference,
                $apiData,
                $nonApiReference . ' should be present in API index as not API.'
            );
            $this->assertEquals(
                0,
                $apiData[$encodedReference],
                $nonApiReference . ' should be zero in API index.'
            );
        }

        $expectedNumberOfNonApiEntries = count($notApi);
        $this->assertEquals(
            $expectedNumberOfNonApiEntries,
            $numberOfNonApiEntries,
            sprintf(
                'Expected %d non API entries but got %d.',
                $expectedNumberOfNonApiEntries,
                $numberOfNonApiEntries
            )
        );

        foreach ($notExisting as $notExistingReference) {
            $encodedReference = $this->getHash($this->getReference($notExistingReference));
            $this->assertArrayNotHasKey(
                $encodedReference,
                $apiData,
                $notExistingReference . ' should NOT be present in API index.'
            );
        }

        $this->assertEquals($dbSchema, $result->getDbSchema()->exportData());
    }

    /**
     * @param array $apiData
     * @return int[]
     */
    private function getNumberOfApiAndNonApiEntries(array $apiData): array
    {
        $apiEntries = 0;
        $nonApiEntries = 0;
        foreach ($apiData as $data) {
            if ($data === 0) {
                $nonApiEntries++;
            } else {
                $apiEntries++;
            }
        }

        return [$apiEntries, $nonApiEntries];
    }

    /**
     * @return \string[][]
     */
    public function projectsProvider(): array
    {
        return [
            [
                'Path' => __DIR__ . '/_files/magentoReleases/project1',
                'Version' => '3.0.0',
                'Modules' => [
                    'vendor_module'
                ],
                'Dictionary' => [
                    'Vendor\Module\Model\ApiDeprecatedClass',
                    'Vendor\Module\Model\ApiDeprecatedInterface',
                    'Vendor\Module\Model\ApiNonDeprecatedClass',
                    'Vendor\Module\Model\ApiNonDeprecatedInterface',
                    'Vendor\Module\Model\ApiNonDeprecatedClass::deprecatedMethod',
                    'Vendor\Module\Model\ApiNonDeprecatedClass::DEPRECATED_CONSTANT',
                    'Vendor\Module\Model\NonApiDeprecatedConstant::DEPRECATED_CONSTANT',
                    'Vendor\Module\Model\NonApiDeprecatedClass',
                    'Vendor\Module\Model\NonApiDeprecatedMethod::deprecatedMethod',
                    'Vendor\Module\Model\NonApiDeprecatedInterface',
                    'Vendor\Module\Model\PhpDocMethodsDeclaration::getStoreId',
                    'Vendor\Module\Model\PhpDocMethodsDeclaration::getAllItems',
                ],
                'PHP code' => [
                    'Vendor\Module\Model\ApiDeprecatedClass',
                    'Vendor\Module\Model\ApiDeprecatedInterface',
                    'Vendor\Module\Model\ApiNonDeprecatedClass',
                    'Vendor\Module\Model\ApiNonDeprecatedInterface'
                ],
                'API' => [
                    'Vendor\Module\Model\ApiDeprecatedClass' => [
                        '100.0.0',
                        [0 => '100.1.0', 1 => 'Not a dummy comment', 2 => 'ApiNonDeprecatedClass']
                    ],
                    'Vendor\Module\Model\ApiDeprecatedInterface' => [
                        '100.0.0',
                        [0 => '100.1.0', 2 => 'ApiNonDeprecatedInterface']
                    ],
                    'Vendor\Module\Model\ApiNonDeprecatedClass' => ['100.0.0'],
                    'Vendor\Module\Model\ApiNonDeprecatedClass::DEPRECATED_CONSTANT' => [
                        '100.0.0',
                        [0 => '100.1.0', 1 => 'Useful comment goes here', 2 => 'PRIVATE_CONSTANT']
                    ],
                    'Vendor\Module\Model\ApiNonDeprecatedClass::deprecatedMethod' => [
                        '100.0.0',
                        [0 => '100.1.0', 2 => 'nonDeprecatedMethod']
                    ],
                    'Vendor\Module\Model\ApiNonDeprecatedClass::nonDeprecatedMethod' => [
                        '100.0.0',
                    ],
                    'Vendor\Module\Model\ApiNonDeprecatedInterface' => ['100.1.0'],
                    'Vendor\Module\Model\ApiNonDeprecatedInterface::SCOPE_GLOBAL' => ['100.1.0'],
                    'Vendor\Module\Model\ApiInheritorClass' => ['100.0.0'],
                    'Vendor\Module\Model\ApiInheritorClass::nonApiParentMethod' => ['100.0.0'],
                    'Vendor\Module\Model\ApiInheritorClass::apiGrandParentMethod' => ['100.0.0'],
                    'Vendor\Module\Model\ApiGrandParentClass' => ['100.0.0'],
                    'Vendor\Module\Model\ApiGrandParentClass::apiGrandParentMethod' => ['100.0.0'],
                    'Vendor\Module\Model\PhpDocMethodsDeclaration' => ['100.0.2'],
                    'Vendor\Module\Model\PhpDocMethodsDeclaration::getStoreId' => ['100.0.2'],
                    'Vendor\Module\Model\PhpDocMethodsDeclaration::setStoreId' => ['100.0.2'],
                    'Vendor\Module\Model\PhpDocMethodsDeclaration::getAllItems' => ['100.0.2'],
                    'Vendor\Module\Model\PhpDocMethodsDeclaration::setAllItems' => ['100.0.2'],
                ],
                'Virtual Types' => [
                    'Magento\Framework\Communication\Config\Reader\XmlReader',
                    'layoutArgumentGeneratorInterpreter',
                    'layoutArrayArgumentReaderInterpreter'
                ],
                'Non API' => [
                    'Vendor\Module\Model\NonApiPhpDocMethodsDeclaration',
                    'Vendor\Module\Model\NonApiPhpDocMethodsDeclaration::getStoreId',
                    'Vendor\Module\Model\NonApiPhpDocMethodsDeclaration::getAllItems',
                    'Vendor\Module\Model\NonApiInheritorClass',
                    'Vendor\Module\Model\NonApiInheritorClass::nonApiParentMethod',
                    'Vendor\Module\Model\NonApiInheritorClass::apiGrandParentMethod',
                    'Vendor\Module\Model\NonApiInheritorClass::SCOPE_GLOBAL',
                    'Vendor\Module\Model\NonApiDeprecatedConstant',
                    'Vendor\Module\Model\NonApiDeprecatedConstant::DEPRECATED_CONSTANT',
                    'Vendor\Module\Model\NonApiDeprecatedInterface',
                    'Vendor\Module\Model\NonApiDeprecatedMethod',
                    'Vendor\Module\Model\NonApiDeprecatedMethod::deprecatedMethod',
                    'Vendor\Module\Model\NonApiDeprecatedClass',
                    'Vendor\Module\Model\NonApiParentClass',
                    'Vendor\Module\Model\NonApiParentClass::nonApiParentMethod',
                    'Vendor\Module\Model\NonApiParentClass::apiGrandParentMethod',
                ],
                'Non existing' => [
                    'Vendor\Module\Model\ApiNonDeprecatedClass::privateMethod',
                    'Vendor\Module\Model\ApiNonDeprecatedClass::PRIVATE_CONSTANT',
                ],
                'DB Schema' => [
                    'patch_list' => [
                        'name' => 'patch_list',
                        'resource' => 'default',
                        'comment' => 'List of data/schema patches',
                        'column' => [
                            'patch_id' => [
                                'name' => 'patch_id',
                                'identity' => 'true',
                                'comment' => 'Patch Auto Increment',
                                'type' => 'int'
                            ],
                            'patch_name' => [
                                'name' => 'patch_name',
                                'length' => '1024',
                                'nullable' => 'false',
                                'comment' => 'Patch Class Name',
                                'type' => 'varchar'
                            ],
                        ],
                        'constraint' => [
                            'PRIMARY' => [
                                'referenceId' => 'PRIMARY',
                                'column' => [
                                    'patch_id' => [
                                        'name' => 'patch_id',
                                    ],
                                ],
                                'type' => 'primary'
                            ],
                        ],
                    ],
                ]
            ],
            [
                'Path' => __DIR__ . '/_files/magentoReleases/project2',
                'Version' => '2.3.7',
                'Modules' => [
                    'magento_catalogrule',
                    'magento_rule'
                ],
                'Dictionary' => [
                    'Magento\CatalogRule\Model\Product',
                    'Magento\Rule\Model\AbstractProduct',
                ],
                'PHP code' => [
                    'Magento\Rule\Model\AbstractProduct',
                ],
                'API' => [
                    'Magento\Rule\Model\AbstractProduct' => ['100.0.2'],
                    'Magento\Rule\Model\AbstractProduct::__construct' => ['100.0.2']
                ],
                'Virtual Types' => [],
                'Non API' => [
                    'Magento\CatalogRule\Model\Product',
                    'Magento\CatalogRule\Model\Product::__construct',
                    'Magento\ThirdPartyInheritor\NonApiThirdPartyInheritorClass',
                    'Magento\ThirdPartyInheritor\NonApiThirdPartyInheritorClass::thirdPartyMethod'
                ],
                'Non existing' => [
                    'Monolog\ThirdPartyParentClass',
                ],
                'DB Schema' => []
            ]
        ];
    }

    /**
     * @param array $data
     * @return string
     * @throws \JsonException
     */
    private function getDataHash(array $data): string
    {
        if ($this->hasDeprecationInfo($data)) {
            $data[1] = $this->getHash($this->stringify($data[1]));
        }
        return $this->getHash($this->stringify($data));
    }

    /**
     * @param array $data
     * @return bool
     */
    private function hasDeprecationInfo(array $data): bool
    {
        return isset($data[1]);
    }

    /**
     * @param string $data
     * @return string
     * @throws \JsonException
     */
    private function getReference(string $data): string
    {
        $parts = explode('::', $data);

        $encodedParts = [];
        foreach ($parts as $part) {
            $encodedParts[] = $this->getHash($part);
        }

        return $this->stringify($encodedParts);
    }

    /**
     * @param array $data
     * @return string
     * @throws \JsonException
     */
    private function stringify(array $data): string
    {
        return substr(json_encode($data, JSON_FORCE_OBJECT | JSON_THROW_ON_ERROR), 1, -1);
    }

    /**
     * @param string $data
     * @return string
     */
    private function getHash(string $data): string
    {
        $hash = str_replace(
            ['+', '/', '='],
            ['-', '_', ''],
            base64_encode(hash('md5', $data, true))
        );
        if (strlen($hash) >= strlen($data)) {
            return $data;
        }
        return $hash;
    }
}
