{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module Main (main) where

import Control.Monad (forM_, replicateM)
import Control.Monad.ST (runST)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Short as SBS
import Data.Int
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Typeable
import Data.Void
import Data.Word
import Foreign.C.Types
import GHC.Exts (fromList)
import GHC.Generics
import Numeric.Natural (Natural)
import System.Random (uniformShortByteString)
import System.Random.Internal (freezeMutableByteArray, newMutableByteArray, writeWord8)
import System.Random.Stateful hiding (uniformShortByteString)
import Test.SmallCheck.Series as SC
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.SmallCheck as SC
#if __GLASGOW_HASKELL__ < 804
import Data.Monoid ((<>))
#endif

import qualified Spec.Range as Range
import qualified Spec.Run as Run
import qualified Spec.Seed as Seed
import qualified Spec.Stateful as Stateful

main :: IO ()
main =
  defaultMain
    $ testGroup
      "Spec"
    $ [ floatingSpec (Proxy :: Proxy Double)
      , floatingSpec (Proxy :: Proxy Float)
      , floatingSpec (Proxy :: Proxy CDouble)
      , floatingSpec (Proxy :: Proxy CFloat)
      , integralSpec (Proxy :: Proxy Word8)
      , integralSpec (Proxy :: Proxy Word16)
      , integralSpec (Proxy :: Proxy Word32)
      , integralSpec (Proxy :: Proxy Word64)
      , integralSpec (Proxy :: Proxy Word)
      , integralSpec (Proxy :: Proxy Int8)
      , integralSpec (Proxy :: Proxy Int16)
      , integralSpec (Proxy :: Proxy Int32)
      , integralSpec (Proxy :: Proxy Int64)
      , integralSpec (Proxy :: Proxy Int)
      , integralSpec (Proxy :: Proxy Char)
      , integralSpec (Proxy :: Proxy Bool)
      , integralSpec (Proxy :: Proxy CChar)
      , integralSpec (Proxy :: Proxy CSChar)
      , integralSpec (Proxy :: Proxy CUChar)
      , integralSpec (Proxy :: Proxy CShort)
      , integralSpec (Proxy :: Proxy CUShort)
      , integralSpec (Proxy :: Proxy CInt)
      , integralSpec (Proxy :: Proxy CUInt)
      , integralSpec (Proxy :: Proxy CLong)
      , integralSpec (Proxy :: Proxy CULong)
      , integralSpec (Proxy :: Proxy CPtrdiff)
      , integralSpec (Proxy :: Proxy CSize)
      , integralSpec (Proxy :: Proxy CWchar)
      , integralSpec (Proxy :: Proxy CSigAtomic)
      , integralSpec (Proxy :: Proxy CLLong)
      , integralSpec (Proxy :: Proxy CULLong)
      , integralSpec (Proxy :: Proxy CIntPtr)
      , integralSpec (Proxy :: Proxy CUIntPtr)
      , integralSpec (Proxy :: Proxy CIntMax)
      , integralSpec (Proxy :: Proxy CUIntMax)
      , integralSpec (Proxy :: Proxy Integer)
      , integralSpec (Proxy :: Proxy Natural)
      , enumSpec (Proxy :: Proxy Colors)
      , enumSpec (Proxy :: Proxy (Int, Int))
      , enumSpec (Proxy :: Proxy (Bool, Bool, Bool))
      , enumSpec (Proxy :: Proxy ((), Int, Bool, Word))
      , runSpec
      , floatTests
      , byteStringSpec
      , fillMutableByteArraySpec
      , SC.testProperty "uniformRangeWithinExcludedF" $ seeded Range.uniformRangeWithinExcludedF
      , SC.testProperty "uniformRangeWithinExcludedD" $ seeded Range.uniformRangeWithinExcludedD
      , randomSpec (Proxy :: Proxy (CFloat, CDouble))
      , randomSpec (Proxy :: Proxy (Int8, Int16, Int32))
      , randomSpec (Proxy :: Proxy (Int8, Int16, Int32, Int64))
      , randomSpec (Proxy :: Proxy (Word8, Word16, Word32, Word64, Word))
      , randomSpec (Proxy :: Proxy (Int8, Word8, Word16, Word32, Word64, Word))
      , randomSpec (Proxy :: Proxy (Int8, Int16, Word8, Word16, Word32, Word64, Word))
      , uniformSpec (Proxy :: Proxy (Int, Bool))
      , uniformSpec (Proxy :: Proxy (Int8, Int16, Int32))
      , uniformSpec (Proxy :: Proxy (Int8, Int16, Int32, Int64))
      , uniformSpec (Proxy :: Proxy (Word8, Word16, Word32, Word64, Word))
      , uniformSpec (Proxy :: Proxy (Int8, Word8, Word16, Word32, Word64, Word))
      , uniformSpec (Proxy :: Proxy (Int8, Int16, Word8, Word16, Word32, Word64, Word))
      , Stateful.statefulGenSpec
      , Seed.spec
      ]
      ++ ghc_8_2_spec
  where
#if __GLASGOW_HASKELL__ >= 802
    ghc_8_2_spec = [integralSpec (Proxy :: Proxy CBool)]
#else
    ghc_8_2_spec = []
#endif

floatTests :: TestTree
floatTests =
  testGroup
    "(Float)"
    [ -- Check that https://github.com/haskell/random/issues/53 does not regress

      testCase "Subnormal generation not above upper bound" $
        [] @?= filter (> 4.0e-45) (take 100000 $ randomRs (0, 4.0e-45 :: Float) $ mkStdGen 0)
    , testCase "Subnormal generation includes upper bound" $
        1.0e-45 `elem` take 100 (randomRs (0, 1.0e-45 :: Float) $ mkStdGen 0)
          @? "Does not contain 1.0e-45"
    ]

showType :: forall t. Typeable t => Proxy t -> String
showType px = show (typeRep px)

byteStringSpec :: TestTree
byteStringSpec =
  testGroup
    "ByteString"
    [ SC.testProperty "uniformShortByteString" $
        seededWithLen $
          \n g -> SBS.length (fst (uniformShortByteString n g)) == n
    , SC.testProperty "uniformByteString" $
        seededWithLen $ \n g ->
          SBS.toShort (fst (uniformByteString n g)) == fst (uniformShortByteString n g)
    , testCase "uniformByteString/ShortByteString consistency" $ do
        let g = mkStdGen 2021
            bs = [78, 232, 117, 189, 13, 237, 63, 84, 228, 82, 19, 36, 191, 5, 128, 192] :: [Word8]
        forM_ [0 .. length bs - 1] $ \n -> do
          xs <- SBS.unpack <$> runStateGenT_ g (uniformShortByteStringM n)
          xs @?= take n bs
          ys <- BS.unpack <$> runStateGenT_ g (uniformByteStringM n)
          ys @?= xs
    ]

fillMutableByteArraySpec :: TestTree
fillMutableByteArraySpec =
  testGroup
    "MutableByteArray"
    [ SC.testProperty "Same as uniformByteArray" $
        forAll $ \isPinned -> seededWithLen $ \n g ->
          let baFilled = runST $ do
                mba <- newMutableByteArray n
                g' <- uniformFillMutableByteArray mba 0 n g
                ba <- freezeMutableByteArray mba
                pure (ba, g')
           in baFilled == uniformByteArray isPinned n g
    , SC.testProperty "Safe uniformFillMutableByteArray" $
        forAll $ \isPinned offset count -> seededWithLen $ \sz g ->
          let (baFilled, gf) = runST $ do
                mba <- newMutableByteArray sz
                forM_ [0 .. sz - 1] (\i -> writeWord8 mba i 0)
                g' <- uniformFillMutableByteArray mba offset count g
                ba <- freezeMutableByteArray mba
                pure (ba, g')
              (baGen, gu) = uniformByteArray isPinned count' g
              offset' = min sz (max 0 offset)
              count' = min (sz - offset') (max 0 count)
              prefix = replicate offset' 0
              suffix = replicate (sz - (count' + offset')) 0
           in gf == gu && baFilled == fromList prefix <> baGen <> fromList suffix
    ]

rangeSpec ::
  forall a.
  (SC.Serial IO a, Typeable a, Ord a, UniformRange a, Show a) =>
  Proxy a -> TestTree
rangeSpec px =
  testGroup
    ("Range " ++ showType px)
    [ SC.testProperty "uniformR" $ seeded $ Range.uniformRangeWithin px
    ]

integralSpec ::
  forall a.
  (SC.Serial IO a, Typeable a, Ord a, UniformRange a, Show a) =>
  Proxy a -> TestTree
integralSpec px =
  testGroup
    (showType px)
    [ SC.testProperty "symmetric" $ seeded $ Range.symmetric px
    , SC.testProperty "bounded" $ seeded $ Range.bounded px
    , SC.testProperty "singleton" $ seeded $ Range.singleton px
    , rangeSpec px
    -- TODO: Add more tests
    ]

enumSpec ::
  forall a.
  (SC.Serial IO a, Typeable a, Ord a, UniformRange a, Show a) =>
  Proxy a -> TestTree
enumSpec = integralSpec

floatingSpec ::
  forall a.
  (SC.Serial IO a, Typeable a, Num a, Ord a, Random a, UniformRange a, Read a, Show a) =>
  Proxy a -> TestTree
floatingSpec px =
  testGroup
    (showType px)
    [ SC.testProperty "uniformR" $ seeded $ Range.uniformRangeWithin px
    , testCase "r = +inf, x = 0" $ positiveInf @?= fst (uniformR (0, positiveInf) (ConstGen 0))
    , testCase "r = +inf, x = 1" $ positiveInf @?= fst (uniformR (0, positiveInf) (ConstGen 1))
    , testCase "l = -inf, x = 0" $ negativeInf @?= fst (uniformR (negativeInf, 0) (ConstGen 0))
    , testCase "l = -inf, x = 1" $ negativeInf @?= fst (uniformR (negativeInf, 0) (ConstGen 1))
    -- TODO: Add more tests
    ]
  where
    positiveInf, negativeInf :: a
    positiveInf = read "Infinity"
    negativeInf = read "-Infinity"

randomSpec ::
  forall a.
  (Typeable a, Eq a, Random a, Show a) =>
  Proxy a -> TestTree
randomSpec px =
  testGroup
    ("Random " ++ showType px)
    [ SC.testProperty "randoms" $
        seededWithLen $ \len g ->
          take len (randoms g :: [a]) == runStateGen_ g (replicateM len . randomM)
    , SC.testProperty "randomRs" $
        seededWithLen $ \len g ->
          case random g of
            (range, g') ->
              take len (randomRs range g' :: [a])
                == runStateGen_ g' (replicateM len . randomRM range)
    ]

uniformSpec ::
  forall a.
  (Typeable a, Eq a, Random a, Uniform a, UniformRange a, Show a) =>
  Proxy a -> TestTree
uniformSpec px =
  testGroup
    ("Uniform " ++ showType px)
    [ SC.testProperty "uniformList" $
        seededWithLen $ \len g ->
          take len (randoms g :: [a]) == fst (uniformList len g)
    , SC.testProperty "uniformListR" $
        seededWithLen $ \len g ->
          case uniform g of
            (range, g') ->
              take len (randomRs range g' :: [a]) == fst (uniformListR len range g')
    , SC.testProperty "uniformShuffleList" $
        seededWithLen $ \len g ->
          case uniformList len g of
            (xs, g') ->
              let xs' = zip [0 :: Int ..] (xs :: [a])
               in sortOn fst (fst (uniformShuffleList xs' g')) == xs'
    , SC.testProperty "uniforms" $
        seededWithLen $ \len g ->
          take len (randoms g :: [a]) == take len (uniforms g)
    , SC.testProperty "uniformRs" $
        seededWithLen $ \len g ->
          case uniform g of
            (range, g') ->
              take len (randomRs range g' :: [a]) == take len (uniformRs range g')
    ]

runSpec :: TestTree
runSpec =
  testGroup
    "runStateGen_ and runPrimGenIO_"
    [SC.testProperty "equal outputs" $ seeded $ \g -> monadic $ Run.runsEqual g]

-- | Create a StdGen instance from an Int and pass it to the given function.
seeded :: (StdGen -> a) -> Int -> a
seeded f = f . mkStdGen

-- | Same as `seeded`, but also produces a length in range 0-65535 suitable for generating
-- lists and such
seededWithLen :: (Int -> StdGen -> a) -> Word16 -> Int -> a
seededWithLen f w16 = seeded (f (fromIntegral w16))

data MyBool = MyTrue | MyFalse
  deriving (Eq, Ord, Show, Generic, Finite, Uniform)

instance Monad m => Serial m MyBool

data MyAction = Code (Maybe MyBool) | Never Void | Eat (Bool, Bool) | Sleep ()
  deriving (Eq, Ord, Show, Generic, Finite)

instance Monad m => Serial m MyAction

instance Uniform MyAction

data Foo
  = Quux Char
  | Bar Int
  | Baz Word
  | Bar8 Int8
  | Baz8 Word8
  | Bar16 Int16
  | Baz16 Word16
  | Bar32 Int32
  | Baz32 Word32
  | Bar64 Int64
  | Baz64 Word64
  | Final ()
  deriving (Eq, Ord, Show, Generic, Finite, Uniform)

instance Monad m => Serial m Foo

newtype ConstGen = ConstGen Word64

instance SeedGen ConstGen where
  type SeedSize ConstGen = 8
  fromSeed64 (w :| _) = ConstGen w
  toSeed64 (ConstGen w) = pure w

instance RandomGen ConstGen where
  genWord64 g@(ConstGen c) = (c, g)

instance SplitGen ConstGen where
  splitGen g = (g, g)

data Colors = Red | Green | Blue | Purple | Yellow | Black | White | Orange
  deriving (Eq, Ord, Show, Generic, Enum, Bounded)

instance Monad m => Serial m Colors

instance Uniform Colors where
  uniformM = uniformEnumM

instance UniformRange Colors where
  uniformRM = uniformEnumRM
  isInRange (lo, hi) x = isInRange (fromEnum lo, fromEnum hi) (fromEnum x)
