-- |
-- Module      : Properties.ByteString
-- Copyright   : (c) Andrew Lelechenko 2021
-- License     : BSD-style

{-# LANGUAGE CPP #-}
{-# LANGUAGE ViewPatterns #-}

-- We are happy to sacrifice optimizations in exchange for faster compilation,
-- but need to test rewrite rules. As one can check using -ddump-rule-firings,
-- rewrite rules do not fire in -O0 mode, so we use -O1, but disable almost all
-- optimizations. It roughly halves compilation time.
{-# OPTIONS_GHC -O1 -fenable-rewrite-rules
  -fmax-simplifier-iterations=1 -fsimplifier-phases=0
  -fno-call-arity -fno-case-merge -fno-cmm-elim-common-blocks -fno-cmm-sink
  -fno-cpr-anal -fno-cse -fno-do-eta-reduction -fno-float-in -fno-full-laziness
  -fno-loopification -fno-specialise -fno-strictness #-}

-- BYTESTRING_CHAR8 and BYTESTRING_LAZY are defined in
-- Properties.ByteString{Char8,Lazy,LazyChar8}, which include this file.
#ifndef BYTESTRING_CHAR8

#if defined(BYTESTRING_SHORT)
module Properties.ShortByteString (tests) where
import qualified Data.ByteString.Short as B
import qualified Data.ByteString.Short.Internal as B
#define BYTESTRING_TYPE B.ShortByteString
#elif !(defined BYTESTRING_LAZY)
module Properties.ByteString (tests) where
#define BYTESTRING_TYPE B.ByteString
import qualified Data.ByteString as B
import GHC.IO.Encoding
#else
module Properties.ByteStringLazy (tests) where
#define BYTESTRING_TYPE B.ByteString
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Lazy.Internal as B (invariant)
#endif

import Data.Word

#else

#ifndef BYTESTRING_LAZY
module Properties.ByteStringChar8 (tests) where
import qualified Data.ByteString.Char8 as B
#define BYTESTRING_TYPE B.ByteString
#else
module Properties.ByteStringLazyChar8 (tests) where
import qualified Data.ByteString.Lazy.Char8 as B
import qualified Data.ByteString.Lazy.Internal as B (invariant)
#define BYTESTRING_TYPE B.ByteString
#endif

import Text.Read

#endif

import Control.Arrow
import Data.Char
import Data.Foldable
import qualified Data.List as List
import Data.Semigroup
import Data.String
import Data.Tuple
import Test.Tasty
import Test.Tasty.QuickCheck
import QuickCheckUtils

#ifndef BYTESTRING_CHAR8
toElem :: Word8 -> Word8
toElem = id
#else
toElem :: Char8 -> Char
toElem (Char8 c) = c
#endif

tests :: [TestTree]
tests =
  [ testProperty "pack . unpack" $
    \x -> x === B.pack (B.unpack x)
  , testProperty "unpack . pack" $
    \(map toElem -> xs) -> xs === B.unpack (B.pack xs)
  , testProperty "read . show" $
    \x -> (x :: BYTESTRING_TYPE) === read (show x)
#ifndef BYTESTRING_SHORT
  , testProperty "fromStrict . toStrict" $
    \x -> B.fromStrict (B.toStrict x) === x
  , testProperty "toStrict . fromStrict" $
    \x -> B.toStrict (B.fromStrict x) === x
#endif
#if !defined(BYTESTRING_LAZY) && !defined(BYTESTRING_CHAR8) && !defined(BYTESTRING_SHORT)
  , testProperty "toFilePath >>= fromFilePath" $
    \x -> ioProperty $ do
      r <- B.toFilePath x >>= B.fromFilePath
      pure (r === x)
  , testProperty "fromFilePath >>= toFilePath" $ ioProperty $ do
    let prop x = ioProperty $ do
          r <- B.fromFilePath x >>= B.toFilePath
          pure (r === x)
    -- Normally getFileSystemEncoding returns a Unicode encoding,
    -- but if it is ASCII, we should not generate Unicode filenames.
    enc <- getFileSystemEncoding
    pure $ case textEncodingName enc of
      "ASCII" -> property (prop . getASCIIString)
      _       -> property prop
#endif

  , testProperty "==" $
    \x y -> (x == y) === (B.unpack x == B.unpack y)
  , testProperty "== refl" $
    \x -> (x :: BYTESTRING_TYPE) == x
  , testProperty "== symm" $
    \x y -> ((x :: BYTESTRING_TYPE) == y) === (y == x)
  , testProperty "== pack unpack" $
    \x -> x == B.pack (B.unpack x)
#ifndef BYTESTRING_SHORT
  , testProperty "== copy" $
    \x -> x == B.copy x
#endif

  , testProperty "compare" $
    \x y -> compare x y === compare (B.unpack x) (B.unpack y)
  , testProperty "compare EQ" $
    \x -> compare (x :: BYTESTRING_TYPE) x == EQ
  , testProperty "compare GT" $
    \x (toElem -> c) -> compare (B.snoc x c) x == GT
  , testProperty "compare LT" $
    \x (toElem -> c) -> compare x (B.snoc x c) == LT
  , testProperty "compare GT empty" $
    \x -> not (B.null x) ==> compare x B.empty == GT
  , testProperty "compare LT empty" $
    \x -> not (B.null x) ==> compare B.empty x == LT
  , testProperty "compare GT concat" $
    \x y -> not (B.null y) ==> compare (x <> y) x == GT
  , testProperty "compare char" $
    \(toElem -> c) (toElem -> d) -> compare c d == compare (B.singleton c) (B.singleton d)
#ifndef BYTESTRING_CHAR8
  , testProperty "compare unsigned" $ once $
    compare (B.singleton 255) (B.singleton 127) == GT
#endif

  , testProperty "null" $
    \x -> B.null x === null (B.unpack x)
  , testProperty "empty 0" $ once $
    B.length B.empty === 0
  , testProperty "empty []" $ once $
    B.unpack B.empty === []
  , testProperty "mempty 0" $ once $
    B.length mempty === 0
  , testProperty "mempty []" $ once $
    B.unpack mempty === []

  , testProperty "concat" $
    \(Sqrt xs) -> B.unpack (B.concat xs) === concat (map B.unpack xs)
  , testProperty "concat [x,x]" $
    \x -> B.unpack (B.concat [x, x]) === concat [B.unpack x, B.unpack x]
  , testProperty "concat [x,[]]" $
    \x -> B.unpack (B.concat [x, B.empty]) === concat [B.unpack x, []]
  , testProperty "mconcat" $
    \(Sqrt xs) -> B.unpack (mconcat xs) === mconcat (map B.unpack xs)
  , testProperty "mconcat [x,x]" $
    \x -> B.unpack (mconcat [x, x]) === mconcat [B.unpack x, B.unpack x]
  , testProperty "mconcat [x,[]]" $
    \x -> B.unpack (mconcat [x, B.empty]) === mconcat [B.unpack x, []]

  , testProperty "null" $
    \x -> B.null x === null (B.unpack x)
  , testProperty "reverse" $
    \x -> B.unpack (B.reverse x) === reverse (B.unpack x)
#ifndef BYTESTRING_SHORT
  , testProperty "transpose" $
    \xs -> map B.unpack (B.transpose xs) === List.transpose (map B.unpack xs)
  , testProperty "group" $
    \x -> map B.unpack (B.group x) === List.group (B.unpack x)
  , testProperty "groupBy" $
    \f x -> map B.unpack (B.groupBy f x) === List.groupBy f (B.unpack x)
  , testProperty "groupBy ==" $
    \x -> map B.unpack (B.groupBy (==) x) === List.groupBy (==) (B.unpack x)
  , testProperty "groupBy /=" $
    \x -> map B.unpack (B.groupBy (/=) x) === List.groupBy (/=) (B.unpack x)
  , testProperty "inits" $
    \x -> map B.unpack (B.inits x) === List.inits (B.unpack x)
  , testProperty "tails" $
    \x -> map B.unpack (B.tails x) === List.tails (B.unpack x)
#endif
  , testProperty "all" $
    \f x -> B.all f x === all f (B.unpack x)
  , testProperty "all ==" $
    \(toElem -> c) x -> B.all (== c) x === all (== c) (B.unpack x)
  , testProperty "any" $
    \f x -> B.any f x === any f (B.unpack x)
  , testProperty "any ==" $
    \(toElem -> c) x -> B.any (== c) x === any (== c) (B.unpack x)
  , testProperty "append" $
    \x y -> B.unpack (B.append x y) === B.unpack x ++ B.unpack y
  , testProperty "mappend" $
    \x y -> B.unpack (mappend x y) === B.unpack x `mappend` B.unpack y
  , testProperty "<>" $
    \x y -> B.unpack (x <> y) === B.unpack x <> B.unpack y
#ifndef BYTESTRING_SHORT
  , testProperty "stimes" $
    \(Sqrt (NonNegative n)) (Sqrt x) -> stimes (n :: Int) (x :: BYTESTRING_TYPE) === mtimesDefault n x
#endif

  , testProperty "break" $
    \f x -> (B.unpack *** B.unpack) (B.break f x) === break f (B.unpack x)
  , testProperty "break ==" $
    \(toElem -> c) x -> (B.unpack *** B.unpack) (B.break (== c) x) === break (== c) (B.unpack x)
  , testProperty "break /=" $
    \(toElem -> c) x -> (B.unpack *** B.unpack) (B.break (/= c) x) === break (/= c) (B.unpack x)
  , testProperty "break span" $
    \f x -> B.break f x === B.span (not . f) x
  , testProperty "breakEnd" $
    \f x -> B.breakEnd f x === swap ((B.reverse *** B.reverse) (B.break f (B.reverse x)))
  , testProperty "breakEnd" $
    \f x -> B.breakEnd f x === B.spanEnd (not . f) x

#ifndef BYTESTRING_LAZY
  , testProperty "break breakSubstring" $
    \(toElem -> c) x -> B.break (== c) x === B.breakSubstring (B.singleton c) x
  , testProperty "breakSubstring" $
    \x y -> not (B.null x) ==> B.null (snd (B.breakSubstring x y)) === not (B.isInfixOf x y)
  , testProperty "breakSubstring empty" $
    \x -> B.breakSubstring B.empty x === (B.empty, x)
#endif
#ifdef BYTESTRING_CHAR8
  , testProperty "break isSpace" $
    \x -> (B.unpack *** B.unpack) (B.break isSpace x) === break isSpace (B.unpack x)
#endif

#ifndef BYTESTRING_SHORT
  , testProperty "concatMap" $
    \f x -> B.unpack (B.concatMap f x) === concatMap (B.unpack . f) (B.unpack x)
  , testProperty "concatMap singleton" $
    \x -> B.unpack (B.concatMap B.singleton x) === concatMap (: []) (B.unpack x)
#endif

  , testProperty "singleton" $
    \(toElem -> c) -> B.unpack (B.singleton c) === [c]
  , testProperty "cons" $
    \(toElem -> c) x -> B.unpack (B.cons c x) === c : B.unpack x
  , testProperty "cons []" $
    \(toElem -> c) -> B.unpack (B.cons c B.empty) === [c]
  , testProperty "uncons" $
    \x -> fmap (second B.unpack) (B.uncons x) === List.uncons (B.unpack x)
  , testProperty "snoc" $
    \(toElem -> c) x -> B.unpack (B.snoc x c) === B.unpack x ++ [c]
  , testProperty "snoc []" $
    \(toElem -> c) -> B.unpack (B.snoc B.empty c) === [c]
  , testProperty "unsnoc" $
    \x -> fmap (first B.unpack) (B.unsnoc x) === unsnoc (B.unpack x)
#ifdef BYTESTRING_LAZY
  , testProperty "cons'" $
    \(toElem -> c) x -> B.unpack (B.cons' c x) === c : B.unpack x
#endif

  , testProperty "drop" $
    \n x -> B.unpack (B.drop n x) === List.genericDrop n (B.unpack x)
  , testProperty "drop 10" $
    \x -> let n = 10 in B.unpack (B.drop n x) === List.genericDrop n (B.unpack x)
  , testProperty "drop 2^31" $
    \x -> let n = 2^31 in B.unpack (B.drop n x) === List.genericDrop n (B.unpack x)
  , testProperty "dropWhile" $
    \f x -> B.unpack (B.dropWhile f x) === dropWhile f (B.unpack x)
  , testProperty "dropWhile ==" $
    \(toElem -> c) x -> B.unpack (B.dropWhile (== c) x) === dropWhile (== c) (B.unpack x)
  , testProperty "dropWhile /=" $
    \(toElem -> c) x -> B.unpack (B.dropWhile (/= c) x) === dropWhile (/= c) (B.unpack x)
#ifdef BYTESTRING_CHAR8
  , testProperty "dropWhile isSpace" $
    \x -> B.unpack (B.dropWhile isSpace x) === dropWhile isSpace (B.unpack x)
#endif

  , testProperty "take" $
    \n x -> B.unpack (B.take n x) === List.genericTake n (B.unpack x)
  , testProperty "take 10" $
    \x -> let n = 10 in B.unpack (B.take n x) === List.genericTake n (B.unpack x)
  , testProperty "take 2^31" $
    \x -> let n = 2^31 in B.unpack (B.take n x) === List.genericTake n (B.unpack x)
  , testProperty "takeWhile" $
    \f x -> B.unpack (B.takeWhile f x) === takeWhile f (B.unpack x)
  , testProperty "takeWhile ==" $
    \(toElem -> c) x -> B.unpack (B.takeWhile (== c) x) === takeWhile (== c) (B.unpack x)
  , testProperty "takeWhile /=" $
    \(toElem -> c) x -> B.unpack (B.takeWhile (/= c) x) === takeWhile (/= c) (B.unpack x)
#ifdef BYTESTRING_CHAR8
  , testProperty "takeWhile isSpace" $
    \x -> B.unpack (B.takeWhile isSpace x) === takeWhile isSpace (B.unpack x)
#endif

  , testProperty "dropEnd" $
    \n x -> B.dropEnd n x === B.take (B.length x - n) x
  , testProperty "dropWhileEnd" $
    \f x -> B.dropWhileEnd f x === B.reverse (B.dropWhile f (B.reverse x))
  , testProperty "takeEnd" $
    \n x -> B.takeEnd n x === B.drop (B.length x - n) x
  , testProperty "takeWhileEnd" $
    \f x -> B.takeWhileEnd f x === B.reverse (B.takeWhile f (B.reverse x))

#ifdef BYTESTRING_LAZY
  , testProperty "invariant" $
    \x -> B.invariant x
  , testProperty "fromChunks . toChunks" $
    \x -> B.fromChunks (B.toChunks x) === x
  , testProperty "toChunks . fromChunks" $
    \xs -> B.toChunks (B.fromChunks xs) === filter (/= mempty) xs
  , testProperty "append lazy" $
    \(toElem -> c) -> B.head (B.singleton c <> undefined) === c
  , testProperty "compareLength 1" $
    \x -> B.compareLength x (B.length x) === EQ
  , testProperty "compareLength 2" $
    \x (toElem -> c) -> B.compareLength (B.snoc x c) (B.length x) === GT
  , testProperty "compareLength 3" $
    \x -> B.compareLength x (B.length x + 1) === LT
  , testProperty "compareLength 4" $
    \x (toElem -> c) -> B.compareLength (B.snoc x c <> undefined) (B.length x) === GT
  , testProperty "compareLength 5" $
    \x n -> B.compareLength x n === compare (B.length x) n
  , testProperty "dropEnd lazy" $
    \(toElem -> c) -> B.take 1 (B.dropEnd 1 (B.singleton c <> B.singleton c <> B.singleton c <> undefined)) === B.singleton c
  , testProperty "dropWhileEnd lazy" $
    \(toElem -> c) -> B.take 1 (B.dropWhileEnd (const False) (B.singleton c <> undefined)) === B.singleton c
  , testProperty "breakEnd lazy" $
    \(toElem -> c) -> B.take 1 (fst $ B.breakEnd (const True) (B.singleton c <> undefined)) === B.singleton c
  , testProperty "spanEnd lazy" $
    \(toElem -> c) -> B.take 1 (fst $ B.spanEnd (const False) (B.singleton c <> undefined)) === B.singleton c
#endif

  , testProperty "length" $
    \x -> B.length x === fromIntegral (length (B.unpack x))
  , testProperty "count" $
    \(toElem -> c) x -> B.count c x === fromIntegral (length (List.elemIndices c (B.unpack x)))
  -- for long strings, the multiplier is non-round (and not power of 2)
  -- to ensure non-trivial prefix or suffix of the string is handled outside any possible SIMD-based loop,
  -- which typically handles chunks of 16 or 32 or 64 etc bytes.
  , testProperty "count (long strings)" $
    \(toElem -> c) x (Positive n) -> B.count c x * fromIntegral n === B.count c (B.concat $ replicate n x)
  , testProperty "filter" $
    \f x -> B.unpack (B.filter f x) === filter f (B.unpack x)
  , testProperty "filter compose" $
    \f g x -> B.filter f (B.filter g x) === B.filter (\c -> f c && g c) x
  , testProperty "filter ==" $
    \(toElem -> c) x -> B.unpack (B.filter (== c) x) === filter (== c) (B.unpack x)
  , testProperty "filter /=" $
    \(toElem -> c) x -> B.unpack (B.filter (/= c) x) === filter (/= c) (B.unpack x)
  , testProperty "partition" $
    \f x -> (B.unpack *** B.unpack) (B.partition f x) === List.partition f (B.unpack x)

  , testProperty "find" $
    \f x -> B.find f x === find f (B.unpack x)
  , testProperty "findIndex" $
    \f x -> B.findIndex f x === fmap fromIntegral (List.findIndex f (B.unpack x))
#ifndef BYTESTRING_SHORT
  , testProperty "findIndexEnd" $
    \f x -> B.findIndexEnd f x === fmap fromIntegral (findIndexEnd f (B.unpack x))
#endif
  , testProperty "findIndices" $
    \f x -> B.findIndices f x === fmap fromIntegral (List.findIndices f (B.unpack x))
  , testProperty "findIndices ==" $
    \(toElem -> c) x -> B.findIndices (== c) x === fmap fromIntegral (List.findIndices (== c) (B.unpack x))

  , testProperty "elem" $
    \(toElem -> c) x -> B.elem c x === elem c (B.unpack x)
#ifndef BYTESTRING_SHORT
  , testProperty "notElem" $
    \(toElem -> c) x -> B.notElem c x === notElem c (B.unpack x)
#endif
  , testProperty "elemIndex" $
    \(toElem -> c) x -> B.elemIndex c x === fmap fromIntegral (List.elemIndex c (B.unpack x))
#ifndef BYTESTRING_SHORT
  , testProperty "elemIndexEnd" $
    \(toElem -> c) x -> B.elemIndexEnd c x === fmap fromIntegral (elemIndexEnd c (B.unpack x))
#endif
  , testProperty "elemIndices" $
    \(toElem -> c) x -> B.elemIndices c x === fmap fromIntegral (List.elemIndices c (B.unpack x))

  , testProperty "isPrefixOf" $
    \x y -> B.isPrefixOf x y === List.isPrefixOf (B.unpack x) (B.unpack y)
  , testProperty "stripPrefix" $
    \x y -> fmap B.unpack (B.stripPrefix x y) === List.stripPrefix (B.unpack x) (B.unpack y)
  , testProperty "isSuffixOf" $
    \x y -> B.isSuffixOf x y === List.isSuffixOf (B.unpack x) (B.unpack y)
  , testProperty "stripSuffix" $
    \x y -> fmap B.unpack (B.stripSuffix x y) === stripSuffix (B.unpack x) (B.unpack y)
#ifndef BYTESTRING_LAZY
  , testProperty "isInfixOf" $
    \x y -> B.isInfixOf x y === List.isInfixOf (B.unpack x) (B.unpack y)
#endif

  , testProperty "map" $
    \f x -> B.unpack (B.map (toElem . f) x) === map (toElem . f) (B.unpack x)
  , testProperty "map compose" $
    \f g x -> B.map (toElem . f) (B.map (toElem . g) x) === B.map (toElem . f . toElem . g) x
  , testProperty "replicate" $
    \n (toElem -> c) -> B.unpack (B.replicate (fromIntegral n) c) === replicate n c
  , testProperty "replicate 0" $
    \(toElem -> c) -> B.unpack (B.replicate 0 c) === replicate 0 c

  , testProperty "span" $
    \f x -> (B.unpack *** B.unpack) (B.span f x) === span f (B.unpack x)
  , testProperty "span ==" $
    \(toElem -> c) x -> (B.unpack *** B.unpack) (B.span (== c) x) === span (== c) (B.unpack x)
  , testProperty "span /=" $
    \(toElem -> c) x -> (B.unpack *** B.unpack) (B.span (/= c) x) === span (/= c) (B.unpack x)
  , testProperty "spanEnd" $
    \f x -> B.spanEnd f x === swap ((B.reverse *** B.reverse) (B.span f (B.reverse x)))
  , testProperty "split" $
    \(toElem -> c) x -> map B.unpack (B.split c x) === split c (B.unpack x)
  , testProperty "split empty" $
    \(toElem -> c) -> B.split c B.empty === []
  , testProperty "splitWith" $
    \f x -> map B.unpack (B.splitWith f x) === splitWith f (B.unpack x)
  , testProperty "splitWith split" $
    \(toElem -> c) x -> B.splitWith (== c) x === B.split c x
  , testProperty "splitWith empty" $
    \f -> B.splitWith f B.empty === []
  , testProperty "splitWith length" $
    \f x -> let splits = B.splitWith f x; l1 = fromIntegral (length splits); l2 = B.length (B.filter f x) in
      (l1 == l2 || l1 == l2 + 1) && sum (map B.length splits) + l2 == B.length x

  , testProperty "splitAt" $
    \n x -> (B.unpack *** B.unpack) (B.splitAt n x) === List.genericSplitAt n (B.unpack x)
  , testProperty "splitAt 10" $
    \x -> let n = 10 in (B.unpack *** B.unpack) (B.splitAt n x) === List.genericSplitAt n (B.unpack x)
  , testProperty "splitAt (2^31)" $
    \x -> let n = 2^31 in (B.unpack *** B.unpack) (B.splitAt n x) === List.genericSplitAt n (B.unpack x)

  , testProperty "head" $
    \x -> not (B.null x) ==> B.head x === head (B.unpack x)
  , testProperty "last" $
    \x -> not (B.null x) ==> B.last x === last (B.unpack x)
  , testProperty "tail" $
    \x -> not (B.null x) ==> B.unpack (B.tail x) === tail (B.unpack x)
  , testProperty "tail length" $
    \x -> not (B.null x) ==> B.length x === 1 + B.length (B.tail x)
  , testProperty "init" $
    \x -> not (B.null x) ==> B.unpack (B.init x) === init (B.unpack x)
  , testProperty "init length" $
    \x -> not (B.null x) ==> B.length x === 1 + B.length (B.init x)
#ifndef BYTESTRING_SHORT
  , testProperty "maximum" $
    \x -> not (B.null x) ==> B.maximum x === maximum (B.unpack x)
  , testProperty "minimum" $
    \x -> not (B.null x) ==> B.minimum x === minimum (B.unpack x)
#endif

  , testProperty "foldl" $
    \f (toElem -> c) x -> B.foldl ((toElem .) . f) c x === foldl ((toElem .) . f) c (B.unpack x)
  , testProperty "foldl'" $
    \f (toElem -> c) x -> B.foldl' ((toElem .) . f) c x === foldl' ((toElem .) . f) c (B.unpack x)
  , testProperty "foldr" $
    \f (toElem -> c) x -> B.foldr ((toElem .) . f) c x === foldr ((toElem .) . f) c (B.unpack x)
  , testProperty "foldr'" $
    \f (toElem -> c) x -> B.foldr' ((toElem .) . f) c x === foldr' ((toElem .) . f) c (B.unpack x)

  , testProperty "foldl cons" $
    \x -> B.foldl (flip B.cons) B.empty x === B.reverse x
  , testProperty "foldr cons" $
    \x -> B.foldr B.cons B.empty x === x
  , testProperty "foldl special" $
    \(Sqrt x) (toElem -> c) -> B.unpack (B.foldl (\acc t -> if t == c then acc else B.cons t acc) B.empty x) ===
      foldl (\acc t -> if t == c then acc else t : acc) [] (B.unpack x)
  , testProperty "foldr special" $
    \(Sqrt x) (toElem -> c) -> B.unpack (B.foldr (\t acc -> if t == c then acc else B.cons t acc) B.empty x) ===
      foldr (\t acc -> if t == c then acc else t : acc) [] (B.unpack x)

  , testProperty "foldl1" $
    \f x -> not (B.null x) ==> B.foldl1 ((toElem .) . f) x === foldl1 ((toElem .) . f) (B.unpack x)
  , testProperty "foldl1'" $
    \f x -> not (B.null x) ==> B.foldl1' ((toElem .) . f) x === List.foldl1' ((toElem .) . f) (B.unpack x)
  , testProperty "foldr1" $
    \f x -> not (B.null x) ==> B.foldr1 ((toElem .) . f) x === foldr1 ((toElem .) . f) (B.unpack x)
  , testProperty "foldr1'" $ -- there is not Data.List.foldr1'
    \f x -> not (B.null x) ==> B.foldr1' ((toElem .) . f) x === foldr1 ((toElem .) . f) (B.unpack x)

  , testProperty "foldl1 const" $
    \x -> not (B.null x) ==> B.foldl1 const x === B.head x
  , testProperty "foldl1 flip const" $
    \x -> not (B.null x) ==> B.foldl1 (flip const) x === B.last x
  , testProperty "foldr1 const" $
    \x -> not (B.null x) ==> B.foldr1 const x === B.head x
  , testProperty "foldr1 flip const" $
    \x -> not (B.null x) ==> B.foldr1 (flip const) x === B.last x
  , testProperty "foldl1 max" $
    \x -> not (B.null x) ==> B.foldl1 max x === B.foldl max minBound x
  , testProperty "foldr1 max" $
    \x -> not (B.null x) ==> B.foldr1 max x === B.foldr max minBound x

#ifndef BYTESTRING_SHORT
  , testProperty "scanl" $
    \f (toElem -> c) x -> B.unpack (B.scanl ((toElem .) . f) c x) === scanl ((toElem .) . f) c (B.unpack x)
  , testProperty "scanl foldl" $
    \f (toElem -> c) x -> not (B.null x) ==> B.last (B.scanl ((toElem .) . f) c x) === B.foldl ((toElem .) . f) c x

  , testProperty "scanr" $
    \f (toElem -> c) x -> B.unpack (B.scanr ((toElem .) . f) c x) === scanr ((toElem .) . f) c (B.unpack x)
  , testProperty "scanl1" $
    \f x -> B.unpack (B.scanl1 ((toElem .) . f) x) === scanl1 ((toElem .) . f) (B.unpack x)
  , testProperty "scanl1 empty" $
    \f -> B.scanl1 f B.empty === B.empty
  , testProperty "scanr1" $
    \f x -> B.unpack (B.scanr1 ((toElem .) . f) x) === scanr1 ((toElem .) . f) (B.unpack x)
  , testProperty "scanr1 empty" $
    \f -> B.scanr1 f B.empty === B.empty
#endif

#if !defined(BYTESTRING_LAZY) && !defined(BYTESTRING_SHORT)
  , testProperty "sort" $
    \x -> B.unpack (B.sort x) === List.sort (B.unpack x)
#endif

#ifndef BYTESTRING_SHORT
  , testProperty "intersperse" $
    \(toElem -> c) x -> B.unpack (B.intersperse c x) === List.intersperse c (B.unpack x)
#endif
  , testProperty "intercalate" $
    \(Sqrt x) (Sqrt ys) -> B.unpack (B.intercalate x ys) === List.intercalate (B.unpack x) (map B.unpack ys)
  , testProperty "intercalate 'c' [x,y]" $
    \(toElem -> c) x y -> B.unpack (B.intercalate (B.singleton c) [x, y]) === List.intercalate [c] [B.unpack x, B.unpack y]
  , testProperty "intercalate split" $
    \(toElem -> c) x -> B.intercalate (B.singleton c) (B.split c x) === x

#ifndef BYTESTRING_SHORT
  , testProperty "mapAccumL" $
    \f (toElem -> c) x -> second B.unpack (B.mapAccumL ((second toElem .) . f) c x) ===
      List.mapAccumL ((second toElem .) . f) c (B.unpack x)
  , testProperty "mapAccumR" $
    \f (toElem -> c) x -> second B.unpack (B.mapAccumR ((second toElem .) . f) c x) ===
      List.mapAccumR ((second toElem .) . f) c (B.unpack x)

  , testProperty "zip" $
    \x y -> B.zip x y === zip (B.unpack x) (B.unpack y)
  , testProperty "zipWith" $
    \f x y -> (B.zipWith f x y :: [Int]) === zipWith f (B.unpack x) (B.unpack y)
  , testProperty "packZipWith" $
    \f x y -> B.unpack (B.packZipWith ((toElem .) . f) x y) === zipWith ((toElem .) . f) (B.unpack x) (B.unpack y)
  , testProperty "unzip" $
    \(fmap (toElem *** toElem) -> xs) -> (B.unpack *** B.unpack) (B.unzip xs) === unzip xs
#endif

  , testProperty "index" $
    \(NonNegative n) x -> fromIntegral n < B.length x ==> B.index x (fromIntegral n) === B.unpack x !! n
  , testProperty "indexMaybe" $
    \(NonNegative n) x -> fromIntegral n < B.length x ==> B.indexMaybe x (fromIntegral n) === Just (B.unpack x !! n)
  , testProperty "indexMaybe Nothing" $
    \n x -> (n :: Int) < 0 || fromIntegral n >= B.length x ==> B.indexMaybe x (fromIntegral n) === Nothing
  , testProperty "!?" $
    \n x -> B.indexMaybe x (fromIntegral (n :: Int)) === x B.!? (fromIntegral n)

#ifdef BYTESTRING_CHAR8
  , testProperty "isString" $
    \x -> x === fromString (B.unpack x)
  , testProperty "readInt 1" $
    \x -> fmap (second B.unpack) (B.readInt x) === readInt (B.unpack x)
  , testProperty "readInt 2" $
    \n -> B.readInt (B.pack (show n)) === Just (n, B.empty)
  , testProperty "readInteger 1" $
    \x -> fmap (second B.unpack) (B.readInteger x) === readInteger (B.unpack x)
  , testProperty "readInteger 2" $
    \n -> B.readInteger (B.pack (show n)) === Just (n, B.empty)
  , testProperty "lines" $
    \x -> map B.unpack (B.lines x) === lines (B.unpack x)
  , testProperty "lines \\n" $ once $
    let x = B.pack "one\ntwo\n\n\nfive\n\nseven\n" in
    map B.unpack (B.lines x) === lines (B.unpack x)
  , testProperty "unlines" $
    \xs -> B.unpack (B.unlines xs) === unlines (map B.unpack xs)
  , testProperty "words" $
    \x -> map B.unpack (B.words x) === words (B.unpack x)
  , testProperty "unwords" $
    \xs -> B.unpack (B.unwords xs) === unwords (map B.unpack xs)
#endif

#ifndef BYTESTRING_LAZY
  , testProperty "unfoldrN" $
    \n f (toElem -> c) -> B.unpack (fst (B.unfoldrN n (fmap (first toElem) . f) c)) ===
      take (fromIntegral n) (List.unfoldr (fmap (first toElem) . f) c)
  , testProperty "unfoldrN replicate" $
    \n (toElem -> c) -> fst (B.unfoldrN n (\t -> Just (t, t)) c) === B.replicate n c
  , testProperty "unfoldr" $
    \n a (toElem -> c) -> B.unpack (B.unfoldr (\x -> if x <= 100 * n then Just (c, x + 1 :: Int) else Nothing) a) ===
      List.unfoldr (\x -> if x <= 100 * n then Just (c, x + 1) else Nothing) a
#endif

#ifdef BYTESTRING_LAZY
  , testProperty "unfoldr" $
    \n f (toElem -> a) -> B.unpack (B.take (fromIntegral n) (B.unfoldr (fmap (first toElem) . f) a)) ===
      take n (List.unfoldr (fmap (first toElem) . f) a)
  , testProperty "repeat" $
    \n (toElem -> c) -> B.take (fromIntegral (n :: Int)) (B.repeat c) ===
      B.take (fromIntegral n) (B.unfoldr (\a -> Just (a, a)) c)
  , testProperty "cycle" $
    \n x -> not (B.null x) ==> B.take (fromIntegral (n :: Int)) (B.cycle x) ===
      B.take (fromIntegral n) (B.concat (List.unfoldr (\a -> Just (a, a)) x))
  , testProperty "iterate" $
    \n f (toElem -> a) -> B.take (fromIntegral (n :: Int)) (B.iterate (toElem . f) a) ===
      B.take (fromIntegral n) (B.unfoldr (\x -> Just (toElem (f x), toElem (f x))) a)
#endif

#ifndef BYTESTRING_CHAR8
  -- issue #393
  , testProperty "fromString non-char8" $
    \s -> fromString s == B.pack (map (fromIntegral . ord :: Char -> Word8) s)
  , testProperty "fromString literal" $
    fromString "\0\1\2\3\4" == B.pack [0,1,2,3,4]
#endif
  ]

unsnoc :: [a] -> Maybe ([a], a)
unsnoc [] = Nothing
unsnoc xs = Just (init xs, last xs)

#ifndef BYTESTRING_SHORT
findIndexEnd :: (a -> Bool) -> [a] -> Maybe Int
findIndexEnd f xs = fmap (\n -> length xs - 1 - n) (List.findIndex f (reverse xs))

elemIndexEnd :: Eq a => a -> [a] -> Maybe Int
elemIndexEnd c xs = fmap (\n -> length xs - 1 - n) (List.elemIndex c (reverse xs))
#endif

stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
stripSuffix x y = fmap reverse (List.stripPrefix (reverse x) (reverse y))

split :: Eq a => a -> [a] -> [[a]]
split c = splitWith (== c)

splitWith :: (a -> Bool) -> [a] -> [[a]]
splitWith _ [] = []
splitWith f ys = go [] ys
  where
    go acc [] = [reverse acc]
    go acc (x : xs)
      | f x       = reverse acc : go [] xs
      | otherwise = go (x : acc) xs

#ifdef BYTESTRING_CHAR8
readInt :: String -> Maybe (Int, String)
readInt xs = case readInteger xs of
  Just (y, zs)
    | y' <- fromInteger y, toInteger y' == y -> Just (y', zs)
  otherwise -> Nothing

readInteger :: String -> Maybe (Integer, String)
readInteger ('+' : xs) = readIntegerUnsigned xs
readInteger ('-' : xs) = fmap (first negate) (readIntegerUnsigned xs)
readInteger xs = readIntegerUnsigned xs

readIntegerUnsigned :: String -> Maybe (Integer, String)
readIntegerUnsigned xs = case readMaybe ys of
  Just y -> Just (y, zs)
  otherwise -> Nothing
  where
    (ys, zs) = span isDigit xs
#endif
