module JSONSchema.Draft4.QuickCheckInstances where

import           Protolude

import qualified Data.Aeson                  as AE
import qualified Data.HashMap.Lazy           as HM
import qualified Data.Vector                 as V
import           JSONSchema.Draft4
import           JSONSchema.Validator.Utils

import           Data.Generics.Uniplate.Data
import           Test.QuickCheck
import           Test.QuickCheck.Instances   ()

-- N.B. it is in general not true that the empty schema acts as the identity
-- value with regards to unifySchema because the empty schema represents a
-- more relaxed value for required, which we
-- must respect. To get around this, we create a newtype RestrictedSchema
-- which has a different instance of Arbitrary which always returns Nothing for
-- required, thus avoiding this issue.
newtype RestrictedSchema = RestrictedSchema
  { getSchema :: Schema
  } deriving (Show)

instance Arbitrary RestrictedSchema where
  arbitrary = sized f
    where
      maybeGen :: Gen a -> Gen (Maybe a)
      maybeGen a = oneof [pure Nothing, Just <$> a]
      maybeRecurse :: Int -> Gen a -> Gen (Maybe a)
      maybeRecurse n a
        | n < 1 = pure Nothing
        | otherwise = maybeGen $ resize (n `div` 10) a
      f :: Int -> Gen RestrictedSchema
      f n = do
        a <- maybeGen arbitraryText
        b <- maybeGen arbitraryText
        c <- maybeGen arbitraryText
        d <- pure Nothing
        e <- pure mempty
        f' <- maybeGen arbitraryPositiveScientific
        g <- maybeGen arbitraryScientific
        h <- maybeGen arbitrary
        i <- maybeGen arbitraryScientific
        j <- maybeGen arbitrary
        k <- maybeGen (getPositive <$> arbitrary)
        l <- maybeGen (getPositive <$> arbitrary)
        m <- maybeGen arbitraryText
        n' <- maybeGen (getPositive <$> arbitrary)
        o <- maybeGen (getPositive <$> arbitrary)
        p <- arbitrary
        q <- maybeRecurse n arbitrary
        r <- maybeRecurse n arbitrary
        s <- maybeGen (getPositive <$> arbitrary)
        t <- maybeGen (getPositive <$> arbitrary)
        u <- pure Nothing
        v <- maybeRecurse n arbitraryHashMap
        w <- maybeRecurse n arbitraryHashMap
        x <- maybeRecurse n arbitraryHashMap
        y <- maybeRecurse n arbitrary
        z <- maybeRecurse n (fmap _unArbitraryValue . _unNonEmpty' <$> arbitrary)
        a2 <- arbitrary
        b2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
        c2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
        d2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
        e2 <- maybeRecurse n arbitrary
        pure $
          RestrictedSchema
            Schema
            { _schemaVersion = a
            , _schemaId = b
            , _schemaRef = c
            , _schemaDefinitions = d
            , _schemaOther = e
            , _schemaMultipleOf = f'
            , _schemaMaximum = g
            , _schemaExclusiveMaximum = h
            , _schemaMinimum = i
            , _schemaExclusiveMinimum = j
            , _schemaMaxLength = k
            , _schemaMinLength = l
            , _schemaPattern = m
            , _schemaMaxItems = n'
            , _schemaMinItems = o
            , _schemaUniqueItems = p
            , _schemaItems = q
            , _schemaAdditionalItems = r
            , _schemaMaxProperties = s
            , _schemaMinProperties = t
            , _schemaRequired = u
            , _schemaDependencies = v
            , _schemaProperties = w
            , _schemaPatternProperties = x
            , _schemaAdditionalProperties = y
            , _schemaEnum = z
            , _schemaType = a2
            , _schemaAllOf = b2
            , _schemaAnyOf = c2
            , _schemaOneOf = d2
            , _schemaNot = e2
            }

-- For properties that cannot be easily unified we simply choose
-- one (e.g. "version": "1.0" and "version": "2.0"). By excluding
-- these properties, unified with `const`, we can test if a
-- unifySchema is commutative otherwise
newtype CommutativeSchema = CommutativeSchema
  { getCommutativeSchema :: Schema
  } deriving (Show)

instance Arbitrary CommutativeSchema where
  arbitrary = sized f
    where
      maybeGen :: Gen a -> Gen (Maybe a)
      maybeGen a = oneof [pure Nothing, Just <$> a]
      maybeRecurse :: Int -> Gen a -> Gen (Maybe a)
      maybeRecurse n a
        | n < 1 = pure Nothing
        | otherwise = maybeGen $ resize (n `div` 10) a
      f :: Int -> Gen CommutativeSchema
      f n = do
        a <- pure Nothing
        b <- pure Nothing
        c <- pure Nothing
        d <- pure Nothing
        e <- pure mempty
        f' <- pure Nothing
        g <- maybeGen arbitraryScientific
        h <- maybeGen arbitrary
        i <- maybeGen arbitraryScientific
        j <- maybeGen arbitrary
        k <- maybeGen (getPositive <$> arbitrary)
        l <- maybeGen (getPositive <$> arbitrary)
        m <- pure Nothing
        n' <- maybeGen (getPositive <$> arbitrary)
        o <- maybeGen (getPositive <$> arbitrary)
        p <- arbitrary
        q <- maybeRecurse n arbitrary
        r <- maybeRecurse n arbitrary
        s <- maybeGen (getPositive <$> arbitrary)
        t <- maybeGen (getPositive <$> arbitrary)
        u <- pure Nothing
        v <- pure Nothing
        w <- maybeRecurse n arbitraryHashMap
        x <- maybeRecurse n arbitraryHashMap
        y <- maybeRecurse n arbitrary
        z <- pure Nothing
        a2 <- arbitrary
        b2 <- pure Nothing
        c2 <- pure Nothing
        d2 <- pure Nothing
        e2 <- pure Nothing
        pure $
          CommutativeSchema
            Schema
            { _schemaVersion = a
            , _schemaId = b
            , _schemaRef = c
            , _schemaDefinitions = d
            , _schemaOther = e
            , _schemaMultipleOf = f'
            , _schemaMaximum = g
            , _schemaExclusiveMaximum = h
            , _schemaMinimum = i
            , _schemaExclusiveMinimum = j
            , _schemaMaxLength = k
            , _schemaMinLength = l
            , _schemaPattern = m
            , _schemaMaxItems = n'
            , _schemaMinItems = o
            , _schemaUniqueItems = p
            , _schemaItems = q
            , _schemaAdditionalItems = r
            , _schemaMaxProperties = s
            , _schemaMinProperties = t
            , _schemaRequired = u
            , _schemaDependencies = v
            , _schemaProperties = w
            , _schemaPatternProperties = x
            , _schemaAdditionalProperties = y
            , _schemaEnum = z
            , _schemaType = a2
            , _schemaAllOf = b2
            , _schemaAnyOf = c2
            , _schemaOneOf = d2
            , _schemaNot = e2
            }

-- Arbitrary instance for Value from Reddit.
instance Arbitrary AE.Value where
  arbitrary = sized sizedArbitraryValue
  -- JSON shrinker borrowed from json-autotype
  shrink = valueShrink

sizedArbitraryValue :: Int -> Gen AE.Value
sizedArbitraryValue n
  | n <= 0 = oneof [pure AE.Null, AE.Bool <$> arbitrary, AE.Number <$> arbitrary, AE.String <$> arbitrary]
  | otherwise =
    resize (div n 2) $
    oneof
      [ pure AE.Null
      , AE.Bool <$> arbitrary
      , AE.Number <$> arbitrary
      , AE.String <$> arbitrary
      , AE.Array <$> arbitrary
      , AE.Object <$> arbitrary
      ]

simpleShrink :: AE.Value -> [AE.Value]
simpleShrink (AE.Array a) = map (AE.Array . V.fromList) $ shrink $ V.toList a
simpleShrink (AE.Object o) = map (AE.Object . HM.fromList) $ shrink $ HM.toList o
simpleShrink _ = [] -- Nothing for simple objects

valueShrink :: AE.Value -> [AE.Value]
valueShrink = concatMap simpleShrink . universe

sizedJsonProp :: Int -> (AE.Value -> Property) -> Property
sizedJsonProp size = forAllShrink jsonGen valueShrink
  where
    jsonGen :: Gen AE.Value
    jsonGen = resize size arbitrary

sizedJsonsProp :: Int -> ([AE.Value] -> Property) -> Property
sizedJsonsProp size = forAllShrink jsonGen $ shrinkList valueShrink
  where
    jsonGen :: Gen [AE.Value]
    jsonGen = resize size arbitrary
