# LANGUAGE DataKinds #
# LANGUAGE FlexibleContexts #
{-# LANGUAGE GADTs #-}
# LANGUAGE GeneralizedNewtypeDeriving #
# LANGUAGE KindSignatures #
# LANGUAGE LambdaCase #
# LANGUAGE QuasiQuotes #
# LANGUAGE ScopedTypeVariables #
# LANGUAGE TemplateHaskell #

module Tests.Object.FromJSON where

import Data.Aeson (FromJSON (..), Value)
import Data.Aeson.QQ (aesonQQ)
import Data.Aeson.Types (parseEither)
import Data.Proxy (Proxy)
import Test.Tasty
import Test.Tasty.QuickCheck

import Data.Aeson.Schema (Object)
import TestUtils (parseProxy, testGolden)
import TestUtils.Arbitrary (ArbitraryObject (..), forAllArbitraryObjects)
import Tests.Object.FromJSON.TH

test :: TestTree
test =
  testGroup "FromJSON instance" $
    map runTestCase testCases
      ++ [ testProperty "QuickCheck arbitrary Schema" $
            $(forAllArbitraryObjects) $ \(ArbitraryObject proxy v _) ->
              case parseProxy proxy v of
                Right _ -> property ()
                Left e -> error $ "Could not parse: " ++ e
         ]

testCases :: [FromJSONTestCase]
testCases =
  [ CheckValid
      "Scalar valid"
      [schemaProxy| { foo: Text } |]
      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]
  , CheckError
      "Scalar invalid"
      "fromjson_scalar_invalid.golden"
      [schemaProxy| { foo: Text } |]
      [aesonQQ| { "foo": 1 } |]
  , CheckValid
      "Maybe valid"
      [schemaProxy| { foo: Maybe Int } |]
      $ \(x :: Maybe Int) -> [aesonQQ| { "foo": #{x} } |]
  , CheckError
      "Maybe invalid"
      "fromjson_maybe_invalid.golden"
      [schemaProxy| { foo: Maybe Int } |]
      [aesonQQ| { "foo": true } |]
  , CheckValid
      "Try valid with valid parse"
      [schemaProxy| { foo: Try Bool } |]
      $ \(x :: Bool) -> [aesonQQ| { "foo": #{x} } |]
  , CheckValid
      "Try valid with invalid parse"
      [schemaProxy| { foo: Try Bool } |]
      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]
  , CheckValid
      "List valid"
      [schemaProxy| { foo: List Double } |]
      $ \(xs :: [Double]) -> [aesonQQ| { "foo": #{xs} } |]
  , CheckError
      "List invalid"
      "fromjson_list_invalid.golden"
      [schemaProxy| { foo: List Double } |]
      [aesonQQ| { "foo": true } |]
  , CheckError
      "List invalid inner"
      "fromjson_list_inner_invalid.golden"
      [schemaProxy| { foo: List Double } |]
      [aesonQQ| { "foo": [true] } |]
  , CheckError
      "Object invalid"
      "fromjson_object_invalid.golden"
      [schemaProxy| { foo: Int } |]
      [aesonQQ| 1 |]
  , CheckError
      "Object invalid in later keys"
      "fromjson_object_later_keys_invalid.golden"
      [schemaProxy| { foo: Int, bar: Int } |]
      [aesonQQ| { "foo": 1, "bar": true } |]
  , CheckValid
      "Nested object valid"
      [schemaProxy| { foo: { bar: Int } } |]
      $ \(x :: Int) -> [aesonQQ| { "foo": { "bar": #{x} } } |]
  , CheckError
      "Nested object invalid"
      "fromjson_nested_invalid.golden"
      [schemaProxy| { foo: { bar: Int } } |]
      [aesonQQ| { "foo": true } |]
  , CheckError
      "Nested object invalid inner"
      "fromjson_nested_inner_invalid.golden"
      [schemaProxy| { foo: { bar: Int } } |]
      [aesonQQ| { "foo": { "bar": true } } |]
  , CheckValid
      "Union object valid"
      [schemaProxy| { foo: Int | Text } |]
      $ \(x :: Int) -> [aesonQQ| { "foo": #{x} } |]
  , CheckError
      "Union object invalid"
      "fromjson_union_invalid.golden"
      [schemaProxy| { foo: Int | Text } |]
      [aesonQQ| { "foo": true } |]
  , CheckValid
      "Phantom key valid object"
      [schemaProxy| { [foo]: { bar: Int } } |]
      $ \(x :: Int) -> [aesonQQ| { "bar": #{x} } |]
  , CheckValid
      "Phantom key valid non-object try"
      [schemaProxy| { [foo]: Try Bool } |]
      $ \(b :: Bool) -> [aesonQQ| { "bar": #{b} } |]
  , CheckError
      "Phantom key invalid"
      "fromjson_phantom_invalid.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| 1 |]
  , CheckError
      "Phantom key missing inner"
      "fromjson_phantom_inner_missing.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| { "foo": true } |]
  , CheckError
      "Phantom key invalid inner"
      "fromjson_phantom_inner_invalid.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| { "bar": true } |]
  , CheckError
      "Decode failure messages are truncated"
      "fromjson_error_messages_truncate.golden"
      [schemaProxy| { foo: Int } |]
      [aesonQQ|
        {
          "foo": [
            { "bar": 1 },
            { "bar": 2 },
            { "bar": 3 },
            { "bar": 4 },
            { "bar": 5 },
            { "bar": 6 },
            { "bar": 7 },
            { "bar": 8 },
            { "bar": 9 }
          ]
        }
      |]
  ]

{- Helpers -}

data FromJSONTestCase where
  CheckValid ::
    (Arbitrary a, Show a, FromJSON (Object schema)) =>
    -- | Name of test case
    TestName ->
    -- | The schema to parse with
    Proxy (Object schema) ->
    -- | A function that builds a Value that should satisfy the schema
    (a -> Value) ->
    FromJSONTestCase
  CheckError ::
    (FromJSON (Object schema), Show (Object schema)) =>
    -- | Name of test case
    TestName ->
    -- | Name of golden file
    String ->
    -- | The schema to parse with
    Proxy (Object schema) ->
    -- | The value that should fail parsing the given schema
    Value ->
    FromJSONTestCase

runTestCase :: FromJSONTestCase -> TestTree
runTestCase = \case
  CheckValid name schema valueGen ->
    testProperty name $ \a ->
      case parse schema (valueGen a) of
        Right _ -> ()
        Left e -> error $ "Unexpected failure: " ++ e
  CheckError name fp schema value ->
    testGolden name fp $
      case parse schema value of
        Right o -> error $ "Unexpectedly parsed: " ++ show o
        Left e -> e

parse :: FromJSON a => Proxy a -> Value -> Either String a
parse _ = parseEither parseJSON

# LANGUAGE GADTs #
 Helpers 
 | Name of test case
 | The schema to parse with
 | A function that builds a Value that should satisfy the schema
 | Name of test case
 | Name of golden file
 | The schema to parse with
 | The value that should fail parsing the given schema
# LANGUAGE DataKinds #
# LANGUAGE FlexibleContexts #
# LANGUAGE GeneralizedNewtypeDeriving #
# LANGUAGE KindSignatures #
# LANGUAGE LambdaCase #
# LANGUAGE QuasiQuotes #
# LANGUAGE ScopedTypeVariables #
# LANGUAGE TemplateHaskell #

module Tests.Object.FromJSON where

import Data.Aeson (FromJSON (..), Value)
import Data.Aeson.QQ (aesonQQ)
import Data.Aeson.Types (parseEither)
import Data.Proxy (Proxy)
import Test.Tasty
import Test.Tasty.QuickCheck

import Data.Aeson.Schema (Object)
import TestUtils (parseProxy, testGolden)
import TestUtils.Arbitrary (ArbitraryObject (..), forAllArbitraryObjects)
import Tests.Object.FromJSON.TH

test :: TestTree
test =
  testGroup "FromJSON instance" $
    map runTestCase testCases
      ++ [ testProperty "QuickCheck arbitrary Schema" $
            $(forAllArbitraryObjects) $ \(ArbitraryObject proxy v _) ->
              case parseProxy proxy v of
                Right _ -> property ()
                Left e -> error $ "Could not parse: " ++ e
         ]

testCases :: [FromJSONTestCase]
testCases =
  [ CheckValid
      "Scalar valid"
      [schemaProxy| { foo: Text } |]
      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]
  , CheckError
      "Scalar invalid"
      "fromjson_scalar_invalid.golden"
      [schemaProxy| { foo: Text } |]
      [aesonQQ| { "foo": 1 } |]
  , CheckValid
      "Maybe valid"
      [schemaProxy| { foo: Maybe Int } |]
      $ \(x :: Maybe Int) -> [aesonQQ| { "foo": #{x} } |]
  , CheckError
      "Maybe invalid"
      "fromjson_maybe_invalid.golden"
      [schemaProxy| { foo: Maybe Int } |]
      [aesonQQ| { "foo": true } |]
  , CheckValid
      "Try valid with valid parse"
      [schemaProxy| { foo: Try Bool } |]
      $ \(x :: Bool) -> [aesonQQ| { "foo": #{x} } |]
  , CheckValid
      "Try valid with invalid parse"
      [schemaProxy| { foo: Try Bool } |]
      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]
  , CheckValid
      "List valid"
      [schemaProxy| { foo: List Double } |]
      $ \(xs :: [Double]) -> [aesonQQ| { "foo": #{xs} } |]
  , CheckError
      "List invalid"
      "fromjson_list_invalid.golden"
      [schemaProxy| { foo: List Double } |]
      [aesonQQ| { "foo": true } |]
  , CheckError
      "List invalid inner"
      "fromjson_list_inner_invalid.golden"
      [schemaProxy| { foo: List Double } |]
      [aesonQQ| { "foo": [true] } |]
  , CheckError
      "Object invalid"
      "fromjson_object_invalid.golden"
      [schemaProxy| { foo: Int } |]
      [aesonQQ| 1 |]
  , CheckError
      "Object invalid in later keys"
      "fromjson_object_later_keys_invalid.golden"
      [schemaProxy| { foo: Int, bar: Int } |]
      [aesonQQ| { "foo": 1, "bar": true } |]
  , CheckValid
      "Nested object valid"
      [schemaProxy| { foo: { bar: Int } } |]
      $ \(x :: Int) -> [aesonQQ| { "foo": { "bar": #{x} } } |]
  , CheckError
      "Nested object invalid"
      "fromjson_nested_invalid.golden"
      [schemaProxy| { foo: { bar: Int } } |]
      [aesonQQ| { "foo": true } |]
  , CheckError
      "Nested object invalid inner"
      "fromjson_nested_inner_invalid.golden"
      [schemaProxy| { foo: { bar: Int } } |]
      [aesonQQ| { "foo": { "bar": true } } |]
  , CheckValid
      "Union object valid"
      [schemaProxy| { foo: Int | Text } |]
      $ \(x :: Int) -> [aesonQQ| { "foo": #{x} } |]
  , CheckError
      "Union object invalid"
      "fromjson_union_invalid.golden"
      [schemaProxy| { foo: Int | Text } |]
      [aesonQQ| { "foo": true } |]
  , CheckValid
      "Phantom key valid object"
      [schemaProxy| { [foo]: { bar: Int } } |]
      $ \(x :: Int) -> [aesonQQ| { "bar": #{x} } |]
  , CheckValid
      "Phantom key valid non-object try"
      [schemaProxy| { [foo]: Try Bool } |]
      $ \(b :: Bool) -> [aesonQQ| { "bar": #{b} } |]
  , CheckError
      "Phantom key invalid"
      "fromjson_phantom_invalid.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| 1 |]
  , CheckError
      "Phantom key missing inner"
      "fromjson_phantom_inner_missing.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| { "foo": true } |]
  , CheckError
      "Phantom key invalid inner"
      "fromjson_phantom_inner_invalid.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| { "bar": true } |]
  , CheckError
      "Decode failure messages are truncated"
      "fromjson_error_messages_truncate.golden"
      [schemaProxy| { foo: Int } |]
      [aesonQQ|
        {
          "foo": [
            { "bar": 1 },
            { "bar": 2 },
            { "bar": 3 },
            { "bar": 4 },
            { "bar": 5 },
            { "bar": 6 },
            { "bar": 7 },
            { "bar": 8 },
            { "bar": 9 }
          ]
        }
      |]
  ]


data FromJSONTestCase where
  CheckValid ::
    (Arbitrary a, Show a, FromJSON (Object schema)) =>
    TestName ->
    Proxy (Object schema) ->
    (a -> Value) ->
    FromJSONTestCase
  CheckError ::
    (FromJSON (Object schema), Show (Object schema)) =>
    TestName ->
    String ->
    Proxy (Object schema) ->
    Value ->
    FromJSONTestCase

runTestCase :: FromJSONTestCase -> TestTree
runTestCase = \case
  CheckValid name schema valueGen ->
    testProperty name $ \a ->
      case parse schema (valueGen a) of
        Right _ -> ()
        Left e -> error $ "Unexpected failure: " ++ e
  CheckError name fp schema value ->
    testGolden name fp $
      case parse schema value of
        Right o -> error $ "Unexpectedly parsed: " ++ show o
        Left e -> e

parse :: FromJSON a => Proxy a -> Value -> Either String a
parse _ = parseEither parseJSON
