{-# LANGUAGE CPP #-}
{-# LANGUAGE RankNTypes #-}
module Main where

import Codec.Compression.Zlib.Internal
import qualified Codec.Compression.Zlib     as Zlib
import qualified Codec.Compression.GZip     as GZip
import qualified Codec.Compression.Zlib.Raw as Raw

import Test.Codec.Compression.Zlib.Internal ()
import Test.Codec.Compression.Zlib.Stream ()

import Test.QuickCheck
import Test.Tasty
import Test.Tasty.QuickCheck
import Test.Tasty.HUnit
import Utils ()

import Control.Monad
import Control.Monad.ST.Lazy (ST)
import Control.Exception
import qualified Data.ByteString.Char8 as BS.Char8
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString      as BS
import System.IO
#if !(MIN_VERSION_base(4,6,0))
import Prelude hiding (catch)
#endif


main :: IO ()
main = defaultMain $
  testGroup "zlib tests" [
    testGroup "property tests" [
      testProperty "decompress . compress = id (standard)"           prop_decompress_after_compress,
      testProperty "decompress . compress = id (Zlib -> GZipOrZLib)" prop_gziporzlib1,
      testProperty "decompress . compress = id (GZip -> GZipOrZlib)" prop_gziporzlib2,
      testProperty "concatenated gzip members"                       prop_gzip_concat,
      testProperty "multiple gzip members, boundaries (all 2-chunks)" prop_multiple_members_boundary2,
      testProperty "multiple gzip members, boundaries (all 3-chunks)" prop_multiple_members_boundary3,
      testProperty "prefixes of valid stream detected as truncated"  prop_truncated
    ],
    testGroup "unit tests" [
      testCase "simple gzip case"          test_simple_gzip,
      testCase "detect bad crc"            test_bad_crc,
      testCase "detect non-gzip"           test_non_gzip,
      testCase "detect custom dictionary"  test_custom_dict,
      testCase "dectect inflate with wrong dict"   test_wrong_dictionary,
      testCase "dectect inflate with right dict"   test_right_dictionary,
      testCase "handle trailing data"      test_trailing_data,
      testCase "multiple gzip members"     test_multiple_members,
      testCase "check small input chunks"  test_small_chunks,
      testCase "check empty input"         test_empty,
      testCase "check exception raised"    test_exception
    ]
  ]


prop_decompress_after_compress :: Format
                               -> CompressParams
                               -> DecompressParams
                               -> Property
prop_decompress_after_compress w cp dp =
   (w /= zlibFormat || decompressWindowBits dp >= compressWindowBits cp) &&
   (decompressWindowBits dp > compressWindowBits cp) &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   liftM2 (==) (decompress w dp . compress w cp) id


prop_gziporzlib1 :: CompressParams
                 -> DecompressParams
                 -> Property
prop_gziporzlib1 cp dp =
   decompressWindowBits dp > compressWindowBits cp &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   liftM2 (==) (decompress gzipOrZlibFormat dp . compress zlibFormat cp) id


prop_gziporzlib2 :: CompressParams
                 -> DecompressParams
                 -> Property
prop_gziporzlib2 cp dp =
   decompressWindowBits dp >= compressWindowBits cp &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   liftM2 (==) (decompress gzipOrZlibFormat dp . compress gzipFormat cp) id

prop_gzip_concat :: CompressParams
                 -> DecompressParams
                 -> BL.ByteString
                 -> Property
prop_gzip_concat cp dp input =
   decompressWindowBits dp >= compressWindowBits cp &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   let catComp = BL.concat (replicate 5 (compress gzipFormat cp input))
       compCat = compress gzipFormat cp (BL.concat (replicate 5 input))

    in decompress gzipFormat dp { decompressAllMembers = True } catComp
    == decompress gzipFormat dp { decompressAllMembers = True } compCat

prop_multiple_members_boundary2 :: Property
prop_multiple_members_boundary2 =
    forAll shortStrings $ \bs ->
      all (\c -> decomp c == BL.append bs bs)
          (twoChunkSplits (comp bs `BL.append` comp bs))
  where
    comp   = compress gzipFormat defaultCompressParams
    decomp = decompress gzipFormat defaultDecompressParams

    shortStrings = fmap BL.pack $ listOf arbitrary

prop_multiple_members_boundary3 :: Property
prop_multiple_members_boundary3 =
    forAll shortStrings $ \bs ->
      all (\c -> decomp c == BL.append bs bs)
          (threeChunkSplits (comp bs `BL.append` comp bs))
  where
    comp   = compress gzipFormat defaultCompressParams
    decomp = decompress gzipFormat defaultDecompressParams

    shortStrings = sized $ \sz -> resize (sz `div` 10) $
                   fmap BL.pack $ listOf arbitrary

prop_truncated :: Format -> Property
prop_truncated format =
   forAll shortStrings $ \bs ->
     all (truncated decomp)
         (init (BL.inits (comp bs)))
  -- All the initial prefixes of a valid compressed stream should be detected
  -- as truncated.
  where
    comp   = compress format defaultCompressParams
    decomp = decompressST format defaultDecompressParams
    truncated :: (forall s. DecompressStream (ST s)) -> BL.ByteString -> Bool
    truncated = foldDecompressStreamWithInput (\_ r -> r) (\_ -> False)
                  (\err -> case err of TruncatedInput -> True; _ -> False)

    shortStrings = sized $ \sz -> resize (sz `div` 6) arbitrary


test_simple_gzip :: Assertion
test_simple_gzip =
  withSampleData "hello.gz" $ \hnd ->
    let decomp = decompressIO gzipFormat defaultDecompressParams
     in assertDecompressOk hnd decomp

test_bad_crc :: Assertion
test_bad_crc =
  withSampleData "bad-crc.gz" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams
    err <- assertDecompressError hnd decomp
    msg <- assertDataFormatError err
    msg @?= "incorrect data check"

test_non_gzip :: Assertion
test_non_gzip = do
  withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams
    err <- assertDecompressError hnd decomp
    msg <- assertDataFormatError err
    msg @?= "incorrect header check"

  withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO zlibFormat defaultDecompressParams
    err <- assertDecompressError hnd decomp
    msg <- assertDataFormatError err
    msg @?= "incorrect header check"

  withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO rawFormat defaultDecompressParams
    err <- assertDecompressError hnd decomp
    msg <- assertDataFormatError err
    msg @?= "invalid code lengths set"

  withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO gzipOrZlibFormat defaultDecompressParams
    err <- assertDecompressError hnd decomp
    msg <- assertDataFormatError err
    msg @?= "incorrect header check"

test_custom_dict :: Assertion
test_custom_dict =
  withSampleData "custom-dict.zlib" $ \hnd -> do
    let decomp = decompressIO zlibFormat defaultDecompressParams
    err <- assertDecompressError hnd decomp
    err @?= DictionaryRequired

test_wrong_dictionary :: Assertion
test_wrong_dictionary = do
  withSampleData "custom-dict.zlib" $ \hnd -> do
    let decomp = decompressIO zlibFormat defaultDecompressParams {
                                           decompressDictionary = -- wrong dict!
                                             Just (BS.pack [65,66,67])
                                         }

    err <- assertDecompressError hnd decomp
    err @?= DictionaryMismatch

test_right_dictionary :: Assertion
test_right_dictionary = do
  withSampleData "custom-dict.zlib" $ \hnd -> do
    dict <- readSampleData "custom-dict.zlib-dict"
    let decomp = decompressIO zlibFormat defaultDecompressParams {
                                           decompressDictionary =
                                             Just (toStrict dict)
                                         }
    assertDecompressOk hnd decomp

test_trailing_data :: Assertion
test_trailing_data =
  withSampleData "two-files.gz" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams {
                   decompressAllMembers = False
                 }
    chunks <- assertDecompressOkChunks hnd decomp
    case chunks of
      [chunk] -> chunk @?= BS.Char8.pack "Test 1"
      _       -> assertFailure "expected single chunk"

test_multiple_members :: Assertion
test_multiple_members =
  withSampleData "two-files.gz" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams {
                   decompressAllMembers = True
                 }
    chunks <- assertDecompressOkChunks hnd decomp
    case chunks of
      [chunk1,
       chunk2] -> do chunk1 @?= BS.Char8.pack "Test 1"
                     chunk2 @?= BS.Char8.pack "Test 2"
      _       -> assertFailure "expected two chunks"

test_small_chunks :: Assertion
test_small_chunks = do
  uncompressedFile <- readSampleData "not-gzip"
  GZip.compress (smallChunks uncompressedFile) @?= GZip.compress uncompressedFile
  Zlib.compress (smallChunks uncompressedFile) @?= Zlib.compress uncompressedFile
  Raw.compress  (smallChunks uncompressedFile) @?= Raw.compress uncompressedFile

  GZip.decompress (smallChunks (GZip.compress uncompressedFile)) @?= uncompressedFile
  Zlib.decompress (smallChunks (Zlib.compress uncompressedFile)) @?= uncompressedFile
  Raw.decompress  (smallChunks (Raw.compress  uncompressedFile)) @?= uncompressedFile

  compressedFile   <- readSampleData "hello.gz"
  (GZip.decompress . smallChunks) compressedFile @?= GZip.decompress compressedFile

test_empty :: Assertion
test_empty = do
  -- Regression test to make sure we only ask for input once in the case of
  -- initially empty input. We previously asked for input twice before
  -- returning the error.
  let decomp = decompressIO zlibFormat defaultDecompressParams
  case decomp of
    DecompressInputRequired next -> do
      decomp' <- next BS.empty
      case decomp' of
        DecompressStreamError TruncatedInput -> return ()
        _ -> assertFailure "expected truncated error"

    _ -> assertFailure "expected input"

test_exception :: Assertion
test_exception =
 (do
    compressedFile <- readSampleData "bad-crc.gz"
    _ <- evaluate (BL.length (GZip.decompress compressedFile))
    assertFailure "expected exception")

  `catch` \err -> do
      msg <- assertDataFormatError err
      msg @?= "incorrect data check"

toStrict :: BL.ByteString -> BS.ByteString
#if MIN_VERSION_bytestring(0,10,0)
toStrict = BL.toStrict
#else
toStrict = BS.concat . BL.toChunks
#endif

-----------------------
-- Chunk boundary utils

smallChunks :: BL.ByteString -> BL.ByteString
smallChunks = BL.fromChunks . map (\c -> BS.pack [c]) . BL.unpack

twoChunkSplits :: BL.ByteString -> [BL.ByteString]
twoChunkSplits bs = zipWith (\a b -> BL.fromChunks [a,b]) (BS.inits sbs) (BS.tails sbs)
  where
    sbs = toStrict bs

threeChunkSplits :: BL.ByteString -> [BL.ByteString]
threeChunkSplits bs =
    [ BL.fromChunks [a,b,c]
    | (a,x) <- zip (BS.inits sbs) (BS.tails sbs)
    , (b,c) <- zip (BS.inits x) (BS.tails x) ]
  where
    sbs = toStrict bs

--------------
-- HUnit Utils

readSampleData :: FilePath -> IO BL.ByteString
readSampleData file = BL.readFile ("test/data/" ++ file)

withSampleData :: FilePath -> (Handle -> IO a) -> IO a
withSampleData file = withFile ("test/data/" ++ file) ReadMode

expected :: String -> String -> IO a
expected e g = assertFailure ("expected: " ++ e ++ "\nbut got: " ++ g)
            >> fail ""

assertDecompressOk :: Handle -> DecompressStream IO -> Assertion
assertDecompressOk hnd =
    foldDecompressStream
      (BS.hGet hnd 4000 >>=)
      (\_ r -> r)
      (\_ -> return ())
      (\err -> expected "decompress ok" (show err))

assertDecompressOkChunks :: Handle -> DecompressStream IO -> IO [BS.ByteString]
assertDecompressOkChunks hnd =
    foldDecompressStream
      (BS.hGet hnd 4000 >>=)
      (\chunk -> liftM (chunk:))
      (\_ -> return [])
      (\err -> expected "decompress ok" (show err))

assertDecompressError :: Handle -> DecompressStream IO -> IO DecompressError
assertDecompressError hnd =
    foldDecompressStream
      (BS.hGet hnd 4000 >>=)
      (\_ r -> r)
      (\_ -> expected "StreamError" "StreamEnd")
      return

assertDataFormatError :: DecompressError -> IO String
assertDataFormatError (DataFormatError detail) = return detail
assertDataFormatError _                        = assertFailure "expected DataError"
                                              >> return ""
