{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Test.Context
  ( contextTests
  , genSomePayloadList
  , mkUAsgn
  , mkSAsgn
  )
where

import           Control.Lens
import           Data.Functor.Product (Product(Pair))
import           Data.Kind
import           Data.Parameterized.Classes
import qualified Data.Parameterized.Context as C
import qualified Data.Parameterized.Context.Safe as S
import qualified Data.Parameterized.Context.Unsafe as U
import           Data.Parameterized.Ctx
import qualified Data.Parameterized.Ctx.Proofs as P
import           Data.Parameterized.Some
import           Data.Parameterized.TraversableFC
import           Data.Parameterized.TraversableFC.WithIndex
import           Hedgehog
import qualified Hedgehog.Gen as HG
import           Hedgehog.Range
import           Test.Tasty
import           Test.Tasty.HUnit ( (@=?), (@?=), testCaseSteps )
import           Test.Tasty.Hedgehog

----------------------------------------------------------------------
-- Create a Payload GADT which is the parameterized type used for many
-- of the Context/Assignment tests in this module.

data Payload (ty :: Type) where
  IntPayload    :: Int -> Payload Int
  StringPayload :: String -> Payload String
  BoolPayload   :: Bool -> Payload Bool

deriving instance Eq (Payload ty)

instance TestEquality Payload where
  testEquality (IntPayload x) (IntPayload y) = if x == y then Just Refl else Nothing
  testEquality (StringPayload x) (StringPayload y) = if x == y then Just Refl else Nothing
  testEquality (BoolPayload x) (BoolPayload y) = if x == y then Just Refl else Nothing
  testEquality _ _ = Nothing

instance Show (Payload tp) where
  show (IntPayload x) = show x <> " :: Int"
  show (StringPayload x) = show x <> " :: String"
  show (BoolPayload x) = show x <> " :: Bool"

instance ShowF Payload

twiddle :: Payload a -> Payload a
twiddle (IntPayload n) = IntPayload (n+1)
twiddle (StringPayload str) = StringPayload (str++"asdf")
twiddle (BoolPayload b) = BoolPayload (not b)

twaddle :: Payload a -> Payload a
twaddle (IntPayload n) = IntPayload (n-1)
twaddle (StringPayload str) = StringPayload (reverse str)
twaddle (BoolPayload b) = BoolPayload (not b)

newtype Fun = Fun (forall a. Payload a -> Payload a)

instance Show Fun where
  show _ = "unshowable"

-- | Functions for e.g. testing functor laws
funs :: [Fun]
funs = [Fun twiddle, Fun twaddle, Fun id]

----------------------------------------------------------------------
-- Create another parameterized type for testing.  This one is not a
-- GADT, which will require some interesting implementation tricks.
--
-- The common 'Maybe' type is potentially useable for this type, but
-- there are some restrictions on 'Maybe'.  For example, it is not
-- possible to create a @ShowF Maybe@ because although 'Maybe' is of type
-- @(k -> type)@, @k@ is unconstrained and doesn't contain a 'Show'
-- constraint.

data MyMaybe t = (Show t) => MyJust t | MyNothing
instance ShowF MyMaybe
instance Show (MyMaybe t) where
  show (MyJust x) = "MyJust " <> show x
  show MyNothing = "MyNothing"

----------------------------------------------------------------------
-- Some Hedgehog generators

genSomePayload :: Monad m => GenT m (Some Payload)
genSomePayload =
  HG.choice
  [ Some . IntPayload    <$> HG.integral (linearBounded :: Range Int)
  , Some . StringPayload <$> HG.string (linear 1 32) HG.ascii
  , Some . BoolPayload   <$> HG.element [ True, False ]
  ]

-- generate a non-empty list of payload entries
genSomePayloadList :: Monad m => GenT m [Some Payload]
genSomePayloadList = HG.list (linear 1 10) genSomePayload


type UAsgn = U.Assignment Payload
type SAsgn = S.Assignment Payload

mkUAsgn :: [Some Payload] -> Some UAsgn
mkUAsgn = go U.empty
 where go :: UAsgn ctx -> [Some Payload] -> Some UAsgn
       go a [] = Some a
       go a (Some x : xs) = go (U.extend a x) xs

mkSAsgn :: [Some Payload] -> Some SAsgn
mkSAsgn = go S.empty
 where go :: SAsgn ctx -> [Some Payload] -> Some SAsgn
       go a [] = Some a
       go a (Some x : xs) = go (S.extend a x) xs

----------------------------------------------------------------------
-- A Ctx type that will be used for some of the Assignments tested here

type TestCtx = U.EmptyCtx '::> Int '::> String '::> Int '::> Bool


----------------------------------------------------------------------

contextTests :: IO TestTree
contextTests = testGroup "Context" <$> return
   [ testProperty "size (unsafe)" $ property $
     do vals <- forAll genSomePayloadList
        Some a <- return $ mkUAsgn vals
        length vals === U.sizeInt (U.size a)
   , testProperty "size (safe)" $ property $
     do vals <- forAll genSomePayloadList
        Some a <- return $ mkSAsgn vals
        length vals === S.sizeInt (S.size a)

   , testProperty "safe_index_eq" $ property $
     do vals <- forAll genSomePayloadList
        i' <- forAll $ HG.int (linear 0 $ length vals - 1)
        Some a <- return $ mkSAsgn vals
        Just (Some idx) <- return $ S.intIndex i' (S.size a)
        Some (a S.! idx) === vals !! i'

   , testProperty "unsafe_index_eq" $ property $
     do vals <- forAll genSomePayloadList
        i' <- forAll $ HG.int (linear 0 $ length vals - 1)
        Some a <- return $ mkUAsgn vals
        Just (Some idx) <- return $ U.intIndex i' (U.size a)
        Some (a U.! idx) === vals !! i'

   , testProperty "safe_tolist" $ property $
     do vals <- forAll genSomePayloadList
        Some a <- return $ mkSAsgn vals
        let vals' = toListFC Some a
        vals === vals'
   , testProperty "unsafe_tolist" $ property $
     do vals <- forAll genSomePayloadList
        Some a <- return $ mkUAsgn vals
        let vals' = toListFC Some a
        vals === vals'

   , testProperty "adjust test monadic" $ property $
     do vals <- forAll genSomePayloadList
        i' <- forAll $ HG.int (linear 0 $ length vals - 1)

        Some x <- return $ mkUAsgn vals
        Some y <- return $ mkSAsgn vals

        Just (Some idx_x) <- return $ U.intIndex i' (U.size x)
        Just (Some idx_y) <- return $ S.intIndex i' (S.size y)

        x' <- U.adjustM (return . twiddle) idx_x x
        y' <- S.adjustM (return . twiddle) idx_y y

        toListFC Some x' === toListFC Some y'

   , testProperty "adjust test" $ property $
     do vals <- forAll genSomePayloadList
        i' <- forAll $ HG.int (linear 0 $ length vals - 1)

        Some x <- return $ mkUAsgn vals
        Some y <- return $ mkSAsgn vals

        Just (Some idx_x) <- return $ U.intIndex i' (U.size x)
        Just (Some idx_y) <- return $ S.intIndex i' (S.size y)

        let x' = x & ixF idx_x %~ twiddle
            y' = y & ixF idx_y %~ twiddle

        toListFC Some x' === toListFC Some y'
        -- adjust actually modified the entry
        toListFC Some x /== toListFC Some x'
        toListFC Some y /== toListFC Some y'

   , testProperty "update test" $ property $
     do vals <- forAll genSomePayloadList
        i' <- forAll $ HG.int (linear 0 $ length vals - 1)

        Some x <- return $ mkUAsgn vals
        Some y <- return $ mkSAsgn vals

        Just (Some idx_x) <- return $ U.intIndex i' (U.size x)
        Just (Some idx_y) <- return $ S.intIndex i' (S.size y)

        let x' = over (ixF idx_x) twiddle x
            y' = (ixF idx_y) %~ twiddle $ y
            updX = x & ixF idx_x .~ x' U.! idx_x
            updY = y & ixF idx_y .~ y' S.! idx_y

        toListFC Some updX === toListFC Some updY
        -- update actually modified the entry
        toListFC Some x /== toListFC Some updX
        toListFC Some y /== toListFC Some updY
        -- update modified the expected entry
        toListFC Some x' === toListFC Some updX
        toListFC Some y' === toListFC Some updY

   , testProperty "safe_eq" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        Some x <- return $ mkSAsgn vals1
        Some y <- return $ mkSAsgn vals2
        case testEquality x y of
          Just Refl -> vals1 === vals2
          Nothing   -> vals1 /== vals2
   , testProperty "unsafe_eq" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        Some x <- return $ mkUAsgn vals1
        Some y <- return $ mkUAsgn vals2
        case testEquality x y of
          Just Refl -> vals1 === vals2
          Nothing   -> vals1 /== vals2

   , testProperty "take none" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        vals3 <- forAll genSomePayloadList
        Some w <- return $ mkUAsgn vals1
        Some x <- return $ mkUAsgn vals2
        Some y <- return $ mkUAsgn vals3
        let z = w U.<++> x U.<++> y
        case P.leftId z of
          Refl -> let r = C.take U.zeroSize (U.size z) z in
                    assert $ isJust $ testEquality U.empty r
   , testProperty "drop none" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        vals3 <- forAll genSomePayloadList
        Some w <- return $ mkUAsgn vals1
        Some x <- return $ mkUAsgn vals2
        Some y <- return $ mkUAsgn vals3
        let z = w U.<++> x U.<++> y
        case P.leftId z of
          Refl -> let r = C.drop U.zeroSize (U.size z) z in
                    assert $ isJust $ testEquality z r

   , testProperty "take all" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        vals3 <- forAll genSomePayloadList
        Some w <- return $ mkUAsgn vals1
        Some x <- return $ mkUAsgn vals2
        Some y <- return $ mkUAsgn vals3
        let z = w U.<++> x U.<++> y
        let r = C.take (U.size z) U.zeroSize z
        assert $ isJust $ testEquality z r
   , testProperty "drop all" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        vals3 <- forAll genSomePayloadList
        Some w <- return $ mkUAsgn vals1
        Some x <- return $ mkUAsgn vals2
        Some y <- return $ mkUAsgn vals3
        let z = w U.<++> x U.<++> y
        let r = C.drop (U.size z) U.zeroSize z
        assert $ isJust $ testEquality U.empty r

   , testProperty "append_take" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        Some x <- return $ mkUAsgn vals1
        Some y <- return $ mkUAsgn vals2
        let z = x U.<++> y
        let x' = C.take (U.size x) (U.size y) z
        assert $ isJust $ testEquality x x'

   , testProperty "append_take_drop" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        Some x <- return $ mkUAsgn vals1
        Some y <- return $ mkUAsgn vals2
        let z = x U.<++> y
        let x' = C.take (U.size x) (U.size y) z
        let y' = C.drop (U.size x) (U.size y) z
        assert $ isJust $ testEquality x x'
        assert $ isJust $ testEquality y y'

   , testProperty "append_take_drop_multiple" $ property $
     do vals1 <- forAll genSomePayloadList
        vals2 <- forAll genSomePayloadList
        vals3 <- forAll genSomePayloadList
        vals4 <- forAll genSomePayloadList
        vals5 <- forAll genSomePayloadList
        Some u <- return $ mkUAsgn vals1
        Some v <- return $ mkUAsgn vals2
        Some w <- return $ mkUAsgn vals3
        Some x <- return $ mkUAsgn vals4
        Some y <- return $ mkUAsgn vals5
        let uv = u U.<++> v
        let wxy = w U.<++> x U.<++> y
        -- let z = u C.<++> v C.<++> w C.<++> x C.<++> y
        let z = uv U.<++> wxy
        let uv' = C.take (U.size uv) (U.size wxy) z
        let wxy' = C.drop (U.size uv) (U.size wxy) z
        let withWXY = C.dropPrefix z uv (error "failed dropPrefix")
        assert $ isJust $ testEquality (u U.<++> v) uv'
        assert $ isJust $ testEquality (w U.<++> x U.<++> y) wxy'
        assert $ isJust $ testEquality uv uv'
        assert $ isJust $ testEquality wxy wxy'
        withWXY $ \t -> assert $ isJust $ testEquality wxy' t

   , testProperty "zip/unzip" $ property $
     do Some x <- mkUAsgn <$> forAll genSomePayloadList
        let zipped = C.zipWith Pair x x
        let (x', x'') = C.unzip zipped
        assert $ isJust $ testEquality x x'
        assert $ isJust $ testEquality x x''

   , testProperty "fmapFC_identity" $ property $
     do Some x <- mkUAsgn <$> forAll genSomePayloadList
        assert $ isJust $ testEquality x (fmapFC id x)

   , testProperty "fmapFC_assoc" $ property $
     do Some x <- mkUAsgn <$> forAll genSomePayloadList
        Fun f <- forAll $ HG.element funs
        Fun g <- forAll $ HG.element funs
        assert $ isJust $ testEquality
                            (fmapFC g (fmapFC f x))
                            (fmapFC (g . f) x)

   , testProperty "imapFC_index_noop" $ property $
     do Some x <- mkUAsgn <$> forAll genSomePayloadList
        assert $
          isJust $
            testEquality x (imapFC (\idx _ -> x U.! idx) x)

   , testProperty "imapFC/fmapFC" $ property $
     do Some x <- mkUAsgn <$> forAll genSomePayloadList
        Fun f <- forAll $ HG.element funs
        assert $ isJust $ testEquality
                            (fmapFC f x)
                            (imapFC (const f) x)

   , testProperty "ifoldMapFC/foldMapFC" $ property $
     do Some x <- mkUAsgn <$> forAll genSomePayloadList
        assert $ foldMapFC show x == ifoldMapFC (const show) x

   , testProperty "itraverseFC/traverseFC" $ property $
     do Some x <- mkUAsgn <$> forAll genSomePayloadList
        Fun f <- forAll $ HG.element funs
        let f' :: forall a. Payload a -> Identity (Payload a)
            f' = Identity . f
        assert $ isJust $ testEquality
                            (runIdentity (traverseFC f' x))
                            (runIdentity (itraverseFC (const f') x))

   , testCaseSteps "explicit indexing (unsafe)" $ \step -> do
       let mkUPayload :: U.Assignment Payload TestCtx
           mkUPayload = U.empty
                        `U.extend` IntPayload 1
                        `U.extend` StringPayload "two"
                        `U.extend` IntPayload 3
                        `U.extend` BoolPayload True

           -- Alternative construction using the 'generate' and a
           -- function consuming @Index ctx tp@ selectors to return
           -- the corresponding value
           mkUMyMaybe :: U.Assignment MyMaybe TestCtx
           mkUMyMaybe = U.generate U.knownSize setMyValue
           setMyValue :: U.Index TestCtx tp -> MyMaybe tp
           setMyValue idx
             | Just Refl <- testEquality (U.lastIndex U.knownSize) idx
             = MyJust False
             | Just Refl <- testEquality (U.skipIndex $ U.skipIndex $ U.skipIndex U.baseIndex) idx
             = MyJust 10
             | Just Refl <- testEquality (U.skipIndex $ U.skipIndex $ U.nextIndex U.knownSize) idx
             = MyJust "twenty"
             | Just Refl <- testEquality (U.skipIndex $ U.nextIndex U.knownSize) idx
             = MyNothing
             | otherwise = error $ "setMyValue with unrecognized Index " <> show idx

       step "Verify size of Assignment"
       U.sizeInt (U.size mkUPayload) @?= 4

       step "Verify show of Assignment"
       "[1 :: Int, \"two\" :: String, 3 :: Int, True :: Bool]" @=? show mkUPayload
       "[MyJust 10, MyJust \"twenty\", MyNothing, MyJust False]" @=? show mkUMyMaybe

       step "Verify show explicit indexing"
       Just "\"two\" :: String" @=?
         do Some i <- U.intIndex 1 (U.size mkUPayload)
            return $ show $ mkUPayload U.! i
       Just "1 :: Int" @=?
         do Some i <- U.intIndex 0 (U.size mkUPayload)
            return $ show $ mkUPayload U.! i
       "#<; @0=1 :: Int; @1=\"two\" :: String; @2=3 :: Int; @3=True :: Bool" @=?
         U.forIndex U.knownSize
         (\s idx -> s <> "; @" <> show idx <> "=" <>
                    show (mkUPayload U.! idx))
         "#<"
       (Nothing @String) @=?
         do Some i <- U.intIndex 8 (U.size mkUPayload)
            return $ show $ mkUPayload U.! i

       step "Verify invalid type at index"
       (Nothing :: Maybe Bool) @=?
         do Some i <- U.intIndex 1 (U.size mkUPayload)
            Refl <- testEquality (mkUPayload U.! i) (IntPayload 1)
            return True

   , testCaseSteps "explicit indexing (safe)" $ \step -> do
       let mkSPayload :: S.Assignment Payload TestCtx
           mkSPayload = S.empty
                        `S.extend` IntPayload 1
                        `S.extend` StringPayload "two"
                        `S.extend` IntPayload 3
                        `S.extend` BoolPayload True

           -- Alternative construction using the 'generate' and a
           -- function consuming @Index ctx tp@ selectors to return
           -- the corresponding value
           mkSMyMaybe :: S.Assignment MyMaybe TestCtx
           mkSMyMaybe = S.generate S.knownSize setMyValue
           setMyValue :: S.Index TestCtx tp -> MyMaybe tp
           setMyValue idx
             | Just Refl <- testEquality (S.lastIndex S.knownSize) idx
             = MyJust False
             | Just Refl <- testEquality (S.skipIndex $ S.skipIndex $ S.skipIndex S.baseIndex) idx
             = MyJust 10
             | Just Refl <- testEquality (S.skipIndex $ S.skipIndex $ S.nextIndex S.knownSize) idx
             = MyJust "twenty"
             | Just Refl <- testEquality (S.skipIndex $ S.nextIndex S.knownSize) idx
             = MyNothing
             | otherwise = error $ "setMyValue with unrecognized Index " <> show idx

       step "Verify size of Assignment"
       S.sizeInt (S.size mkSPayload) @?= 4

       step "Verify show of Assignment"
       "[1 :: Int, \"two\" :: String, 3 :: Int, True :: Bool]" @=? show mkSPayload
       "[MyJust 10, MyJust \"twenty\", MyNothing, MyJust False]" @=? show mkSMyMaybe

       step "Verify show explicit indexing"
       Just "\"two\" :: String" @=?
         do Some i <- S.intIndex 1 (S.size mkSPayload)
            return $ show $ mkSPayload S.! i
       Just "1 :: Int" @=?
         do Some i <- S.intIndex 0 (S.size mkSPayload)
            return $ show $ mkSPayload S.! i
       "#<; @3=True :: Bool; @2=3 :: Int; @1=\"two\" :: String; @0=1 :: Int" @=?
         S.forIndex S.knownSize
         (\s idx -> s <> "; @" <> show idx <> "=" <>
                    show (mkSPayload S.! idx))
         "#<"
       (Nothing @String) @=?
         do Some i <- S.intIndex 8 (S.size mkSPayload)
            return $ show $ mkSPayload S.! i

       step "Verify invalid type at index"
       (Nothing :: Maybe Bool) @=?
         do Some i <- S.intIndex 1 (S.size mkSPayload)
            Refl <- testEquality (mkSPayload S.! i) (IntPayload 1)
            return True

   , testCaseSteps "joined Assigment operations (unsafe)" $ \step -> do
       let mkU1 = U.empty
                  `U.extend` IntPayload 1
           mkU2 = U.empty
                  `U.extend` StringPayload "two"
                  `U.extend` IntPayload 3
                  `U.extend` BoolPayload True

       step "Length"
       U.sizeInt (U.size mkU1) + U.sizeInt (U.size mkU2) @?=
         U.sizeInt (U.size (mkU1 U.<++> mkU2))

       step "Index adjustments"
       Just (Some i1) <- return $ U.intIndex 0 (U.size mkU1)
       v1s <- return $ show $ mkU1 U.! i1
       "1 :: Int" @=? v1s
       Just (Some i2) <- return $ U.intIndex 2 (U.size mkU2)
       v2s <- return $ show $ mkU2 U.! i2
       "True :: Bool" @=? v2s
       let mkUB = mkU1 U.<++> mkU2
       v1s' <- return $ show $ mkUB U.! (U.leftIndex (U.size mkU2) i1)
       v1s' @?= v1s
       v2s' <- return $ show $ mkUB U.! (U.rightIndex (U.size mkU1) (U.size mkU2) i2)
       v2s' @?= v2s

   , testCaseSteps "joined Assigment operations (safe)" $ \step -> do
       let mkS1 = S.empty
                  `S.extend` IntPayload 1
           mkS2 = S.empty
                  `S.extend` StringPayload "two"
                  `S.extend` IntPayload 3
                  `S.extend` BoolPayload True

       step "Length"
       S.sizeInt (S.size mkS1) + S.sizeInt (S.size mkS2) @?=
         S.sizeInt (S.size (mkS1 S.<++> mkS2))

       step "Index adjustments"
       Just (Some i1) <- return $ S.intIndex 0 (S.size mkS1)
       v1s <- return $ show $ mkS1 S.! i1
       "1 :: Int" @=? v1s
       Just (Some i2) <- return $ S.intIndex 2 (S.size mkS2)
       v2s <- return $ show $ mkS2 S.! i2
       "True :: Bool" @=? v2s
       let mkSB = mkS1 S.<++> mkS2
       v1s' <- return $ show $ mkSB S.! (S.leftIndex (S.size mkS2) i1)
       v1s' @?= v1s
       v2s' <- return $ show $ mkSB S.! (S.rightIndex (S.size mkS1) (S.size mkS2) i2)
       v2s' @?= v2s

   ]
