{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleContexts #-}

module Data.Binary.IOSpec (spec) where

import Prelude hiding (read)

import           Control.Exception (Exception, throw)
import           Control.Monad (join)
import           Data.Bifoldable (bitraverse_)
import           Data.Binary (Binary (..), encode)
import           Data.Binary.IO
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Lazy as ByteString.Lazy
import           Data.Foldable (for_)
import           Data.List (isInfixOf)
import           Data.Typeable (typeOf)
import qualified System.IO as IO
import           System.IO.Error (ioeGetErrorString, isIllegalOperation)
import           System.Process (createPipe)
import qualified Test.Hspec as Hspec

-- | Create a pipe with no buffering on read and write side.
createUnbufferedPipe :: IO (IO.Handle, IO.Handle)
createUnbufferedPipe = do
  handles <- createPipe
  join bitraverse_ (`IO.hSetBuffering` IO.NoBuffering) handles
  pure handles

-- | The 'Binary' instance of this type implements a 'get' that always fails
data BadGet

instance Binary BadGet where
  put = error "Not implemented"

  get = fail "get for BadGet will always"

data ExampleException = ExampleException
  deriving (Show, Exception)

-- | Check that a read from the 'IO.Handle' yields the given value.
shouldRead :: (Show a, Eq a, Binary a) => Reader -> a -> Hspec.Expectation
shouldRead reader expectedValue = do
  value <- read reader
  Hspec.shouldBe value expectedValue

-- | Close a handle and verify.
closeHandle :: IO.Handle -> Hspec.Expectation
closeHandle handle = do
  IO.hClose handle
  closed <- IO.hIsClosed handle
  Hspec.shouldBe closed True

withReader :: Hspec.SpecWith (Reader, IO.Handle) -> Hspec.SpecWith a
withReader = Hspec.aroundWith $ \run _ -> do
  (handleRead, handleWrite) <- createUnbufferedPipe

  reader <- newReader handleRead
  run (reader, handleWrite)

  readerWith <- newReaderWith (ByteString.hGetSome handleRead 1024)
  run (readerWith, handleWrite)

withReaderAndWriter :: Hspec.SpecWith (Reader, Writer) -> Hspec.SpecWith a
withReaderAndWriter =
  Hspec.aroundWith $ \run _ ->
    for_ readerMakers $ \mkReader -> for_ writerMakers $ \mkWriter -> do
      (handleRead, handleWrite) <- createUnbufferedPipe

      reader <- mkReader handleRead
      let writer = mkWriter handleWrite

      run (reader, writer)
  where
    readerMakers = [newReader, \handle -> newReaderWith (ByteString.hGetSome handle 1024)]

    writerMakers = [newWriter, newWriterWith . ByteString.hPut]

withPipe :: Hspec.SpecWith (IO.Handle, IO.Handle) -> Hspec.Spec
withPipe = Hspec.before createUnbufferedPipe

spec :: Hspec.Spec
spec = do
  Hspec.describe "Reader" $ do
    let
      testReads value =
        Hspec.it ("reads " <> show (typeOf value)) $ \(reader, handleWrite) -> do
          write handleWrite value
          shouldRead reader value

          write handleWrite value
          write handleWrite value

          shouldRead reader value
          shouldRead reader value

    withReader $ do
      -- Test something with 0 length
      testReads ()

      -- Test something with fixed non-zero length
      testReads (1337 :: Int)

      -- Test something with variable length
      testReads "Hello World"

    Hspec.it "recovers from exception in Get operation" $ do
      reader <- newReaderWith (pure (ByteString.Lazy.toStrict (encode "Hello World")))

      Hspec.shouldThrow (runGet reader (throw ExampleException)) (\ExampleException -> True)

      "Hello World" <- read reader

      pure ()

    withPipe $ do
      -- When the read handle has reached its end, reading from it should not throw an error.
      -- However, no more input can be read therefore the underling 'Get' parser should fail.
      Hspec.it "throws ReaderGetError when Handle is EOF" $ \(handleRead, handleWrite) -> do
        reader <- newReader handleRead

        IO.hClose handleWrite
        eof <- IO.hIsEOF handleRead
        Hspec.shouldBe eof True

        Hspec.shouldThrow (read reader :: IO String) (\ReaderGetError{} -> True)

      -- Reading from a closed handle should throw. That exception needs to surface.
      Hspec.it "throws IllegalOperation when read Handle is closed" $ \(handleRead, _handleWrite) -> do
        reader <- newReader handleRead

        closeHandle handleRead

        Hspec.shouldThrow (read reader :: IO String) isIllegalOperation

      -- Failing 'Get' operations should not advance the stream position.
      Hspec.it "preserves the stream position when Get operation fails" $ \(handleRead, handleWrite) -> do
        reader <- newReader handleRead

        write handleWrite "Hello World"
        Hspec.shouldThrow (read reader :: IO BadGet) (\ReaderGetError{} -> True)
        "Hello World" <- read reader

        pure ()

  Hspec.describe "Writer" $ do
    let
      testWrites value =
        Hspec.it ("writes " <> show (typeOf value)) $ \(reader, writer) -> do
          write writer value
          shouldRead reader value

          write writer value
          write writer value

          shouldRead reader value
          shouldRead reader value

    withReaderAndWriter $ do
      -- Test something with 0 length
      testWrites ()

      -- Test something with fixed non-zero length
      testWrites (1337 :: Int)

      -- Test something with variable length
      testWrites "Hello World"

    withPipe $ do
      Hspec.it "throws ResourceVanished when read Handle is closed" $ \(handleRead, handleWrite) -> do
        let writer = newWriter handleWrite

        closeHandle handleRead

        Hspec.shouldThrow (write writer "Hello World") $ \exception ->
          isInfixOf "resource vanished" (ioeGetErrorString exception)

      Hspec.it "throws IllegalOperation when write Handle is closed" $ \(_handleRead, handleWrite) -> do
        let writer = newWriter handleWrite

        closeHandle handleWrite

        Hspec.shouldThrow (write writer "Hello World") isIllegalOperation

  Hspec.describe "Pipe" $ do
    Hspec.it "is connected" $ do
      (reader, writer) <- newPipe

      write writer "Hello World"
      "Hello World" <- read reader

      empty <- isEmpty reader
      empty `Hspec.shouldBe` True

    Hspec.it "ends if writer is out of scope" $ do
      (reader, _writer) <- newPipe

      isEmpty <- isEmpty reader
      Hspec.shouldBe isEmpty True