# frozen_string_literal: true
require "spec_helper"

describe GraphQL::Query::Arguments do
  let(:arguments) {
    test_input_1 = GraphQL::InputObjectType.define do
      name "TestInput1"
      argument :d, types.Int
      argument :e, types.Int
    end

    test_input_2 = GraphQL::InputObjectType.define do
      name "TestInput2"
      argument :a, types.Int
      argument :b, types.Int
      argument :c, !test_input_1, as: :inputObject
    end

    GraphQL::Query::Arguments.new({
      a: 1,
      b: 2,
      c: GraphQL::Query::Arguments.new({
        d: 3,
        e: 4,
      }, argument_definitions: test_input_1.arguments),
    }, argument_definitions: test_input_2.arguments)
  }

  it "returns keys as strings, with aliases" do
    assert_equal(["a", "b", "inputObject"], arguments.keys)
  end

  it "delegates values to values hash" do
    assert_equal([1, 2, {"d" => 3, "e" => 4}], arguments.values)
  end

  it "delegates each to values hash" do
    pairs = []
    arguments.each do |key, value|
      pairs << [key, value]
    end
    assert_equal([["a", 1], ["b", 2], ["inputObject", {"d" => 3, "e" => 4}]], pairs)
  end

  it "returns a stringified, aliased hash with to_h" do
    assert_equal({ "a"=> 1, "b" => 2, "inputObject" => { "d" => 3, "e" => 4 } }, arguments.to_h)
  end

  it "yields key, value, and arg_defnition" do
    type_info = []
    arguments.each_value do |arg_value|
      value = arg_value.value.is_a?(GraphQL::Query::Arguments) ? arg_value.value.to_h : arg_value.value
      type_info << [arg_value.key, value, arg_value.definition.type.unwrap.name]
    end

    expected_type_info =[
      ["a", 1, "Int"],
      ["b", 2, "Int"],
      ["inputObject", { "d" => 3, "e" => 4 }, "TestInput1"],
    ]
    assert_equal expected_type_info, type_info
  end

  it "can be copied to a new Arguments instance" do
    transformed_args = {}
    types = {}
    arguments.each_value do |arg_value|
      transformed_args[arg_value.key.upcase] = arg_value.value
      defn = arg_value.definition
      types[arg_value.key.upcase] = defn.redefine(
        name: defn.name.upcase,
        as: defn.as ? defn.as.to_s.upcase : nil,
      )
    end

    new_arguments = GraphQL::Query::Arguments.new(transformed_args, argument_definitions: types)
    expected_hash = {
      "A" => 1,
      "B" => 2,
      "INPUTOBJECT" => { "d"  => 3 , "e" => 4 },
    }
    assert_equal expected_hash, new_arguments.to_h
  end

  describe "nested hashes" do
    let(:input_type) {
      test_input_type = GraphQL::InputObjectType.define do
        name "TestInput"
        argument :a, types.Int
        argument :b, test_input_type
        argument :c, types.Int # will be a hash
      end
      GraphQL::Query::Arguments.construct_arguments_class(test_input_type)
      test_input_type
    }
    it "wraps input objects, but not other hashes" do
      args = GraphQL::Query::Arguments.new(
        {a: 1, b: {a: 2}, c: {a: 3}},
        argument_definitions: input_type.arguments
      )
      assert_kind_of GraphQL::Query::Arguments, args["b"]
      assert_instance_of Hash, args["c"]
    end
  end

  describe "#[]" do
    it "fetches using specified `as` keyword" do
      assert arguments["inputObject"].is_a?(GraphQL::Query::Arguments)
    end

    it "returns the value at that key" do
      assert_equal 1, arguments["a"]
      assert_equal 1, arguments[:a]
      assert arguments["inputObject"].is_a?(GraphQL::Query::Arguments)
    end

    it "returns nil for missing keys" do
      assert_equal nil, arguments["z"]
      assert_equal nil, arguments[7]
    end
  end

  describe "#key?" do
    let(:arg_values) { [] }
    let(:schema) {
      arg_values_array = arg_values

      test_input_type = GraphQL::InputObjectType.define do
        name "TestInput"
        argument :a, types.Int
        argument :b, types.Int, default_value: 2
        argument :c, types.Int
        argument :d, types.Int
      end

      query = GraphQL::ObjectType.define do
        name "Query"
        field :argTest, types.Int do
          argument :a, types.Int
          argument :b, types.Int, default_value: 2
          argument :c, types.Int, as: :specialKeyName
          argument :d, test_input_type
          resolve ->(obj, args, ctx) {
            arg_values_array << args
            1
          }
        end

        field :noArgTest, types.Int do
          resolve ->(obj, args, ctx) {
            arg_values_array << args
            1
          }
        end

        field :noDefaultsTest, types.Int do
          argument :a, types.Int
          argument :b, types.Int
          resolve ->(obj, args, ctx) {
            arg_values_array << args
            1
          }
          resolve ->(obj, args, ctx) {
            arg_values_array << args
            1
          }
        end
      end

      GraphQL::Schema.define(query: query)
    }

    it "detects missing keys by string or symbol" do
      assert_equal true, arguments.key?(:a)
      assert_equal true, arguments.key?("a")
      assert_equal false, arguments.key?(:f)
      assert_equal false, arguments.key?("f")
    end

    it "detects keys using `as` to rename an arg at resolve time" do
      schema.execute("{ argTest(c: 1) }")

      last_args = arg_values.last

      assert_equal true, last_args.key?(:specialKeyName)
      assert_equal true, last_args.key?("specialKeyName")
    end

    it "works from query literals" do
      schema.execute("{ argTest(a: 1) }")

      last_args = arg_values.last

      assert_equal true, last_args.key?(:a)
      # This is present from default value:
      assert_equal true, last_args.key?(:b)
      assert_equal false, last_args.key?(:c)
      assert_equal({"a" => 1, "b" => 2}, last_args.to_h)
    end

    it "works from variables" do
      variables = { "arg" => { "a" => 1, "d" => nil } }
      schema.execute("query ArgTest($arg: TestInput){ argTest(d: $arg) }", variables: variables)

      test_inputs = arg_values.last["d"]

      assert_equal true, test_inputs.key?(:a)
      # This is present from default value:
      assert_equal true, test_inputs.key?(:b)

      assert_equal false, test_inputs.key?(:c)
      assert_equal true, test_inputs.key?(:d)

      assert_equal({"a" => 1, "b" => 2, "d" => nil}, test_inputs.to_h)
    end

    it "works with variable default values" do
      schema.execute("query ArgTest($arg: TestInput = {a: 1}){ argTest(d: $arg) }")

      test_defaults = arg_values.last["d"]

      assert_equal true, test_defaults.key?(:a)
      # This is present from default val
      assert_equal true, test_defaults.key?(:b)

      assert_equal false, test_defaults.key?(:c)
      assert_equal false, test_defaults.key?(:d)
      assert_equal({"a" => 1, "b" => 2}, test_defaults.to_h)
    end

    it "works with variable default values with null" do
      schema.execute("query ArgTest($arg: TestInput = {d: null}){ argTest(d: $arg) }")

      test_defaults = arg_values.last["d"]

      assert_equal false, test_defaults.key?(:a)
      # This is present from default val
      assert_equal true, test_defaults.key?(:b)

      assert_equal false, test_defaults.key?(:c)
      assert_equal true, test_defaults.key?(:d)

      assert_equal({"d" => nil, "b" => 2}, test_defaults.to_h)
    end
  end

  describe "prepare" do
    let(:arg_values) { [] }
    let(:schema) {
      arg_values_array = arg_values
      test_input_1 = GraphQL::InputObjectType.define do
        name "TestInput1"
        argument :a, types.Int, prepare: ->(value, ctx) do
          value * 10
        end
      end

      test_input_2 = GraphQL::InputObjectType.define do
        name "TestInput2"
        argument :b, !test_input_1, as: :inputObject
      end

      query = GraphQL::ObjectType.define do
        name "Query"
        field :prepareTest, types.Int do
          argument :a, test_input_2
          resolve ->(obj, args, ctx) {
            arg_values_array << args
            1
          }
        end
      end

      GraphQL::Schema.define(query: query)
    }

    it "returns prepared argument value for nested input type" do
      schema.execute("query prepareTest($arg: TestInput2 = {b: {a: 2}}){ prepareTest(a: $arg) }")

      args = arg_values[0].values[0]
      assert_equal 2 * 10, args['inputObject']['a']
    end

    it "returns prepared argument value for nested input type" do
      query_str = "
        query($arg: TestInput2){
          prepareTest(a: $arg)
      }"

      schema.execute(query_str, variables: { "arg" => { "b" => { "a" => 3 } } } )

      args = arg_values[0].values[0]
      assert_equal 30, args['inputObject']['a']
    end
  end

  describe "construct_arguments_class" do
    let(:input_object) do
      GraphQL::InputObjectType.define do
        argument :foo, types.Int
        argument :bar, types.Int
      end
    end

    it "generates argument classes that responds to keys as functions" do
      assert_equal nil, input_object.arguments_class

      GraphQL::Query::Arguments.construct_arguments_class(input_object)
      args = input_object.arguments_class.new({foo: 3, bar: -90})

      assert_equal 3, args.foo
      assert_equal -90, args.bar
    end
  end
end