{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}

-- | Tests for query validation.
module ValidationSpec (spec) where

import Protolude

import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck ((===))
import Test.Hspec
import qualified Data.Set as Set

import GraphQL.Internal.Name (Name)
import qualified GraphQL.Internal.Syntax.AST as AST
import GraphQL.Internal.Schema (emptySchema, Schema)
import GraphQL.Internal.Validation
  ( ValidationError(..)
  , findDuplicates
  , getErrors
  , formatErrors
  )

me :: Maybe Name
me = pure "me"

someName :: Name
someName = "name"

dog :: Name
dog = "dog"

-- | Schema used for these tests. Since none of them do type-level stuff, we
-- don't need to define it.
schema :: Schema
schema = emptySchema

spec :: Spec
spec = describe "Validation" $ do
  describe "getErrors" $ do
    it "Treats simple queries as valid" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                  ( AST.Query
                    ( AST.Node me [] []
                      [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                      ]
                    )
                  )
                ]
      getErrors schema doc `shouldBe` []

    it "Treats anonymous queries as valid" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                  (AST.Query
                    (AST.Node Nothing [] []
                      [ AST.SelectionField
                        (AST.Field Nothing dog [] []
                          [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                          ])
                      ]))
                ]
      getErrors schema doc `shouldBe` []

    it "Treats anonymous queries with variables as valid" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                    (AST.Query
                      (AST.Node Nothing
                       [ AST.VariableDefinition
                           (AST.Variable "atOtherHomes")
                           (AST.TypeNamed (AST.NamedType "Boolean"))
                           (Just (AST.ValueBoolean True))
                       ] []
                       [ AST.SelectionField
                           (AST.Field Nothing dog [] []
                            [ AST.SelectionField
                                (AST.Field Nothing "isHousetrained"
                                 [ AST.Argument "atOtherHomes"
                                     (AST.ValueVariable (AST.Variable "atOtherHomes"))
                                 ] [] [])
                            ])
                       ]))
                ]
      getErrors schema doc `shouldBe` []
    it "Treats anonymous queries with annotated variables as valid ([[Boolean]]!)" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                    (AST.Query
                      (AST.Node Nothing
                       [ AST.VariableDefinition
                           (AST.Variable "atOtherHomes")
                           (AST.TypeNonNull (AST.NonNullTypeList (AST.ListType 
                            (AST.TypeList (AST.ListType (AST.TypeNamed (AST.NamedType "Boolean"))))
                           )))
                           Nothing
                       ] []
                       [ AST.SelectionField
                           (AST.Field Nothing dog [] []
                            [ AST.SelectionField
                                (AST.Field Nothing "isHousetrained"
                                 [ AST.Argument "atOtherHomes"
                                     (AST.ValueVariable (AST.Variable "atOtherHomes"))
                                 ] [] [])
                            ])
                       ]))
                ]
      getErrors schema doc `shouldBe` []

    it "Detects duplicate operation names" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                  ( AST.Query
                    ( AST.Node me [] []
                      [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                      ]
                    )
                  )
                , AST.DefinitionOperation
                  ( AST.Query
                    ( AST.Node me [] []
                      [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                      ]
                    )
                  )
                ]
      getErrors schema doc `shouldBe` [DuplicateOperation me]

    it "Detects duplicate anonymous operations" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                  ( AST.AnonymousQuery
                    [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                    ]
                  )
                , AST.DefinitionOperation
                  ( AST.AnonymousQuery
                    [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                    ]
                  )
                ]
      let errors = getErrors schema doc
      errors `shouldBe` [MixedAnonymousOperations 2 []]
      formatErrors errors `shouldBe` ["Multiple anonymous operations defined. Found 2"]

    it "Detects mixed operations" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                  ( AST.AnonymousQuery
                    [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                    ]
                  )
                , AST.DefinitionOperation
                  ( AST.Query (AST.Node (pure "houseTrainedQuery") [] []
                    [ AST.SelectionField (AST.Field Nothing someName [] [] [])
                    ]
                  ))
                ]
      let errors = getErrors schema doc
      errors `shouldBe` [MixedAnonymousOperations 1 [Just "houseTrainedQuery"]]
      formatErrors errors `shouldBe` ["Document contains both anonymous operations (1) and named operations ([Just (Name {unName = \"houseTrainedQuery\"})])"]

    it "Detects non-existing type in variable definition" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                    (AST.Query
                      (AST.Node Nothing
                       [ AST.VariableDefinition
                           (AST.Variable "atOtherHomes")
                           (AST.TypeNamed (AST.NamedType "MyNonExistingType"))
                           (Just (AST.ValueBoolean True))
                       ] []
                       [ AST.SelectionField
                           (AST.Field Nothing dog [] []
                            [ AST.SelectionField
                                (AST.Field Nothing "isHousetrained"
                                 [ AST.Argument "atOtherHomes"
                                     (AST.ValueVariable (AST.Variable "atOtherHomes"))
                                 ] [] [])
                            ])
                       ]))
                ]
      getErrors schema doc `shouldBe` [VariableTypeNotFound (AST.Variable "atOtherHomes") "MyNonExistingType"]

    it "Detects unused variable definition" $ do
      let doc = AST.QueryDocument
                [ AST.DefinitionOperation
                    (AST.Query
                      (AST.Node Nothing
                       [ AST.VariableDefinition
                           (AST.Variable "atOtherHomes")
                           (AST.TypeNamed (AST.NamedType "String"))
                           (Just (AST.ValueBoolean True))
                       ] []
                       [ AST.SelectionField
                           (AST.Field Nothing dog [] []
                            [ AST.SelectionField
                                (AST.Field Nothing "isHousetrained"
                                 [] [] [])
                            ])
                       ]))
                ]
      getErrors schema doc `shouldBe` [UnusedVariables (Set.fromList [AST.Variable "atOtherHomes"])]

    it "Treats anonymous queries with inline arguments as valid" $ do
      let doc = AST.QueryDocument
                     [ AST.DefinitionOperation
                         (AST.Query
                           (AST.Node Nothing
                            [] []
                            [ AST.SelectionField
                                (AST.Field Nothing dog [] []
                                 [ AST.SelectionField
                                     (AST.Field Nothing "isHousetrained"
                                      [ AST.Argument "atOtherHomes"
                                          (AST.ValueList (AST.ListValue [
                                            (AST.ValueObject (AST.ObjectValue [
                                              (AST.ObjectField "testKey" (AST.ValueInt 123)),
                                              (AST.ObjectField "anotherKey" (AST.ValueString (AST.StringValue "string")))
                                            ]))
                                          ]))
                                      ] [] [])
                                 ])
                            ]))
                     ]
      getErrors schema doc `shouldBe` []
    it "Detects non-existent fragment type" $ do
      let doc = AST.QueryDocument
                  [(AST.DefinitionFragment (AST.FragmentDefinition "dogTest"
                    (AST.NamedType "Dog") [] [
                      AST.SelectionField (AST.Field Nothing "name" [] [] [])
                      ])),
                        (AST.DefinitionOperation
                         (AST.Query
                           (AST.Node Nothing
                            [] []
                            [AST.SelectionField
                              (AST.Field Nothing dog [] []
                                [AST.SelectionFragmentSpread (AST.FragmentSpread "dogTest" [])
                                ])    
                            ])))
                     ]
      getErrors schema doc `shouldBe` [TypeConditionNotFound "Dog"]

  describe "findDuplicates" $ do
    prop "returns empty on unique lists" $ do
      \xs -> findDuplicates @Int (ordNub xs) === []
    prop "finds only duplicates" $ \xs -> do
      all (>1) (count xs <$> findDuplicates @Int xs)
    prop "finds all duplicates" $ \xs -> do
      (sort . findDuplicates @Int) xs === (ordNub . sort . filter ((> 1) . count xs)) xs


-- | Count the number of times 'x' occurs in 'xs'.
count :: Eq a => [a] -> a -> Int
count xs x = (length . filter (== x)) xs
