<?php

namespace Tests\Unit\Framework;

use App\Filters\ProductAttribute;
use App\Filters\ProductCategories;
use App\Filters\ProductCategory;
use App\Filters\ProductManufacturer;
use App\Filters\ProductModel;
use App\Filters\ProductPrice;
use App\Filters\ProductQuantity;
use App\Filters\ProductStatus;
use DirectoryIterator;
use InvalidArgumentException;
use JsonException;
use Mockery;
use Monolog\Logger;
use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\CriteriaBuilder\CriteriaBuilder;
use Openguru\OpenCartFramework\CriteriaBuilder\RulesRegistry;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Openguru\OpenCartFramework\QueryBuilder\Connections\MySqlConnection;
use Openguru\OpenCartFramework\QueryBuilder\Grammars\MySqlGrammar;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\Support\Arr;
use Tests\TestCase;

class CriteriaBuilderTest extends TestCase
{
    protected function tearDown(): void
    {
        parent::tearDown();

        Mockery::close();
    }

    /** @dataProvider CriteriaBuilderFixturesDataProvider */
    public function testRule($folder): void
    {
        $this->expectNotToPerformAssertions();

        $input = $this->jsonFileToArray($folder . DIRECTORY_SEPARATOR . 'input.json');
        $outputFilename = $folder . '/output.sql';
        $settingsFilename = $folder . DIRECTORY_SEPARATOR . 'settings_override.json';

        $settingsOverride = [];
        if (file_exists($settingsFilename)) {
            $settingsOverride = $this->jsonFileToArray($settingsFilename);
        }

        $baseSettings = [
            'database' => [
                'prefix' => 'oc_',
            ],
        ];
        $config = Arr::mergeArraysRecursively($baseSettings, $settingsOverride);
        $application = new Application($config);
        $application->setLogger($this->getNullLogger());
        $mysqlConnection = new MySqlConnection($this->getPdoMock());
        $application->boot();

        $application->bind(ConnectionInterface::class, function () use ($mysqlConnection) {
            return $mysqlConnection;
        });

        $application->bind(Builder::class, function (Container $container) {
            return new Builder(
                $container->get(ConnectionInterface::class),
                $container->get(MySqlGrammar::class),
            );
        });

        $application->singleton(RulesRegistry::class, function () {
            return new RulesRegistry();
        });

        $application->singleton(Settings::class, function () {
            return new Settings([]);
        });

        /** @var RulesRegistry $rulesRegistry */
        $rulesRegistry = $application->get(RulesRegistry::class);
        $rulesRegistry->register(ProductPrice::NAME, ProductPrice::class);
        $rulesRegistry->register(ProductStatus::NAME, ProductStatus::class);
        $rulesRegistry->register(ProductModel::NAME, ProductModel::class);
        $rulesRegistry->register(ProductCategories::NAME, ProductCategories::class);
        $rulesRegistry->register(ProductManufacturer::NAME, ProductManufacturer::class);
        $rulesRegistry->register(ProductQuantity::NAME, ProductQuantity::class);
        $rulesRegistry->register(ProductAttribute::NAME, ProductAttribute::class);
        $rulesRegistry->register(ProductCategory::NAME, ProductCategory::class);

        $this->builder = $application->get(Builder::class);
        $this->criteriaBuilder = $application->get(CriteriaBuilder::class);
        $this->baseQuery = $this->builder->newQuery()
            ->select([
                'products.product_id' => 'product_id',
                'products.image' => 'image',
                'product_description.name' => 'name',
                'products.model' => 'model',
                'products.price' => 'price',
                'products.quantity' => 'quantity',
                'products.status' => 'status',
                'products.noindex' => 'noindex'
            ])
            ->from('oc_product', 'products')
            ->join(
                'oc_product_description AS product_description',
                function (JoinClause $join) {
                    return $join->on('products.product_id', '=', 'product_description.product_id')
                        ->where('product_description.language_id', '=', 1);
                }
            );

        $this->criteriaBuilder->apply($this->baseQuery, $input);

        $actual = $this->formatSql($this->baseQuery->toRawSql());

        if (! file_exists($outputFilename)) {
            file_put_contents($outputFilename, $actual);
            $this->markTestSkipped('Regenerated fixtures. Rerun the test.');
        }

        if (file_get_contents($outputFilename) !== $actual) {
            file_put_contents($outputFilename, $actual);
            $this->markTestIncomplete('SQL changed. Please check it and regenerate fixtures.');
        }
    }

    public static function CriteriaBuilderFixturesDataProvider(): array
    {
        return self::findDirectories(static::fixturesPath() . '/criteria_builder');
    }

    private static function findDirectories($rootFolder, $currentLevel = 1, $maxLevel = 2): array
    {
        $directories = [];

        if ($currentLevel > $maxLevel) {
            return $directories;
        }

        $iterator = new DirectoryIterator($rootFolder);
        foreach ($iterator as $fileInfo) {
            if ($fileInfo->isDot()) {
                continue;
            }

            if ($fileInfo->isDir()) {
                if ($currentLevel === 2) {
                    $parts = array_reverse(explode('/', $fileInfo->getPathname()));
                    $key = $parts[1] . '_' . $parts[0];
                    $directories[$key] = [
                        $fileInfo->getPathname(),
                    ];
                }

                $subDirectories = self::findDirectories($fileInfo->getPathname(), $currentLevel + 1, $maxLevel);
                $directories = array_merge($directories, $subDirectories);
            }
        }

        return $directories;
    }

    /**
     * @throws JsonException
     */
    private function jsonFileToArray(string $path): array
    {
        if (! file_exists($path)) {
            throw new InvalidArgumentException('Input file not found: ' . $path);
        }

        return json_decode(
            file_get_contents($path),
            true,
            512,
            JSON_THROW_ON_ERROR
        );
    }
}