-- |HUnit tests and QuickQuick properties for Happstack.Server.*
module Happstack.Server.Tests (allTests) where

import qualified Codec.Compression.GZip as GZ
import qualified Codec.Compression.Zlib as Z
import Control.Arrow ((&&&))
import Control.Applicative ((<$>))
import Control.Concurrent.MVar
import Control.Monad
import Data.ByteString.Lazy.Char8     (pack, unpack)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy  as L
import Data.List (intercalate)
import qualified Data.Map              as Map
import Happstack.Server                      ( Request(..), Method(..), Response(..), ServerPart, Headers, RqBody(Body), HttpVersion(..)
                                             , ToMessage(..), HeaderPair(..), ok, dir, simpleHTTP'', composeFilter, noContentLength, matchMethod
                                             , look, getDataFn)
import Happstack.Server.FileServe.BuildingBlocks (sendFileResponse)
import Happstack.Server.Cookie
import Happstack.Server.Internal.Compression
import Happstack.Server.Internal.Cookie
import Happstack.Server.Internal.Multipart
import Happstack.Server.Internal.MessageWrap
import Happstack.Server.Internal.RFC822Headers (ContentDisposition(..), parseContentDisposition)
import Happstack.Server.SURI(ToSURI(..), path, query)
import Test.HUnit as HU (Test(..), (~:), (@?=), (@=?), assertEqual)
import Text.ParserCombinators.Parsec

-- |All of the tests for happstack-util should be listed here.
allTests :: Test
allTests =
    "happstack-server tests" ~: [ cookieParserTest
                                , acceptEncodingParserTest
                                , multipart
                                , compressFilterResponseTest
                                , matchMethodTest
                                , cookieHeaderOrderTest
                                , pContentDispositionFilename
                                , applicativeTest
                                ]

cookieParserTest :: Test
cookieParserTest =
    "cookieParserTest" ~:
    [parseCookies "$Version=1;Cookie1=value1;$Path=\"/testpath\";$Domain=example.com;cookie2=value2"
        @?= (Right [
            Cookie "1" "/testpath" "example.com" "cookie1" "value1" False False SameSiteNoValue False
          , Cookie "1" "" "" "cookie2" "value2" False False SameSiteNoValue False
          ])
    ,parseCookies "  \t $Version = \"1\" ; cookie1 = \"randomcrap!@#%^&*()-_+={}[]:;'<>,.?/\\|\" , $Path=/  "
        @?= (Right [
            Cookie "1" "/" "" "cookie1" "randomcrap!@#%^&*()-_+={}[]:;'<>,.?/|" False False SameSiteNoValue False
          ])
    ,parseCookies " cookie1 = value1  "
        @?= (Right [
            Cookie "" "" "" "cookie1" "value1" False False SameSiteNoValue False
          ])
    ,parseCookies " $Version=\"1\";buggygooglecookie = valuewith=whereitshouldnotbe  "
        @?= (Right [
            Cookie "1" "" "" "buggygooglecookie" "valuewith=whereitshouldnotbe" False False SameSiteNoValue False
          ])
    , parseCookies "foo=\"\\\"bar\\\"\""
        @?= (Right [
              Cookie "" "" ""  "foo" "\"bar\"" False False SameSiteNoValue False
             ])
    ]

acceptEncodingParserTest :: Test
acceptEncodingParserTest =
    "acceptEncodingParserTest" ~:
    map (\(str, result) -> either (Left . show) Right (parse encodings "" str) @?= (Right result)) acceptEncodings
    where
      acceptEncodings =
       [ (" gzip;q=1,*, compress ; q = 0.5 ", [("gzip", Just 1),("*", Nothing),("compress", Just 0.5)])
       , (" compress , gzip", [ ("compress", Nothing), ("gzip", Nothing)])
       , (" ", [])
       , (" *", [("*", Nothing)])
       , (" compress;q=0.5, gzip;q=1.0", [("compress", Just 0.5), ("gzip", Just 1.0)])
       , (" gzip;q=1.0, identity; q=0.5, *;q=0", [("gzip", Just 1.0), ("identity",Just 0.5), ("*", Just 0)])
       , (" x-gzip",[("x-gzip", Nothing)])
       ]

multipart :: Test
multipart =
    "split multipart" ~:
    [ ([BodyPart (pack "content-type: text/plain\r\n") (pack "1")], Nothing) @=?
           parseMultipartBody (pack "boundary")
                      (pack "--boundary\r\ncontent-type: text/plain\r\n\r\n1\r\n--boundary--\r\nend")

    , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1")], Nothing) @=?
           parseMultipartBody (pack "boundary.with.dot")
                      (pack "--boundary.with.dot\r\ncontent-type: text/plain\r\n\r\n1\r\n--boundary.with.dot--\r\nend")

    , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1")], Nothing) @=?
           parseMultipartBody (pack "boundary")
                      (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\r\n--boundary--\r\nend")

    , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1\n")], Nothing) @=?
           parseMultipartBody (pack "boundary")
                      (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\n\r\n--boundary--\r\nend")

    , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1\r\n")], Nothing) @=?
           parseMultipartBody (pack "boundary")
                      (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\r\n\r\n--boundary--\r\nend")
    , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1\n\r")], Nothing) @=?
           parseMultipartBody (pack "boundary")
                      (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\n\r\r\n--boundary--\r\nend")

    , ([BodyPart (pack "content-type: text/plain\r\n") (pack "\r\n1\n\r")], Nothing) @=?
           parseMultipartBody (pack "boundary")
                      (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n\r\n1\n\r\r\n--boundary--\r\nend")
    ]

compressFilterResponseTest :: Test
compressFilterResponseTest =
    "compressFilterResponseTest" ~:
     [ uncompressedResponse
     , uncompressedSendFile
     , compressedResponseGZ
     , compressedResponseZ
     , compressedSendFile
     , compressedSendFileNoIdentity
     ]

mkRequest :: Method -> String -> [(String, Cookie)] -> Headers -> L.ByteString -> IO Request
mkRequest method uri cookies headers body =
    do let u = toSURI uri
       ib <- newEmptyMVar
       b  <- newMVar (Body body)
       return $ Request { rqMethod      = method
                        , rqPaths       = (pathEls (path u))
                        , rqUri         = (path u)
                        , rqQuery       = (query u)
                        , rqInputsQuery = (queryInput u)
                        , rqInputsBody  = ib
                        , rqCookies     = cookies
                        , rqVersion     = HttpVersion 1 1
                        , rqHeaders     = headers
                        , rqBody        = b
                        , rqPeer        = ("",0)
                        , rqSecure      = False
                        }

compressPart :: ServerPart Response
compressPart =
    do void compressedResponseFilter
       composeFilter noContentLength
       msum [ dir "response" $ ok (toResponse "compress Response")
            , dir "sendfile" $ ok (sendFileResponse "text/plain" "/dev/null" Nothing 0 100)
            ]

cookieHeaderOrderTestHandler :: ServerPart Response
cookieHeaderOrderTestHandler = do
  expireCookie "thecookie"
  addCookie Session $ mkCookie "thecookie" "value"
  ok $ toResponse $ "works"

cookieHeaderOrderTest :: Test
cookieHeaderOrderTest =
  "cookie header order test" ~:
    do req <- mkRequest GET "/" [] Map.empty L.empty
       res <- simpleHTTP'' cookieHeaderOrderTestHandler req
       let Just pair = (Map.lookup (B.pack "set-cookie") (rsHeaders res))
       assertEqual "Add cookie wins" False $ B.isInfixOf (B.pack "Max-Age=0")
                                                         (last $ hValue pair)

uncompressedResponse :: Test
uncompressedResponse =
    "uncompressedResponse" ~:
      do req <- mkRequest GET "/response" [] Map.empty L.empty
         res <- simpleHTTP'' compressPart req
         assertEqual "respone code"     (rsCode res) 200
         assertEqual "body"             (unpack (rsBody res)) "compress Response"
         assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing

uncompressedSendFile :: Test
uncompressedSendFile =
    "uncompressedSendFile" ~:
      do req <- mkRequest GET "/sendfile" [] Map.empty L.empty
         res <- simpleHTTP'' compressPart req
         assertEqual "respone code"     (rsCode res) 200
         assertEqual "filepath"         (sfFilePath res) "/dev/null"
         assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing

compressedResponseGZ :: Test
compressedResponseGZ =
    "compressedResponseGZ" ~:
      do req <- mkRequest GET "/response" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " gzip;q=1"])) L.empty
         res <- simpleHTTP'' compressPart req
         assertEqual "respone code"     (rsCode res) 200
         assertEqual "body"             (unpack (GZ.decompress (rsBody res))) ("compress Response")
         assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) (Just (B.pack "Content-Encoding", [B.pack "gzip"]))

compressedResponseZ :: Test
compressedResponseZ =
    "compressedResponseZ" ~:
      do req <- mkRequest GET "/response" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " deflate;q=1"])) L.empty
         res <- simpleHTTP'' compressPart req
         assertEqual "respone code"     (rsCode res) 200
         assertEqual "body"             (unpack (Z.decompress (rsBody res))) ("compress Response")
         assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) (Just (B.pack "Content-Encoding", [B.pack "deflate"]))

compressedSendFile :: Test
compressedSendFile =
    "compressedSendfile" ~:
      do req <- mkRequest GET "/sendfile" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " gzip;q=1"])) L.empty
         res <- simpleHTTP'' compressPart req
         assertEqual "respone code"     (rsCode res) 200
         assertEqual "filepath"         (sfFilePath res) "/dev/null"
         assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing

compressedSendFileNoIdentity :: Test
compressedSendFileNoIdentity =
    "compressedSendFileNoIdentity" ~:
      do req <- mkRequest GET "/sendfile" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " gzip;q=1, identity: q=0.0"])) L.empty
         res <- simpleHTTP'' compressPart req
         assertEqual "respone code"     (rsCode res) 406
         assertEqual "body"             (unpack (rsBody res)) ""
         assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing

matchMethodTest :: Test
matchMethodTest =
    "matchMethodTest" ~:
      do forM_ gethead $ \m -> matchMethod GET m @?= True
         forM_ others  $ \m -> matchMethod GET m @?= False
         forM_ gethead $ \m -> matchMethod [GET] m @?= True
         forM_ others  $ \m -> matchMethod [GET] m @?= False
         forM_ gethead $ \m -> matchMethod [GET, HEAD] m @?= True
         forM_ others  $ \m -> matchMethod [GET, HEAD] m @?= False
         matchMethod POST GET   @?= False
         matchMethod POST HEAD  @?= False
         matchMethod POST TRACE @?= False
         matchMethod POST POST  @?= True
         matchMethod [POST, PUT] GET   @?= False
         matchMethod [POST, PUT] HEAD  @?= False
         matchMethod [POST, PUT] TRACE @?= False
         matchMethod [POST, PUT] POST  @?= True
         matchMethod [POST, PUT] PUT   @?= True
         forM_ (others) $ \m -> matchMethod (`notElem` gethead) m @?= True
         forM_ (gethead ++ others) $ \m -> matchMethod () m @?= True
  where
    gethead = [GET, HEAD]
    others  = [POST, PUT, DELETE, TRACE, OPTIONS, CONNECT]


-- | https://github.com/Happstack/happstack-server/pull/56
pContentDispositionFilename :: Test
pContentDispositionFilename =
  "pContentDispositionFilename" ~:
    do let doesNotWorkWithOldParserButWithNew = "form-data; filename=\"file.pdf\"; name=\"file\"" :: String
       c <- parseContentDisposition doesNotWorkWithOldParserButWithNew
       assertEqual "parseContentDisposition" c (ContentDisposition "form-data" [("filename","file.pdf"),("name","file")])

applicativeTest :: Test
applicativeTest =
  "applicativeTest" ~:
    do req <- mkRequest GET "/response" [] mempty L.empty
       res <- flip simpleHTTP'' req $ do
         Left errors <- getDataFn $ (++) <$> look "a" <*> look "b"
         pure $ intercalate "," errors
       let ref = "Parameter not found: a,Parameter not found: b"
       assertEqual "getDataFn/ReaderError doesn't short-circuit" ref (unpack (rsBody res))
