{-# LANGUAGE OverloadedStrings #-}
{-|
Module      : Test.Swagger.Validate
Description : Exposes functions to validate responses
Copyright   : (c) Rodrigo Setti, 2017
License     : BSD3
Maintainer  : rodrigosetti@gmail.com
Stability   : experimental
Portability : POSIX

Exposes some functions to validate responses against a Swagger schema.
There are four functions that can be used depending whether the response
is parsed, if the operation is available (or just the id)
-}
module Test.Swagger.Validate ( parseResponse
                             , ValidationResult
                             , validateResponseBytes
                             , validateResponseWithOperation
                             , validateResponse ) where

import           Control.Applicative
import           Control.Lens
import           Control.Monad
import           Data.Aeson                              hiding (Result)
import           Data.Attoparsec.ByteString              hiding (Result,
                                                          eitherResult, parse)
import qualified Data.Attoparsec.ByteString.Char8        as AC
import           Data.Attoparsec.ByteString.Lazy         hiding (Result)
import qualified Data.ByteString                         as BS
import qualified Data.ByteString.Lazy                    as LBS
import           Data.CaseInsensitive
import           Data.Char                               (digitToInt)
import           Data.Generics
import qualified Data.HashMap.Strict.InsOrd              as M
import           Data.List
import           Data.Maybe
import           Data.Monoid                             ((<>))
import           Data.Swagger
import           Data.Swagger.Internal.Schema.Validation
import qualified Data.Text                               as T
import           Data.Text.Encoding
import           Network.HTTP.Media
import           Network.HTTP.Types
import           Test.Swagger.Types

type ValidationResult = Either String ()

-- |Validate a response, from a particular operation id, (encoded in a byte-string)
-- against a Swagger schema
validateResponseBytes :: LBS.ByteString -> NormalizedSwagger -> OperationId -> ValidationResult
validateResponseBytes input s opId =
  case parseResponse input of
    Left e         -> Left $ "could not parse HTTP response: " <> e
    Right response -> validateResponse response s opId

-- |Validate a response, from a particular operation id against a Swagger schema
validateResponse:: HttpResponse -> NormalizedSwagger -> OperationId -> ValidationResult
validateResponse res s opid =
    case maybeOp of
      Nothing        -> Left $ "operation not defined: " <> T.unpack opid
      Just operation -> validateResponseWithOperation res s operation
  where
   maybeOp = listToMaybe $ listify operationMatches $ getSwagger s

   operationMatches :: Operation -> Bool
   operationMatches o = Just opid == o ^. operationId

-- |Validate a response, from a particular operation against a Swagger schema
validateResponseWithOperation :: HttpResponse -> NormalizedSwagger -> Operation -> ValidationResult
validateResponseWithOperation res ns operation =
        do let code = statusCode $ responseStatus res
               msr = M.lookup code (operation ^. responses.responses)
                    <|> operation ^. responses.default_

           sr <- maybe (Left $ "unspecified status code: " <> show code) pure (msr >>= refToMaybe)

           -- validate headers
           forM_ (M.toList $ sr ^. headers) $ uncurry $ \k h ->
                do hv <- maybe (Left $ "expected header: " <> T.unpack k) pure
                              $ lookup (mk k) $ responseHeaders res
                   let jhv = fromMaybe (toJSON hv) $ decodeStrict $ encodeUtf8 hv
                   withPrefix ("invalid " <> T.unpack k <> " header value: " <> T.unpack hv)
                              $ validateWithParamSchema' jhv $ h ^. paramSchema

           -- validate body
           case (sr ^. schema >>= refToMaybe, responseBody res) of
             (Nothing, Nothing) -> pure () -- no response expected, got no response (OK)
             (Nothing, Just bs) | LBS.null bs -> pure () -- no response expected, got no response (OK)
             (Just _, Nothing) -> Left $ "expected response body: " <> T.unpack (sr ^. description)
             (Nothing, Just _) -> Left "unexpected response body"
             (Just rs, Just bs) ->
                do jsonMime <- maybe (Left "unexpected!") pure $ parseAccept "application/json"

                   -- TODO: should default be JSON?
                   let respMime = fromMaybe jsonMime
                                   (lookup (mk "Content-Type") (responseHeaders res) >>= (parseAccept . encodeUtf8))

                   -- all possible content-types the operation can produce
                   let mimes = fromMaybe (s ^. produces) $ operation ^. produces

                   -- find one mime that matches
                   matchedMime <- maybe (Left "unexpected content-type") pure
                                $ find (matches respMime) (getMimeList mimes)

                   -- If JSON, validate
                   -- TODO: validate other non-JSON content-types
                   when (matches matchedMime jsonMime) $
                        do b <- withPrefix "Could not parse body" $ eitherDecode bs
                           withPrefix "invalid body" $ validateWithSchema' b rs

 where
   s = getSwagger ns

   -- TODO: make it support patterns
   cfg = defaultConfig

   withPrefix :: String -> Either String a -> Either String a
   withPrefix p (Left e) = Left $ p <> ": " <> e
   withPrefix _ v = v

   validateWithSchema' :: Value -> Schema -> ValidationResult
   validateWithSchema' v = resultToEither . runValidation (validateWithSchema v) cfg

   validateWithParamSchema' :: Value -> ParamSchema t -> ValidationResult
   validateWithParamSchema' v = resultToEither . runValidation (validateWithParamSchema v) cfg

   resultToEither :: Result a -> Either String a
   resultToEither (Failed es) = Left $ intercalate ", " es
   resultToEither (Passed a)  = Right a

-- |Parse a HttpResponse from ByteString
parseResponse :: LBS.ByteString -> Either String HttpResponse
parseResponse = eitherResult . parse responseParser

responseParser :: Parser HttpResponse
responseParser = do ver <- versionParser <?> "http version"
                    skipHorizontalSpace1
                    status <- statusParser <?> "http status"
                    void endOfLine
                    hs <- headerParser `sepBy` endOfLine
                    body <- try (endOfLine >> endOfLine >> (Just <$> bodyParser)) <|> pure Nothing
                    endOfInput
                    pure $ HttpResponse ver status hs body
    where
      endOfLine = string "\r\n" <|> string "\n"

      versionParser :: Parser HttpVersion
      versionParser = choice [ string "HTTP/0.9" >> pure http09
                             , string "HTTP/1.0" >> pure http10
                             , string "HTTP/1.1" >> pure http11 ]

      statusParser :: Parser Status
      statusParser = Status <$> (statusCodeParser <* skipHorizontalSpace1)
                            <*> takeTill (inClass "\r\n")

      skipHorizontalSpace1 = skipMany1 (skip $ inClass " \t")
      skipHorizontalSpace = skipMany (skip $ inClass " \t")

      headerParser :: Parser HttpHeader
      headerParser = do key <- BS.pack <$> many1 (satisfy $ notInClass " \t:")
                        skipHorizontalSpace >> string ":" >> skipHorizontalSpace
                        val <- BS.pack <$> many1 (satisfy $ notInClass "\r\n")
                        pure (mk $ decodeUtf8 key, decodeUtf8 val)

      bodyParser :: Parser LBS.ByteString
      bodyParser = takeLazyByteString

      statusCodeParser :: Parser Int
      statusCodeParser = do a <- digitToInt <$> AC.digit
                            b <- digitToInt <$> AC.digit
                            c <- digitToInt <$> AC.digit
                            pure $ fromIntegral $ a*100 + b*10 + c