{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}

module Jordan.Servant.Query.ParseSpec where

import qualified Data.Attoparsec.ByteString as AP
import Data.ByteString (ByteString)
import Data.Text (Text)
import GHC.Generics
import Jordan.FromJSON.Class
import Jordan.Servant.Query
import Jordan.Servant.Query.Parse
import Network.HTTP.Types.URI
import Test.Hspec

data CategoryName = CategoryName {category :: Text, name :: Text}
  deriving (Show, Eq, Ord, Generic)
  deriving anyclass (FromJSON)

data CoolFilter = CoolFilter {filterName :: Maybe Text, filterDesc :: Maybe Text}
  deriving (Show, Eq, Ord, Generic)

instance FromJSON CoolFilter where
  fromJSON =
    parseObject $
      CoolFilter
        <$> parseFieldWithDefault "name" (Just <$> fromJSON) Nothing
        <*> parseFieldWithDefault "desc" (Just <$> fromJSON) Nothing

newtype CoolFilters = CoolFilters {getCoolFilters :: [CoolFilter]}
  deriving (Show, Eq, Ord, Generic)

instance FromJSON CoolFilters where
  fromJSON = CoolFilters <$> fromJSON

data IntRange
  = UnboundedAfter Int
  | UnboundedBefore Int
  | Within Int Int
  deriving (Show, Eq, Ord, Generic)

instance FromJSON IntRange where
  fromJSON =
    parseObject (Within <$> parseField "s" <*> parseField "e")
      <> parseObject (UnboundedAfter <$> parseField "s")
      <> parseObject (UnboundedBefore <$> parseField "e")

newtype IntMultiRange = IntMultiRange {getRanges :: [IntRange]}
  deriving (Show, Eq, Ord, Generic)

instance FromJSON IntMultiRange where
  fromJSON = IntMultiRange <$> fromJSON

newtype FilterValues = FilterValues {getFilterValues :: [(Text, Int)]}
  deriving (Show, Eq, Ord, Generic)

instance FromJSON FilterValues where
  fromJSON = FilterValues <$> parseDictionary fromJSON

newtype DictRange = DictRange {getDictRange :: [(Text, IntRange)]}
  deriving (Show, Eq, Ord, Generic)

instance FromJSON DictRange where
  fromJSON = DictRange <$> parseDictionary fromJSON

parsesQTo :: (Show a, Eq a, FromJSON a) => ByteString -> a -> SpecWith ()
parsesQTo bs a =
  it ("parses query string " <> show bs <> " to " <> show a) $
    parseQueryAtKey "q" (parseQueryReplacePlus False bs) `shouldBe` Right a

spec :: Spec
spec = describe "Jordan.Servant.Query" $ do
  stringParsing

stringParsing :: Spec
stringParsing = describe "parsing query strings" $ do
  describe "basic single-field parsing" $ do
    "q=10" `parsesQTo` (10 :: Int)
    "q=foo" `parsesQTo` ("foo" :: Text)
  describe "object parsing with no defaults" $ do
    "q[category]=posts&q[name]=bob" `parsesQTo` CategoryName "posts" "bob"
    "q[name]=joe&q[category]=videos" `parsesQTo` CategoryName "videos" "joe"
  describe "array parsing" $ do
    "q[]=1" `parsesQTo` [1 :: Int]
    "q[]=1&q[]=2" `parsesQTo` [1 :: Int, 2]
  describe "object parsing with two defaults" $ do
    "q[s]=100" `parsesQTo` UnboundedAfter 100
    "q[e]=10" `parsesQTo` UnboundedBefore 10
    "q[e]=10&q[s]=1" `parsesQTo` Within 1 10
  describe "arrays of object parsing" $ do
    "q[][s]=1&q[][e]=10&q[][s]=25" `parsesQTo` IntMultiRange [Within 1 10, UnboundedAfter 25]
  describe "arrays of non-failing objects" $ do
    "q[][name]=wow" `parsesQTo` CoolFilters [CoolFilter (Just "wow") Nothing]
    "" `parsesQTo` CoolFilters []
  describe "simple dictionaries" $ do
    "q[foo]=10&q[bar]=11" `parsesQTo` FilterValues [("foo", 10), ("bar", 11)]
    "" `parsesQTo` FilterValues []
    "q[foo]=100" `parsesQTo` FilterValues [("foo", 100)]
  describe "dictionaries of objects" $ do
    "q[var][e]=10&q[bar][s]=11&q[bar][e]=100" `parsesQTo` DictRange [("var", UnboundedBefore 10), ("bar", Within 11 100)]

parsesAsKey :: ByteString -> QueryKey -> SpecWith ()
parsesAsKey input output =
  it ("should parse " <> show input <> " to query key " <> show output) $
    AP.parseOnly parseQueryKey input `shouldBe` Right output