content: {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE TypeApplications #-}
  {-|
  Module      : HsLua.Packaging.FunctionTests
  Copyright   : © 2020-2024 Albert Krewinkel
  License     : MIT
  Maintainer  : Albert Krewinkel <tarleb@hslua.org>
  
  Tests for calling exposed Haskell functions.
  -}
  module HsLua.Packaging.FunctionTests (tests) where
  
  import Data.Maybe (fromMaybe)
  import Data.Version (makeVersion)
  import HsLua.Core (StackIndex, top)
  import HsLua.Packaging.Convenience
  import HsLua.Packaging.Documentation (getdocumentation)
  import HsLua.Packaging.Function
  import HsLua.Packaging.Types
  import HsLua.Marshalling
    ( forcePeek, peekIntegral, peekRealFloat, peekText
    , pushIntegral, pushRealFloat)
  import Test.Tasty.HsLua ((=:), shouldBeResultOf)
  import Test.Tasty (TestTree, testGroup)
  import Test.Tasty.HUnit ((@=?))
  
  import qualified HsLua.Core as Lua
  
  -- | Calling Haskell functions from Lua.
  tests :: TestTree
  tests = testGroup "Call"
    [ testGroup "push Haskell function"
      [ "DocumentedFunction building" =:
        720
        `shouldBeResultOf` do
          factLua <- factLuaAtIndex <$> Lua.gettop
          Lua.pushinteger 6
          _ <- callFunction factLua
          forcePeek $ peekIntegral @Integer Lua.top
  
      , "error message" =:
        mconcat [ "Integral expected, got boolean\n"
                , "\twhile retrieving function argument n\n"
                , "\twhile retrieving arguments for function factorial"]
        `shouldBeResultOf` do
          factLua <- factLuaAtIndex <$> Lua.gettop
          Lua.pushboolean True
          _ <- callFunction factLua
          forcePeek $ peekText Lua.top
      ]
    , testGroup "use as C function"
      [ "push factorial" =:
        Lua.TypeFunction
        `shouldBeResultOf` do
          pushDocumentedFunction $ factLuaAtIndex 0
          Lua.ltype Lua.top
      , "call factorial" =:
        120
        `shouldBeResultOf` do
          pushDocumentedFunction $ factLuaAtIndex 0
          Lua.pushinteger 5
          Lua.call 1 1
          forcePeek $ peekIntegral @Integer Lua.top
      , "use from Lua" =:
        24
        `shouldBeResultOf` do
          pushDocumentedFunction $ factLuaAtIndex 0
          Lua.setglobal "factorial"
          Lua.loadstring "return factorial(4)" *> Lua.call 0 1
          forcePeek $ peekIntegral @Integer Lua.top
  
      , "with setting an optional param" =:
        8
        `shouldBeResultOf` do
          pushDocumentedFunction nroot
          Lua.setglobal "nroot"
          Lua.loadstring "return nroot(64)" *> Lua.call 0 1
          forcePeek $ peekRealFloat @Double Lua.top
      , "with setting an optional param" =:
        2
        `shouldBeResultOf` do
          pushDocumentedFunction nroot
          Lua.setglobal "nroot"
          Lua.loadstring "return nroot(64, 6)" *> Lua.call 0 1
          forcePeek $ peekRealFloat @Double Lua.top
      ]
  
    , testGroup "documentation access"
      [ "pushDocumentedFunction pushes one value" =:
        1 `shouldBeResultOf` do
          oldtop <- Lua.gettop
          pushDocumentedFunction (factLuaAtIndex 0)
          newtop <- Lua.gettop
          pure (newtop - oldtop)
  
      , "getdocumentation" =:
        "factorial" `shouldBeResultOf` do
          pushDocumentedFunction (factLuaAtIndex 0)
          Lua.TypeTable <- getdocumentation top
          Lua.TypeString <- Lua.getfield top "name"
          forcePeek (peekText top)
  
      , "undocumented value" =:
        Lua.TypeNil `shouldBeResultOf` do
          Lua.pushboolean True
          getdocumentation top
      ]
  
    , testGroup "helpers"
      [ "parameter doc" =:
        ( ParameterDoc
          { parameterName = "test"
          , parameterDescription = "test param"
          , parameterType = "string"
          , parameterIsOptional = False
          }
          @=?
          parameterDoc
            (parameter @Lua.Exception peekText "string" "test" "test param")
        )
      , "optional parameter doc" =:
        ( ParameterDoc
          { parameterName = "test"
          , parameterDescription = "test param"
          , parameterType = "string"
          , parameterIsOptional = True
          }
          @=?
          parameterDoc
            (opt (textParam @Lua.Exception "test" "test param"))
        )
      , "functionResult doc" =:
        ( [ ResultValueDoc
            { resultValueDescription = "int result"
            , resultValueType = "integer"
            }
          ]
          @=?
          fnResultDoc <$>
            functionResult (pushIntegral @Int) "integer" "int result"
        )
      ]
    ]
  
  factLuaAtIndex :: StackIndex -> DocumentedFunction Lua.Exception
  factLuaAtIndex idx =
    toHsFnPrecursor idx "factorial" (liftPure factorial)
    <#> factorialParam
    =#> factorialResult
    #? "Calculates the factorial of a positive integer."
    `since` makeVersion [1,0,0]
  
  -- | Calculate the factorial of a number.
  factorial :: Integer -> Integer
  factorial n = product [1..n]
  
  factorialParam :: Parameter Lua.Exception Integer
  factorialParam = Parameter
    { parameterDoc = ParameterDoc
      { parameterName = "n"
      , parameterType = "integer"
      , parameterDescription = "number for which the factorial is computed"
      , parameterIsOptional = False
      }
    , parameterPeeker = peekIntegral @Integer
    }
  
  factorialResult :: FunctionResults Lua.Exception Integer
  factorialResult = (:[]) $ FunctionResult
    (pushIntegral @Integer)
    (ResultValueDoc "integer" "factorial")
  
  -- | Calculate the nth root of a number. Defaults to square root.
  nroot :: DocumentedFunction Lua.Exception
  nroot = defun "nroot"
    ### liftPure2 nroot'
    <#> parameter (peekRealFloat @Double) "number" "x" ""
    <#> opt (integralParam @Int "n" "")
    =#> functionResult pushRealFloat "number" "nth root"
    where
      nroot' :: Double -> Maybe Int -> Double
      nroot' x nOpt =
        let n = fromMaybe 2 nOpt
        in x ** (1 / fromIntegral n)
  