<?php

declare(strict_types=1);

namespace GraphQL\Tests\Experimental\Executor;

use GraphQL\Error\FormattedError;
use GraphQL\Experimental\Executor\Collector;
use GraphQL\Experimental\Executor\Runtime;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\Parser;
use GraphQL\Tests\StarWarsSchema;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use PHPUnit\Framework\TestCase;
use stdClass;
use Throwable;
use function array_map;
use function basename;
use function file_exists;
use function file_put_contents;
use function json_encode;
use function strlen;
use function strncmp;
use const DIRECTORY_SEPARATOR;
use const JSON_PRETTY_PRINT;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

class CollectorTest extends TestCase
{
    /**
     * @param mixed[]|null $variableValues
     *
     * @dataProvider provideForTestCollectFields
     */
    public function testCollectFields(Schema $schema, DocumentNode $documentNode, string $operationName, ?array $variableValues)
    {
        $runtime = new class($variableValues) implements Runtime
        {
            /** @var Throwable[] */
            public $errors = [];

            /** @var mixed[]|null */
            public $variableValues;

            public function __construct($variableValues)
            {
                $this->variableValues = $variableValues;
            }

            public function evaluate(ValueNode $valueNode, InputType $type)
            {
                return AST::valueFromAST($valueNode, $type, $this->variableValues);
            }

            public function addError($error)
            {
                $this->errors[] = $error;
            }
        };

        $collector = new Collector($schema, $runtime);
        $collector->initialize($documentNode, $operationName);

        $pipeline = [];
        foreach ($collector->collectFields($collector->rootType, $collector->operation->selectionSet) as $shared) {
            $execution = new stdClass();
            if (! empty($shared->fieldNodes)) {
                $execution->fieldNodes = array_map(static function (Node $node) {
                    return $node->toArray(true);
                }, $shared->fieldNodes);
            }
            if (! empty($shared->fieldName)) {
                $execution->fieldName = $shared->fieldName;
            }
            if (! empty($shared->resultName)) {
                $execution->resultName = $shared->resultName;
            }
            if (! empty($shared->argumentValueMap)) {
                $execution->argumentValueMap = [];
                foreach ($shared->argumentValueMap as $argumentName => $valueNode) {
                    /** @var Node $valueNode */
                    $execution->argumentValueMap[$argumentName] = $valueNode->toArray(true);
                }
            }

            $pipeline[] = $execution;
        }
        if (strncmp($operationName, 'ShouldEmitError', strlen('ShouldEmitError')) === 0) {
            self::assertNotEmpty($runtime->errors, 'There should be errors.');
        } else {
            self::assertEmpty($runtime->errors, 'There must be no errors. Got: ' . json_encode($runtime->errors, JSON_PRETTY_PRINT));

            if (strncmp($operationName, 'ShouldNotEmit', strlen('ShouldNotEmit')) === 0) {
                self::assertEmpty($pipeline, 'No instructions should be emitted.');
            } else {
                self::assertNotEmpty($pipeline, 'There should be some instructions emitted.');
            }
        }

        $result = [];
        if (! empty($runtime->errors)) {
            $result['errors'] = array_map(
                FormattedError::prepareFormatter(null, false),
                $runtime->errors
            );
        }
        if (! empty($pipeline)) {
            $result['pipeline'] = $pipeline;
        }

        $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";

        $fileName = __DIR__ . DIRECTORY_SEPARATOR . basename(__FILE__, '.php') . 'Snapshots' . DIRECTORY_SEPARATOR . $operationName . '.json';
        if (! file_exists($fileName)) {
            file_put_contents($fileName, $json);
        }

        self::assertStringEqualsFile($fileName, $json);
    }

    public function provideForTestCollectFields()
    {
        $testCases = [
            [
                StarWarsSchema::build(),
                'query ShouldEmitFieldWithoutArguments {
                    human {
                        name                    
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitFieldThatHasArguments($id: ID!) {
                    human(id: $id) {
                        name
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitForInlineFragment($id: ID!) {
                    ...HumanById
                }
                fragment HumanById on Query {
                    human(id: $id) {
                        ... on Human {
                            name
                        }
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitObjectFieldForFragmentSpread($id: ID!) {
                    human(id: $id) {
                        ...HumanName
                    }
                }
                fragment HumanName on Human {
                    name
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitTypeName {
                    queryTypeName: __typename
                    __typename
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitIfIncludeConditionTrue($id: ID!, $condition: Boolean!) {
                    droid(id: $id) @include(if: $condition) {
                        id
                    }
                }',
                ['condition' => true],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitIfIncludeConditionFalse($id: ID!, $condition: Boolean!) {
                    droid(id: $id) @include(if: $condition) {
                        id
                    }
                }',
                ['condition' => false],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitIfSkipConditionTrue($id: ID!, $condition: Boolean!) {
                    droid(id: $id) @skip(if: $condition) {
                        id
                    }
                }',
                ['condition' => true],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitIfSkipConditionFalse($id: ID!, $condition: Boolean!) {
                    droid(id: $id) @skip(if: $condition) {
                        id
                    }
                }',
                ['condition' => false],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitIncludeSkipTT($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
                    droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
                        id
                    }
                }',
                ['includeCondition' => true, 'skipCondition' => true],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitIncludeSkipTF($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
                    droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
                        id
                    }
                }',
                ['includeCondition' => true, 'skipCondition' => false],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitIncludeSkipFT($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
                    droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
                        id
                    }
                }',
                ['includeCondition' => false, 'skipCondition' => true],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitIncludeSkipFF($id: ID!, $includeCondition: Boolean!, $skipCondition: Boolean!) {
                    droid(id: $id) @include(if: $includeCondition) @skip(if: $skipCondition) {
                        id
                    }
                }',
                ['includeCondition' => false, 'skipCondition' => false],
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitSkipAroundInlineFragment {
                    ... on Query @skip(if: true) {
                        hero(episode: 5) {
                            name
                        }
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitSkipAroundInlineFragment {
                    ... on Query @skip(if: false) {
                        hero(episode: 5) {
                            name
                        }
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitIncludeAroundInlineFragment {
                    ... on Query @include(if: true) {
                        hero(episode: 5) {
                            name
                        }
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitIncludeAroundInlineFragment {
                    ... on Query @include(if: false) {
                        hero(episode: 5) {
                            name
                        }
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitSkipFragmentSpread {
                    ...Hero @skip(if: true)
                }
                fragment Hero on Query {
                    hero(episode: 5) {
                        name
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitSkipFragmentSpread {
                    ...Hero @skip(if: false)
                }
                fragment Hero on Query {
                    hero(episode: 5) {
                        name
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitIncludeFragmentSpread {
                    ...Hero @include(if: true)
                }
                fragment Hero on Query {
                    hero(episode: 5) {
                        name
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldNotEmitIncludeFragmentSpread {
                    ...Hero @include(if: false)
                }
                fragment Hero on Query {
                    hero(episode: 5) {
                        name
                    }
                }',
                null,
            ],
            [
                StarWarsSchema::build(),
                'query ShouldEmitSingleInstrictionForSameResultName($id: ID!) {
                    human(id: $id) {
                        name
                        name: secretBackstory 
                    }
                }',
                null,
            ],
        ];

        $data = [];
        foreach ($testCases as [$schema, $query, $variableValues]) {
            $documentNode  = Parser::parse($query, ['noLocation' => true]);
            $operationName = null;
            foreach ($documentNode->definitions as $definitionNode) {
                /** @var Node $definitionNode */
                if ($definitionNode->kind === NodeKind::OPERATION_DEFINITION) {
                    /** @var OperationDefinitionNode $definitionNode */
                    self::assertNotNull($definitionNode->name);
                    $operationName = $definitionNode->name->value;
                    break;
                }
            }

            self::assertArrayNotHasKey($operationName, $data);

            $data[$operationName] = [$schema, $documentNode, $operationName, $variableValues];
        }

        return $data;
    }
}