# frozen_string_literal: true
require "graphql/compatibility/execution_specification/counter_schema"
require "graphql/compatibility/execution_specification/specification_schema"

module GraphQL
  module Compatibility
    # Test an execution strategy. This spec is not meant as a development aid.
    # Rather, when the strategy _works_, run it here to see if it has any differences
    # from the built-in strategy.
    #
    # - Custom scalar input / output
    # - Null propagation
    # - Query-level masking
    # - Directive support
    # - Typecasting
    # - Error handling (raise / return GraphQL::ExecutionError)
    # - Provides Irep & AST node to resolve fn
    # - Skipping fields
    #
    # Some things are explicitly _not_ tested here, because they're handled
    # by other parts of the system:
    #
    # - Schema definition (including types and fields)
    # - Parsing & parse errors
    # - AST -> IRep transformation (eg, fragment merging)
    # - Query validation and analysis
    # - Relay features
    #
    module ExecutionSpecification
      # Make a minitest suite for this execution strategy, making sure it
      # fulfills all the requirements of this library.
      # @param execution_strategy [<#new, #execute>] An execution strategy class
      # @return [Class<Minitest::Test>] A test suite for this execution strategy
      def self.build_suite(execution_strategy)
        Class.new(Minitest::Test) do
          class << self
            attr_accessor :counter_schema, :specification_schema
          end

          self.specification_schema = SpecificationSchema.build(execution_strategy)
          self.counter_schema = CounterSchema.build(execution_strategy)

          def execute_query(query_string, **kwargs)
            kwargs[:root_value] = SpecificationSchema::DATA
            self.class.specification_schema.execute(query_string, **kwargs)
          end

          def test_it_fetches_data
            query_string = %|
            query getData($nodeId: ID = "1001") {
              flh: node(id: $nodeId) {
                __typename
                ... on Person {
                  name @include(if: true)
                  skippedName: name @skip(if: true)
                  birthdate
                  age(on: 1477660133)
                }

                ... on NamedEntity {
                  ne_tn: __typename
                  ne_n: name
                }

                ... on Organization {
                  org_n: name
                }
              }
            }
            |
            res = execute_query(query_string)

            assert_equal nil, res["errors"], "It doesn't have an errors key"

            flh = res["data"]["flh"]
            assert_equal "Fannie Lou Hamer", flh["name"], "It returns values"
            assert_equal Time.new(1917, 10, 6).to_i, flh["birthdate"], "It returns custom scalars"
            assert_equal 99, flh["age"], "It runs resolve functions"
            assert_equal "Person", flh["__typename"], "It serves __typename"
            assert_equal "Person", flh["ne_tn"], "It serves __typename on interfaces"
            assert_equal "Fannie Lou Hamer", flh["ne_n"], "It serves interface fields"
            assert_equal false, flh.key?("skippedName"), "It obeys @skip"
            assert_equal false, flh.key?("org_n"), "It doesn't apply other type fields"
          end

          def test_it_iterates_over_each
            query_string = %|
              query getData($nodeId: ID = "1002") {
                node(id: $nodeId) {
                  ... on Person {
                    organizations { name }
                  }
                }
              }
            |

            res = execute_query(query_string)
            assert_equal ["SNCC"], res["data"]["node"]["organizations"].map { |o| o["name"] }
          end

          def test_it_skips_skipped_fields
            query_str = <<-GRAPHQL
            {
              o3001: organization(id: "3001")  { name }
              o2001: organization(id: "2001")  { name }
            }
            GRAPHQL

            res = execute_query(query_str)
            assert_equal ["o2001"], res["data"].keys
            assert_equal false, res.key?("errors")
          end

          def test_it_propagates_nulls_to_field
            query_string = %|
            query getOrg($id: ID = "2001"){
              failure: node(id: $id) {
                ... on Organization {
                  name
                  leader { name }
                }
              }
              success: node(id: $id) {
                ... on Organization {
                  name
                }
              }
            }
            |
            res = execute_query(query_string)

            failure = res["data"]["failure"]
            success = res["data"]["success"]

            assert_equal nil, failure, "It propagates nulls to the next nullable field"
            assert_equal({"name" => "SNCC"}, success, "It serves the same object if no invalid null is encountered")
            assert_equal 1, res["errors"].length , "It returns an error for the invalid null"
          end

          def test_it_propages_nulls_to_operation
            query_string = %|
              {
                foundOrg: organization(id: "2001") {
                  name
                }
                organization(id: "2999") {
                  name
                }
              }
            |

            res = execute_query(query_string)
            assert_equal nil, res["data"]
            assert_equal 1, res["errors"].length
          end

          def test_it_exposes_raised_and_returned_user_execution_errors
            query_string = %|
              {
                organization(id: "2001") {
                  name
                  returnedError
                  raisedError
                }
                organizations {
                  returnedError
                  raisedError
                }
              }
            |

            res = execute_query(query_string)

            assert_equal "SNCC", res["data"]["organization"]["name"], "It runs the rest of the query"

            expected_errors = [
              {
                "message"=>"This error was returned",
                "locations"=>[{"line"=>5, "column"=>19}],
                "path"=>["organization", "returnedError"]
              },
              {
                "message"=>"This error was raised",
                "locations"=>[{"line"=>6, "column"=>19}],
                "path"=>["organization", "raisedError"]
              },
              {
                "message"=>"This error was raised",
                "locations"=>[{"line"=>10, "column"=>19}],
                "path"=>["organizations", 0, "raisedError"]
              },
              {
                "message"=>"This error was raised",
                "locations"=>[{"line"=>10, "column"=>19}],
                "path"=>["organizations", 1, "raisedError"]
              },
              {
                "message"=>"This error was returned",
                "locations"=>[{"line"=>9, "column"=>19}],
                "path"=>["organizations", 0, "returnedError"]
              },
              {
                "message"=>"This error was returned",
                "locations"=>[{"line"=>9, "column"=>19}],
                "path"=>["organizations", 1, "returnedError"]
              },
            ]

            expected_errors.each do |expected_err|
              assert_includes res["errors"], expected_err
            end
          end

          def test_it_applies_masking
            no_org = ->(member, ctx) { member.name == "Organization" }
            query_string = %|
            {
              node(id: "2001") {
                __typename
              }
            }|

            err = assert_raises(GraphQL::UnresolvedTypeError) {
              execute_query(query_string, except: no_org)
            }

            query_string = %|
            {
              organization(id: "2001") { name }
            }|

            res = execute_query(query_string, except: no_org)

            assert_equal nil, res["data"]
            assert_equal 1, res["errors"].length
            assert_equal "SNCC", err.value.name
            assert_equal GraphQL::Relay::Node.interface, err.field.type
            assert_equal 1, err.possible_types.length
            assert_equal "Organization", err.resolved_type.name
            assert_equal "Query", err.parent_type.name

            query_string = %|
            {
              __type(name: "Organization") { name }
            }|

            res = execute_query(query_string, except: no_org)

            assert_equal nil, res["data"]["__type"]
            assert_equal nil, res["errors"]
          end

          def test_it_provides_nodes_to_resolve
            query_string = %|
            {
              organization(id: "2001") {
                name
                nodePresence
              }
            }|

            res = execute_query(query_string)
            assert_equal "SNCC", res["data"]["organization"]["name"]
            assert_equal [true, true, false], res["data"]["organization"]["nodePresence"]
          end

          def test_it_runs_the_introspection_query
            execute_query(GraphQL::Introspection::INTROSPECTION_QUERY)
          end

          def test_it_propagates_deeply_nested_nulls
            query_string = %|
            {
              node(id: "1001") {
                ... on Person {
                  name
                  first_organization {
                    leader {
                      name
                    }
                  }
                }
              }
            }
            |
            res = execute_query(query_string)
            assert_equal nil, res["data"]["node"]
            assert_equal 1, res["errors"].length
          end

          def test_it_doesnt_add_errors_for_invalid_nulls_from_execution_errors
            query_string = %|
            query getOrg($id: ID = "2001"){
              failure: node(id: $id) {
                ... on Organization {
                  name
                  leader { name }
                }
              }
            }
            |
            res = execute_query(query_string, context: {return_error: true})
            error_messages = res["errors"].map { |e| e["message"] }
            assert_equal ["Error on Nullable"], error_messages
          end

          def test_it_only_resolves_fields_once_on_typed_fragments
            res = self.class.counter_schema.execute("
            {
              counter { count }
              ... on HasCounter {
                counter { count }
              }
            }
            ")

            expected_data = {
              "counter" => { "count" => 1 }
            }
            assert_equal expected_data, res["data"]
            assert_equal 1, self.class.counter_schema.metadata[:count]

            # Deep typed children are correctly distinguished:
            res = self.class.counter_schema.execute("
            {
              counter {
                ... on Counter {
                  counter { count }
                }
                ... on AltCounter {
                  counter { count, t: __typename }
                }
              }
            }
            ")

            expected_data = {
              "counter" => { "counter" => { "count" => 2 } }
            }
            assert_equal expected_data, res["data"]
          end

          def test_it_runs_middleware
            log = []
            query_string = %|
            {
              node(id: "2001") {
                __typename
              }
            }|
            execute_query(query_string, context: {middleware_log: log})
            assert_equal ["node", "__typename"], log
          end

          def test_it_uses_type_error_hooks_for_invalid_nulls
            log = []
            query_string = %|
            {
              node(id: "1001") {
                ... on Person {
                  name
                  first_organization {
                    leader {
                      name
                    }
                  }
                }
              }
            }|

            res = execute_query(query_string, context: { type_errors: log })
            assert_equal nil, res["data"]["node"]
            assert_equal [nil], log
          end

          def test_it_uses_type_error_hooks_for_failed_type_resolution
            log = []
            query_string = %|
            {
              node(id: "2003") {
                __typename
              }
            }|

            assert_raises(GraphQL::UnresolvedTypeError) {
              execute_query(query_string, context: { type_errors: log })
            }

            assert_equal [SpecificationSchema::BOGUS_NODE], log
          end

          def test_it_treats_failed_type_resolution_like_nil
            log = []
            ctx = { type_errors: log, gobble: true }
            query_string = %|
            {
              node(id: "2003") {
                __typename
              }
            }|

            res = execute_query(query_string, context: ctx)

            assert_equal nil, res["data"]["node"]
            assert_equal false, res.key?("errors")
            assert_equal [SpecificationSchema::BOGUS_NODE], log

            query_string_2 = %|
            {
              requiredNode(id: "2003") {
                __typename
              }
            }|

            res = execute_query(query_string_2, context: ctx)

            assert_equal nil, res["data"]
            assert_equal false, res.key?("errors")
            assert_equal [SpecificationSchema::BOGUS_NODE, SpecificationSchema::BOGUS_NODE], log
          end

          def test_it_skips_connections
            query_type = GraphQL::ObjectType.define do
              name "Query"
              connection :skipped, types[query_type], resolve: ->(o,a,c) { c.skip }
            end
            schema = GraphQL::Schema.define(query: query_type)
            res = schema.execute("{ skipped { __typename } }")
            assert_equal({"data" => nil}, res)
          end
        end
      end
    end
  end
end
