{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Utils.QuickCheck
  ( CreateTable'(..)
  , toOperation
  , ColumnIdentifier(..)
  , Identifier(..)
  , genPersistValue
  -- * Utilities
  , DistinctList(..)
  , mapSome
  , group
  ) where

import Control.Monad ((>=>))
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as ByteString
import Data.List (nub)
#if !MIN_VERSION_base(4,11,0)
import Data.Monoid ((<>))
#endif
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time.Calendar (Day, fromGregorian)
import Data.Time.Clock (UTCTime(..), secondsToDiffTime)
import Data.Time.LocalTime (TimeOfDay(..))
import Database.Persist.Migration
    (Column(..), ColumnProp(..), Operation(..), TableConstraint(..))
import Database.Persist.Sql (PersistValue(..), SqlType(..))
import Test.QuickCheck hiding (scale)

-- | A duplicate of the CreateTable constructor for testing.
data CreateTable' = CreateTable'
  { ctName        :: Text
  , ctSchema      :: [Column]
  , ctConstraints :: [TableConstraint]
  } deriving (Show)

toOperation :: CreateTable' -> Operation
toOperation CreateTable'{..} = CreateTable ctName ctSchema ctConstraints

instance Arbitrary CreateTable' where
  arbitrary = do
    name <- arbitrary
    let Identifier ctName = name

    -- get names of tables this table can have foreign keys towards
    DistinctList colNames <- arbitrary
    -- max out at 100 names
    let colNames' = take 100 $ map unColIdent colNames

    -- generate schema
    DistinctList tableNames <- arbitrary
    let tableNames' = filter (/= name) tableNames
    cols <- vectorOf (length colNames') $ genColumn tableNames'
    let idCol = Column "id" SqlInt32 [NotNull, AutoIncrement]
        cols' = zipWith (\colName' col -> col{colName = colName'}) colNames' cols
        ctSchema = idCol : cols'

    -- all of the columns that will be unique
    uniqueCols <- sublistOf $ map colName cols'
    let mkUnique names =
          -- constraint name can be max 63 characters
          let namespace = Text.take 10 ctName
              constraintName = Text.take 63 $ "unique_" <> Text.intercalate "_" (namespace:names)
          in Unique constraintName names
        -- unique constraints should not have more than 32 columns
        max32 l = if length l > 32
          then take 32 l : max32 (drop 32 l)
          else [l]
    uniqueConstraints <- map mkUnique . concatMap max32 <$> group uniqueCols

    let ctConstraints = PrimaryKey ["id"] : uniqueConstraints

    return $ CreateTable'{..}

-- | Generate an arbitrary Column with a possibly pre-determined name.
--
-- Also given the set of table names that can be referenced by foreign keys.
genColumn :: [Identifier] -> Gen Column
genColumn tableNames = do
  colName <- fmap unColIdent arbitrary

  references <- if null tableNames
    then return []
    else do
      Identifier table <- elements tableNames
      arbitrarySingleton 10 $ References (table, "id")

  colType <- if null references
    then arbitrary
    else return SqlInt32

  autoIncrement <- arbitrarySingleton 1 AutoIncrement
  notNull <- arbitrarySingleton 50 NotNull
  colDefault <- case autoIncrement of
    [] -> arbitrarySingleton 10 . Default =<< genPersistValue colType
    _ -> return []

  let colProps = notNull ++ autoIncrement ++ references ++ colDefault

  return Column{..}
  where
    -- get list with x% chance of having the given element and (100 - x)% chance of
    -- being an empty list
    arbitrarySingleton x v = frequency [(x, pure [v]), (100 - x, pure [])]

instance Arbitrary Column where
  arbitrary = listOf arbitrary >>= genColumn

newtype Identifier = Identifier { unIdent :: Text }
  deriving (Show,Eq)

instance Arbitrary Identifier where
  arbitrary = do
    first <- elements underletter
    rest <- listOf $ elements $ underletter ++ ['0'..'9']
    return . Identifier . Text.pack . take 63 $ first : rest
    where
      underletter = '_':['a'..'z']

newtype ColumnIdentifier = ColumnIdentifier { unColIdent :: Text }
  deriving (Show,Eq)

instance Arbitrary ColumnIdentifier where
  arbitrary = do
    Identifier ident <- arbitrary
    if ident `elem` invalidIdents
      then arbitrary
      else return $ ColumnIdentifier ident
    where
      invalidIdents =
        [ "id"
        -- https://www.postgresql.org/docs/9.6/static/ddl-system-columns.html
        , "oid", "tableoid", "xmin", "cmin", "xmax", "cmax", "ctid"
        ]

instance Arbitrary SqlType where
  arbitrary = do
    numPrecision <- choose (1, 1000)
    numScale <- choose (0, numPrecision)
    elements
      [ SqlString
      , SqlInt32
      , SqlInt64
      , SqlReal
      , SqlNumeric numPrecision numScale
      , SqlBool
      , SqlDay
      , SqlTime
      , SqlDayTime
      , SqlBlob
      ]

-- | Generate an arbitrary PersistValue for the given SqlType.
genPersistValue :: SqlType -> Gen PersistValue
genPersistValue = \case
  SqlString -> PersistText . Text.map cleanText <$> arbitrary
  SqlInt32 -> PersistInt64 <$> choose (-2147483648, 2147483647)
  SqlInt64 -> PersistInt64 <$> choose (-2147483648, 2147483647)
  SqlReal -> PersistDouble . cleanDouble  <$> arbitrary
  SqlNumeric precision scale -> do
    v <- choose (0, 1) :: Gen Double
    let v' = truncate (v * (10 ^ precision)) :: Integer
        x = fromIntegral v' / (10 ^ scale)
    return . PersistRational . toRational . cleanDouble $ x
  SqlBool -> PersistBool <$> arbitrary
  SqlDay -> PersistDay <$> arbitrary
  SqlTime -> PersistTimeOfDay <$> arbitrary
  SqlDayTime -> PersistUTCTime <$> arbitrary
  SqlBlob -> PersistByteString . ByteString.map cleanText <$> arbitrary
  SqlOther _ -> error "SqlOther not supported"
  where
    cleanDouble :: Double -> Double
    cleanDouble x = if isInfinite x || isNaN x then 0 else x
    -- https://github.com/lpsmith/postgresql-simple/issues/169
    cleanText :: Char -> Char
    cleanText '?' = '_'
    cleanText c = c

{- Utilities -}

newtype DistinctList a = DistinctList { unDistinctList :: [a] }
  deriving (Show)

instance (Arbitrary a, Eq a) => Arbitrary (DistinctList a) where
  arbitrary = DistinctList . nub <$> listOf arbitrary

instance Arbitrary Text where
  arbitrary = Text.pack . getASCIIString <$> arbitrary

instance Arbitrary ByteString where
  arbitrary = Text.encodeUtf8 <$> arbitrary

instance Arbitrary Day where
  arbitrary = fromGregorian <$> choose (1, 294276) <*> choose (1, 12) <*> choose (1, 31)

instance Arbitrary TimeOfDay where
  arbitrary = TimeOfDay <$> choose (0, 23) <*> choose (0, 59) <*> genSeconds
    where
      genSeconds = fromRational . toRational <$> (choose (0, 60) :: Gen Double)

instance Arbitrary UTCTime where
  arbitrary = UTCTime <$> arbitrary <*> (secondsToDiffTime <$> choose (0, 86400))

-- | Randomly modify at least one element in the list with the given function.
mapSome :: (a -> a) -> [a] -> Gen [a]
mapSome _ [] = return []
mapSome f l = do
  i <- choose (0, length l - 1)
  let (half1, half2) = splitAt i l
      modify = mapM $ \x -> do
        shouldModify <- arbitrary
        return $ if shouldModify then f x else x
  half1' <- modify half1
  half2' <- modify $ tail half2
  return $ half1' ++ [f $ head half2] ++ half2'

-- | Randomly group the given list.
group :: [a] -> Gen [[a]]
group = shuffle >=> partition []
  where
    partition res [] = return res
    partition res l = do
      i <- choose (1, length l)
      let (first, rest) = splitAt i l
      partition (first : res) rest
