{-# OPTIONS_GHC -fno-warn-deprecations #-}

module TestImport
    ( module TestImport
    , module X
    ) where

import Application           (makeFoundation, makeMiddleware, shutdownApp)
import ClassyPrelude         as X
  hiding ( delete, deleteBy
         , Handler, Index
         , (<.>), (<|)
         , index, uncons, unsnoc, cons, snoc
         , try, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_
         )
import Database.Persist      as X hiding (get)
import Database.Persist.Sql  as X (SqlPersistM)
import Foundation            as X
import Model                 as X
import Test.Hspec            as X
import Yesod.Default.Config2 (useEnv, loadYamlSettings)
import Yesod.Auth            as X
import Yesod.Test            as X
import Yesod.Core.Unsafe     (fakeHandlerGetLogger)
import Test.QuickCheck       as X
import Test.Hspec.QuickCheck as X hiding (prop)
import Test.QuickCheck.Gen   as X
import Data.Default          as X
import Test.QuickCheck.Instances as X ()
import Test.QuickCheck.Arbitrary.Generic as X
import Test.QuickCheck.Classes as X hiding (jsonLaws)
import Test.QuickCheck.Classes.PathPiece as X
import Test.QuickCheck.Classes.PersistField as X
import Test.QuickCheck.Classes.Hashable as X
import Test.QuickCheck.Classes.JSON as X
import Test.QuickCheck.Classes.HttpApiData as X
import Test.QuickCheck.Classes.Universe as X
import Test.QuickCheck.Classes.Binary as X
import Test.QuickCheck.Classes.Csv as X
import Test.QuickCheck.IO as X
import Control.Lens.Properties as X
import Data.Proxy as X
import Data.UUID as X (UUID)
import System.IO as X (hPrint, hPutStrLn)
import Jobs (handleJobs)
import Numeric.Natural as X
import Network.URI.Arbitrary as X ()

import qualified Network.Wai as Wai
import qualified Network.Wai.Test as Wai
import qualified Network.Wai.Test.Internal as Wai (ClientState)
import Network.HTTP.Types (Status(..), hContentType, hAccept)
import Network.HTTP.Types.Header (hHost)
import qualified Network.HTTP.Types as Wai

import Control.Monad.Trans.Except (ExceptT)
import qualified Servant.Client.Core as Servant
import Servant.Client.Core.ClientError
import Servant.Client.Core.RunClient
import Control.Monad.Except (MonadError(..))
import Control.Monad.State.Class (MonadState(..))
import qualified Control.Monad.State.Class as State
import qualified Servant.Types.SourceT as S
import Servant.API (SourceIO)

import Utils (throwExceptT)

import Yesod.Servant (ServantApi, servantApiBaseUrl)

import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.ByteString.Lazy as LBS hiding (ByteString)
import qualified Data.Binary.Builder as B
import Network.HTTP.Media (renderHeader)
import Control.Monad.Fail

import Control.Lens as X hiding ((<.), elements)

import Network.IP.Addr as X (IP)

import Database (truncateDb)
import Database as X (fillDb)
import User as X (fakeUser)

import Control.Monad.Catch as X hiding (Handler(..))

import Control.Monad.Trans.Resource (runResourceT)

import Settings.WellKnownFiles as X

import Data.CaseInsensitive as X (CI)
import qualified Data.CaseInsensitive as CI

import Data.Typeable

import Handler.Utils (runAppLoggingT)

import Web.PathPieces (toPathPiece)
import Utils.Parameters (GlobalPostParam(PostLoginDummy))

import Control.Monad.Morph as X (generalize)

import Control.Monad.Logger (runNoLoggingT)
import Utils.DB (customRunSqlPool)


runDB :: SqlPersistM a -> YesodExample UniWorX a
runDB query = do
  app <- getTestYesod
  liftIO $ runDBWithApp app query

runDBWithApp :: MonadIO m => UniWorX -> SqlPersistM a -> m a
runDBWithApp app query = liftIO . runResourceT . runNoLoggingT . customRunSqlPool query $ appConnPool app

runHandler :: Handler a -> YesodExample UniWorX a
runHandler handler = do
  app <- getTestYesod
  logger <- liftIO . readTVarIO . snd $ appLogger app
  fakeHandlerGetLogger (const logger) app handler


withApp :: YSpec UniWorX -> Spec
withApp = around $ \act -> runResourceT $ do
    settings <- liftIO $ loadYamlSettings
      ["config/test-settings.yml", "config/settings.yml"]
      []
      useEnv
    foundation <- makeFoundation settings
    wipeDB foundation
    runAppLoggingT foundation $ handleJobs foundation
    logWare <- makeMiddleware foundation
    lift $ act (foundation, logWare) `finally` shutdownApp foundation

-- This function will truncate all of the tables in your database.
-- 'withApp' calls it before each test, creating a clean environment for each
-- spec to run in.
wipeDB :: MonadUnliftIO m => UniWorX -> m ()
wipeDB app = runDBWithApp app Database.truncateDb

-- | Authenticate as a user. This relies on the `auth-dummy-login: true` flag
-- being set in test-settings.yaml, which enables dummy authentication in
-- Foundation.hs
authenticateAs :: Entity User -> YesodExample UniWorX ()
authenticateAs (Entity _ User{..}) = do
  request $ do
    setMethod "GET"
    addRequestHeader ("Accept-Language", "de")
    setUrl $ AuthR LoginR

  request $ do
    setMethod "POST"
    addToken_ "#login--dummy"
    addPostParam (toPathPiece PostLoginDummy) $ CI.original userIdent
    setUrl $ AuthR $ PluginR "dummy" []

-- | Create a user.  The dummy email entry helps to confirm that foreign-key
-- checking is switched off in wipeDB for those database backends which need it.
createUser :: (User -> User) -> YesodExample UniWorX (Entity User)
createUser = runDB . insertEntity . fakeUser

lawsCheckHspec :: Typeable a => Proxy a -> [Proxy a -> Laws] -> Spec
lawsCheckHspec p = parallel . describe (show $ typeRep p) . mapM_ (checkHspec . ($ p))
  where
    checkHspec (Laws className properties) = describe className $
      forM_ properties $ \(name, prop) -> it name $ property prop


newtype ServantExample a = ServantExample
  { unServantExample :: ReaderT ServantExampleEnv (ExceptT ClientError Wai.Session) a
  } deriving stock (Generic, Typeable)
    deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader ServantExampleEnv, MonadError ClientError, MonadThrow, MonadCatch, MonadState Wai.ClientState)

data ServantExampleEnv = ServantExampleEnv
  { yseBaseUrl :: BaseUrl
  , yseMakeClientRequest :: BaseUrl -> Servant.Request -> IO Wai.Request
  } deriving (Generic, Typeable)

runServantExample :: (Route (ServantApi proxy) -> Route UniWorX) -> ServantExample a -> YesodExample UniWorX a
runServantExample apiR (ServantExample act) = do
  yseBaseUrl <- runHandler $ servantApiBaseUrl apiR
  let yseMakeClientRequest burl Servant.Request{..} = do
        ((body, bodyLength), contentTypeHdr) <- case requestBody of
            Nothing           -> return ((return BS.empty, Wai.KnownLength 0), Nothing)
            Just (body', typ) -> let (mkBody, bLength) = convertBody body'
                                  in (, Just (hContentType, renderHeader typ)) . (, bLength) <$> mkBody

        return $ Wai.defaultRequest
          { Wai.requestMethod = requestMethod
          , Wai.requestHeaders = maybeToList acceptHdr ++ maybeToList contentTypeHdr ++ headers
          , Wai.requestHeaderHost =
              let BaseUrl{..} = yseBaseUrl
               in Just . encodeUtf8 . pack $ baseUrlHost <> bool (":" <> show baseUrlPort) mempty (baseUrlPort == 80)
          , Wai.requestBody = body, Wai.requestBodyLength = bodyLength
          , Wai.isSecure = isSecure
          }
          & flip Wai.setPath (encodeUtf8 (pack $ baseUrlPath burl) <> toStrict (B.toLazyByteString requestPath) <> Wai.renderQuery True (toList requestQueryString))
        where
          headers = filter (\(h, _) -> h `notElem` [hAccept, hContentType, hHost]) $ toList requestHeaders

          acceptHdr
              | null hs   = Nothing
              | otherwise = Just (hAccept, renderHeader hs)
            where
              hs = toList requestAccept

          convertBody :: Servant.RequestBody -> (IO (IO ByteString), Wai.RequestBodyLength)
          convertBody bd = case bd of
              Servant.RequestBodyLBS body'       -> ( givesPopper . S.source . map fromStrict $ LBS.toChunks body'
                                                    , Wai.KnownLength . fromIntegral $ LBS.length body'
                                                    )
              Servant.RequestBodyBS body'        -> ( return $ return body'
                                                    , Wai.KnownLength . fromIntegral $ BS.length body'
                                                    )
              Servant.RequestBodySource sourceIO -> ( givesPopper sourceIO
                                                    , Wai.ChunkedBody
                                                    )
            where
              givesPopper :: SourceIO Lazy.ByteString -> IO (IO ByteString)
              givesPopper sourceIO = S.unSourceT sourceIO $ \step0 -> do
                  ref <- newMVar step0
                  return $ modifyMVar ref nextBs

              nextBs S.Stop          = return (S.Stop, BS.empty)
              nextBs (S.Error err)   = fail err
              nextBs (S.Skip s)      = nextBs s
              nextBs (S.Effect ms)   = ms >>= nextBs
              nextBs (S.Yield lbs s) = case LBS.toChunks lbs of
                  []     -> nextBs s
                  (x:xs) | BS.null x -> nextBs step'
                         | otherwise -> return (step', x)
                      where
                        step' = S.Yield (LBS.fromChunks xs) s

          isSecure = case baseUrlScheme burl of
              Servant.Http -> False
              Servant.Https -> True
  YesodExampleData waiApp _ _ _ <- State.get
  liftIO . flip Wai.runSession waiApp . throwExceptT $ runReaderT act ServantExampleEnv{..}

instance RunClient ServantExample where
  runRequestAcceptStatus acceptStatus req = do
    ServantExampleEnv{..} <- ask
    waiRequest <- liftIO $ yseMakeClientRequest yseBaseUrl req
    waiResponse@Wai.SResponse{..} <- ServantExample . lift . lift $ Wai.request waiRequest
    let Status{..} = simpleStatus
        statusOk = case acceptStatus of
          Nothing -> 200 <= statusCode && statusCode < 300
          Just good -> simpleStatus `elem` good
        response = (waiResponseToResponse waiResponse) { Servant.responseHttpVersion = Wai.httpVersion waiRequest }
    unless statusOk $
      throwError $ mkFailureResponse yseBaseUrl req response
    return response
    where
      mkFailureResponse :: BaseUrl -> Servant.Request -> Servant.ResponseF Lazy.ByteString -> ClientError
      mkFailureResponse burl request' =
          FailureResponse (bimap (const ()) f request')
        where
          f b = (burl, LBS.toStrict $ B.toLazyByteString b)

      waiResponseToResponse :: Wai.SResponse -> Servant.Response
      waiResponseToResponse Wai.SResponse{..} = Servant.Response
          { responseStatusCode  = simpleStatus
          , responseBody        = simpleBody
          , responseHeaders     = fromList simpleHeaders
          , responseHttpVersion = error "WAI Response does not carry http version information"
          }
  throwClientError = throwError
