{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Property (testProperties) where

import Control.Applicative (liftA2)
import Data.Either (isRight)
import Database.Persist.Migration
import Test.QuickCheck
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (testProperty)

import Utils.QuickCheck (CreateTable'(..), Identifier(..), mapSome, toOperation)

-- | Run tests related to migration validaton.
testProperties :: TestTree
testProperties = testGroup "properties"
  [ testProperty "Valid migration" $ forAll arbitrary isValidOperationPaths
  , testProperty "Invalid decreasing migration" $
      forAll arbitrary $ \(OperationPaths opPaths) -> do
        opPaths' <- mapSome (\(a, b) -> (b, a)) opPaths
        return . not . isValidOperationPaths $ OperationPaths opPaths'
  , testProperty "Invalid duplicate operation path" $
      forAll arbitrary $ \(OperationPaths opPaths) -> do
        opPaths' <- mapSomeDupl opPaths
        return . not . isValidOperationPaths $ OperationPaths opPaths'
  , testProperty "Valid CreateTable" $
      forAll arbitrary $ isValidOperation . toOperation
  , testProperty "Duplicate ColumnProps in CreateTable" $ do
      colsMaybeProps <- listOf arbitrary
      colsWithProps <- listOf1 (arbitrary `suchThat` (not . null . colProps))
      let duplProps = mapM $ \col@Column{colProps} -> do
            colProps' <- mapSomeDupl colProps
            return col{colProps = colProps'}
      cols <- liftA2 (++) (duplProps colsMaybeProps) (duplProps colsWithProps)
      let ct = CreateTable "foo" cols []
      return . not . isValidOperation $ ct
  , testProperty "Duplicate Constraints in CreateTable" $
      forAll arbitrary $ \ct@CreateTable'{..} -> do
        constraints <- mapSomeDupl ctConstraints
        return . not . isValidOperation . toOperation $ ct{ctConstraints = constraints}
  , testProperty "Constraint references non-existent column" $
      forAll arbitrary $ \ct@CreateTable'{..} -> do
        let existing = map (Identifier . colName) ctSchema
            genConstraint = do
              Identifier name' <- arbitrary
              cols <- map unIdent <$> listOf1 (arbitrary `suchThat` (`notElem` existing))
              elements [PrimaryKey cols, Unique name' cols]
        newConstraints <- listOf1 genConstraint
        return . not . isValidOperation . toOperation $
          ct{ctConstraints = ctConstraints ++ newConstraints}
  , testProperty "Duplicate ColumnProps in AddColumn" $
      forAll arbitrary $ \col@Column{colProps} -> do
        Identifier table <- arbitrary
        colProps' <- mapSomeDupl colProps
        let column = col{colProps = colProps'}
            colDefault = Nothing
        return $ not (null colProps) ==> not (isValidOperation AddColumn{..})
  , testProperty "Non-null AddColumn without default" $
      forAll arbitrary $ \col@Column{colProps} -> do
        Identifier table <- arbitrary
        let colProps' = if NotNull `elem` colProps then colProps else NotNull : colProps
            column = col{colProps = colProps'}
            colDefault = Nothing
        return . not . isValidOperation $ AddColumn{..}
  ]

newtype OperationPaths = OperationPaths { getOpPaths :: [OperationPath] }
  deriving (Show)

instance Arbitrary OperationPaths where
  arbitrary = do
    Positive maxVersion <- arbitrary
    let versions = [0..maxVersion]
        opPaths = zip versions $ tail versions
    -- order should not matter
    OperationPaths <$> shuffle opPaths

-- | Validate an Operation.
isValidOperation :: Operation -> Bool
isValidOperation = isRight . validateOperation

-- | Validate OperationPaths in a Migration.
isValidOperationPaths :: OperationPaths -> Bool
isValidOperationPaths = isRight . validateMigration . map mkOperation . getOpPaths
  where
    mkOperation path = path := []

-- | Duplicate some elements in the given list.
mapSomeDupl :: [a] -> Gen [a]
mapSomeDupl = fmap concat . mapSome dupl . map pure
  where
    dupl [x] = [x, x]
    dupl _ = error "unreachable"
