{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}
-- The tests have a number of cases where we do stuff like:
--
-- let 4 = ...
--
-- Letting pattern-match failure fail the test. GHC warns about this,
-- let's shut off that warning:
{-# OPTIONS_GHC -Wno-unused-pattern-binds #-}
module Module.Capnp.Untyped (untypedTests) where

import Prelude hiding (length)

import Test.Hspec

import Control.Monad           (forM_, when)
import Control.Monad.Primitive (RealWorld)
import Data.Foldable           (traverse_)
import Data.ReinterpretCast    (doubleToWord, wordToDouble)
import Data.Text               (Text)
import Test.QuickCheck         (property)
import Test.QuickCheck.IO      (propertyIO)
import Text.Heredoc            (here)

import qualified Data.ByteString as BS
import qualified Data.Vector     as V

import Capnp.Untyped
import Util

import Capnp                (cerialize, createPure, def, getRoot, newRoot)
import Capnp.TraversalLimit (LimitT, evalLimitT, execLimitT)
import Data.Mutable         (Thaw(..))

import Instances ()

import Capnp.Gen.Capnp.Schema.Pure (Brand, Method(..), Node'Parameter)

import qualified Capnp.Classes as C
import qualified Capnp.Message as M

import qualified Capnp.Gen.Capnp.Schema as Schema

untypedTests :: Spec
untypedTests = describe "low-level untyped API tests" $ do
    readTests
    modifyTests
    farPtrTest
    otherMessageTest

readTests :: Spec
readTests = describe "read tests" $
    it "Should agree with `capnp decode`" $ do
        msg <- encodeValue
                    aircraftSchemaSrc
                    "Aircraft"
                    [here|(f16 = (base = (
                       name = "bob",
                       homes = [],
                       rating = 7,
                       canFly = true,
                       capacity = 5173,
                       maxSpeed = 12.0
                    )))|]
        endQuota <- execLimitT 128 $ do
            root <- rootPtr msg
            -- Aircraft just has the union tag, nothing else in it's data
            -- section.
            let 1 = structWordCount root
            3 <- getData 0 root -- tag for F16
            let 1 = structPtrCount root
            Just (PtrStruct f16) <- getPtr 0 root
            let 0 = structWordCount f16
            let 0 = structByteCount f16
            let 1 = structPtrCount f16
            Just (PtrStruct base) <- getPtr 0 f16
            let 4 = structWordCount base -- Except canFly, each field is 1 word, and
                                         -- canFly is aligned such that it ends up
                                         -- consuming a whole word.
            let 32 = structByteCount base -- 32 = 4 * 8
            let 2 = structPtrCount base -- name, homes

            -- Walk the data section:
            7 <- getData 0 base -- rating
            1 <- getData 1 base -- canFly
            5173 <- getData 2 base -- capacity
            12.0 <- wordToDouble <$> getData 3 base

            -- ...and the pointer section:
            Just (PtrList (List8 name)) <- getPtr 0 base
            -- Text values have a NUL terminator, which is included in the
            -- length on the wire. The spec says that this shouldn't be
            -- included in the length reported to the caller, but that needs
            -- to be dealt with by schema-aware code, so this is the length of
            -- "bob\0"
            let 4 = length name

            forM_ (zip [0..3] (BS.unpack "bob\0")) $ \(i, c) -> do
                c' <- index i name
                when (c /= c') $
                    error ("index " ++ show i ++ ": " ++ show c ++ " /= " ++ show c')
            Just (PtrList (List16 homes)) <- getPtr 1 base
            let 0 = length homes
            return ()
        endQuota `shouldBe` 110

data ModTest s = ModTest
    { testIn   :: String
    , testMod  :: Struct (M.MutMsg RealWorld) -> LimitT IO ()
    , testOut  :: String
    , testType :: String
    }

modifyTests :: Spec
modifyTests = describe "modification tests" $ traverse_ testCase
    -- tests for setIndex
    [ ModTest
        { testIn = "(year = 2018, month = 6, day = 20)\n"
        , testType = "Zdate"
        , testOut = "(year = 0, month = 0, day = 0)\n"
        , testMod = setData 0 0
        }
    , ModTest
        { testIn = "(text = \"Hello, World!\")\n"
        , testType = "Z"
        , testOut = "(text = \"hEllo, world!\")\n"
        , testMod = \struct -> do
            Just (PtrList (List8 list)) <- getPtr 0 struct
            setIndex (fromIntegral (fromEnum 'h')) 0 list
            setIndex (fromIntegral (fromEnum 'E')) 1 list
            setIndex (fromIntegral (fromEnum 'w')) 7 list
        }
    , ModTest
        { testIn = "(boolvec = [true, true, false, true])\n"
        , testType = "Z"
        , testOut = "( boolvec = [false, true, true, false] )\n"
        , testMod = \struct -> do
            Just (PtrList (List1 list)) <- getPtr 0 struct
            setIndex False 0 list
            setIndex True 2 list
            setIndex False 3 list
        }
    , ModTest
        { testIn = "(f64 = 2.0)\n"
        , testType = "Z"
        , testOut = "(f64 = 7.2)\n"
        , testMod = setData (doubleToWord 7.2) 1
        }
    , ModTest
        { testIn = unlines
            [ "( size = 4,"
            , "  words = \"Hello, World!\","
            , "  wordlist = [\"apples\", \"oranges\"] )"
            ]
        , testType = "Counter"
        , testOut = unlines
            [ "( size = 4,"
            , "  words = \"oranges\","
            , "  wordlist = [\"apples\", \"Hello, World!\"] )"
            ]
        , testMod = \struct -> do
            Just (PtrList (ListPtr list)) <- getPtr 1 struct
            helloWorld <- getPtr 0 struct
            oranges <- index 1 list
            setPtr oranges 0 struct
            setIndex helloWorld 1 list
        }
    , ModTest
        { testIn = unlines
            [ "( aircraftvec = ["
            , "    ( f16 = ("
            , "        base = ("
            , "          name = \"alice\","
            , "          homes = [],"
            , "          rating = 7,"
            , "          canFly = true,"
            , "          capacity = 4,"
            , "          maxSpeed = 100 ) ) ),"
            , "    ( b737 = ("
            , "        base = ("
            , "          name = \"bob\","
            , "          homes = [],"
            , "          rating = 2,"
            , "          canFly = false,"
            , "          capacity = 9,"
            , "          maxSpeed = 50 ) ) ) ] )"
            ]
        , testType = "Z"
        , testOut = unlines
            [ "( aircraftvec = ["
            , "    ( f16 = ("
            , "        base = ("
            , "          name = \"alice\","
            , "          homes = [],"
            , "          rating = 7,"
            , "          canFly = true,"
            , "          capacity = 4,"
            , "          maxSpeed = 100 ) ) ),"
            , "    ( f16 = ("
            , "        base = ("
            , "          name = \"alice\","
            , "          homes = [],"
            , "          rating = 7,"
            , "          canFly = true,"
            , "          capacity = 4,"
            , "          maxSpeed = 100 ) ) ) ] )"
            ]
        , testMod = \struct -> do
            Just (PtrList (ListStruct list)) <- getPtr 0 struct
            src <- index 0 list
            setIndex src 1 list
        }
    -- tests for allocation functions
    , ModTest
        { testIn = "()"
        , testType = "StackingRoot"
        , testOut = "( aWithDefault = (num = 6400),\n  a = (num = 65, b = (num = 90000)) )\n"

        , testMod = \struct -> do
            when (structPtrCount struct /= 2) $
                error "struct's pointer section is unexpedly small"

            let msg = message struct
            a <- allocStruct msg 1 1
            aWithDefault <- allocStruct msg 1 1
            b <- allocStruct msg 1 0
            setPtr (Just (PtrStruct b)) 0 a
            setPtr (Just (PtrStruct aWithDefault)) 0 struct
            setPtr (Just (PtrStruct a)) 1 struct
            setData 65 0 a
            setData 6400 0 aWithDefault
            setData 90000 0 b
        }
    , ModTest
        { testIn = "()"
        , testType = "HoldsVerTwoTwoList"
        , testOut = "( mylist = [(val = 0, duo = 70), (val = 0, duo = 71), (val = 0, duo = 72), (val = 0, duo = 73)] )\n"
        , testMod = \struct -> do
            mylist <- allocCompositeList (message struct) 2 2 4
            forM_ [0..3] $ \i ->
                index i mylist >>= setData (70 + fromIntegral i) 1
            setPtr (Just $ PtrList $ ListStruct mylist) 0 struct
        }
    , allocNormalListTest "u64vec" 21 allocList64 List64
    , allocNormalListTest "u32vec" 22 allocList32 List32
    , allocNormalListTest "u16vec" 23 allocList16 List16
    , allocNormalListTest "u8vec"  24 allocList8  List8
    , ModTest
        { testIn = "()"
        , testType = "Z"
        , testOut = "( boolvec = [true, false, true] )\n"
        , testMod = \struct -> do
            setData 39 0 struct -- Set the union tag.
            boolvec <- allocList1 (message struct) 3
            forM_ [0..2] $ \i ->
                setIndex (even i) i boolvec
            setPtr (Just $ PtrList $ List1 boolvec) 0 struct
        }
    ]
  where
    -- generate a ModTest for a (normal) list allocation function.
    --
    -- parameters:
    --
    -- * tagname   - the name of the union variant
    -- * tagvalue  - the numeric value of the tag for this variant
    -- * allocList - the allocation function
    -- * dataCon   - the data constructor for 'List' to use.
    allocNormalListTest tagname tagvalue allocList dataCon =
        ModTest
            { testIn = "()"
            , testType = "Z"
            , testOut = "(" ++ tagname ++ " = [0, 1, 2, 3, 4])\n"
            , testMod = \struct -> do
                setData tagvalue 0 struct
                vec <- allocList (message struct) 5
                forM_ [0..4] $ \i -> setIndex (fromIntegral i) i vec
                setPtr (Just $ PtrList $ dataCon vec) 0 struct
            }
    testCase ModTest{..} =
        it ("Should satisfy: " ++ show testIn ++ " : " ++ testType ++ " == " ++ show testOut) $ do
            msg <- thaw =<< encodeValue aircraftSchemaSrc testType testIn
            evalLimitT 128 $ rootPtr msg >>= testMod
            actualOut <- decodeValue aircraftSchemaSrc testType =<< freeze msg
            actualOut `shouldBe` testOut


farPtrTest :: Spec
farPtrTest = describe "Setting cross-segment pointers shouldn't crash" $ do
    -- I(zenhack) am disappointed in hindsight that we only check for crashes
    -- here; we should make these more thorough, actually checking validity
    -- somehow.
    it "Should work when setting the root pointer" $ do
        pure () :: IO () -- Not sure why ghc needs this hint, but it does.
        msg <- M.newMessage Nothing
        -- The allocator always allocates new objects in the last segment, so
        -- if we create a new segment, the call to allocStruct below should
        -- allocate there:
        (1, _) <- M.newSegment msg 16
        struct <- allocStruct msg 3 4
        setRoot struct :: IO ()
    it "Should work when setting a field in a struct" $ do
        pure () :: IO () -- Not sure why ghc needs this hint, but it does.
        evalLimitT maxBound $ do
            msg <- M.newMessage Nothing
            srcStruct <- allocStruct msg 4 4
            (1, _) <- M.newSegment msg 10
            dstStruct <- allocStruct msg 2 2
            ptr <- C.toPtr msg dstStruct
            setPtr ptr 0 srcStruct

otherMessageTest :: Spec
otherMessageTest = describe "Setting pointers in other messages" $
    it "Should copy them if needed." $
        property $ \(name :: Text) (params :: V.Vector Node'Parameter) (brand :: Brand) ->
            propertyIO $ do
                let expected = def
                        { name = name
                        , implicitParameters = params
                        , paramBrand = brand
                        }
                msg :: M.ConstMsg <- createPure maxBound $ do
                        methodMsg <- M.newMessage Nothing
                        nameMsg <- M.newMessage Nothing
                        paramsMsg <- M.newMessage Nothing
                        brandMsg <- M.newMessage Nothing

                        methodCerial <- newRoot methodMsg
                        nameCerial <- cerialize nameMsg name
                        brandCerial <- cerialize brandMsg brand

                        -- We don't implement Cerialize for Vector, so we can't just
                        -- inject params directly. TODO: implement Cerialize for Vector.
                        wrapper <- cerialize paramsMsg expected
                        paramsCerial <- Schema.get_Method'implicitParameters wrapper

                        Schema.set_Method'name methodCerial nameCerial
                        Schema.set_Method'implicitParameters methodCerial paramsCerial
                        Schema.set_Method'paramBrand methodCerial brandCerial

                        pure methodMsg
                actual <- evalLimitT maxBound $ getRoot msg >>= C.decerialize
                actual `shouldBe` expected
