{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE StandaloneDeriving         #-}

module Test.Hspec.Dirstream
    ( -- Functions to generate a `Spec`
      testFiles
    , testFilesErr
    , testFilesIO
    , testFilesPure
    -- * Configuration helper functions
    , dirFiles
    , allFiles
    -- * Helper functions for dealing with file extensions
    , F.extension
    , hasExtension
    -- * Types and type synonyms
    , FileProcessor (..)
    , PathFilter
    , Recursor
    , PathProducer
    , SafeSpecM
    ) where

import           Control.Monad.Fail        (MonadFail (..))
import           Data.DirStream
import           Data.Text                 (Text)
import qualified Filesystem.Path.CurrentOS as F
import           Pipes
import qualified Pipes.Prelude             as P
import           Pipes.Safe
import           System.FilePath           hiding (hasExtension)
import           Test.Hspec
import           Test.Hspec.Core.Spec

deriving instance MonadCatch (SpecM a)
deriving instance MonadThrow (SpecM a)
deriving instance MonadMask (SpecM a)
deriving instance MonadIO (SpecM a)

instance MonadFail (SpecM a) where
    fail = error

hasExtension :: Text -> F.FilePath -> Bool
hasExtension = flip F.hasExtension

mapS :: (a -> SpecM () ()) -> Proxy () a y' y (SafeT (SpecM ())) r
mapS = P.mapM_ . (lift .)

type SafeSpecM = SafeT (SpecM ())
type Recursor = F.FilePath -> ListT SafeSpecM F.FilePath
type PathProducer = String -> PathFilter -> Producer String SafeSpecM ()

dirFiles :: PathProducer
dirFiles = getDirFiles childOf

allFiles :: PathProducer
allFiles = getDirFiles descendentOf

getDirFiles :: Recursor -> PathProducer
getDirFiles g dir p = every (g path) >-> P.filter p >-> P.map F.encodeString
    where path = F.decodeString dir

-- | Helper function to generate a spec. The spec runs on the given directory,
-- filtering by the given function. It then compares their output to the text of
-- the file with @.out@ as the new extension.
--
-- As an example, consider the directory structure
--
-- > test/data
-- > ├── file.hs
-- > └── file.out
--
-- If we have a function called @formatFile@ and we run
--
-- > testFiles "test/data" (hasExtension "hs") formatFile
--
-- This would read @test\/data\/file.hs@, format the file if it can, and compare
-- the output to the contents of @test\/data\/file.out@.
testFiles :: (Eq a, Show a)
          => FilePath -- ^ Base directory
          -> (F.FilePath -> Bool) -- ^ Filter on file extensions
          -> (String -> Either a String) -- ^ Function to process a file
          -> SpecWith ()
testFiles = testHelper testFile dirFiles

testHelper :: (a -> String -> SpecWith ()) -> PathProducer -> String -> PathFilter -> a -> SpecWith ()
testHelper testFunction paths dir p = runSafeT . runEffect . (paths dir p >->) . mapS . testFunction

testFilesIO :: FilePath
            -> PathFilter
            -> (String -> IO String)
            -> SpecWith ()
testFilesIO = testHelper testFileIO dirFiles

-- | A very general means of testing files, where the check function is pure.
testFilesPure :: (Show b, Eq b)
           => FilePath
           -> PathFilter
           -> PathProducer
           -> FileProcessor a b
           -> SpecWith ()
testFilesPure dir p paths = runSafeT . runEffect . (paths dir p >->) . mapS . testFilePure

testFileIO :: (String -> IO String) -> String -> SpecWith ()
testFileIO fun f = it f $ do
    sample <- readFile f
    expected <- readFile (replaceExtension f ".out")
    fun sample >>= (`shouldBe` expected)

type PathFilter = F.FilePath -> Bool

-- | This function simply tests that each file returns a 'Left' value and that
-- the error message contained therein matches the contents of the appropriate
-- file.
testFilesErr :: (Show b, Eq b) => FilePath -> PathFilter -> (String -> Either String b) -> SpecWith ()
testFilesErr dir p f = runSafeT $ runEffect $ dirFiles dir p >-> mapS (testFileErr f)

data FileProcessor a b = FileProc { reader    :: FilePath -> IO a -- ^ A function to read files
                                  , processor :: a -> b -- ^ A function to process files after they have been read
                                  , check     :: b -> Bool -- ^ A predicate to check the output.
                                  }

testFilePure :: (Eq b, Show b) => FileProcessor a b -- ^ How to process a given file
                                      -> String -- ^ File name
                                      -> SpecWith ()
testFilePure (FileProc rdr g p) f = it f $ do
    sample <- rdr f
    g sample `shouldSatisfy` p

testFileErr :: (Eq b, Show b) => (String -> Either String b) -> String -> SpecWith ()
testFileErr fun f = it f $ do
    sample <- readFile f
    expected <- readFile (replaceExtension f ".out")
    fun sample `shouldBe` Left expected

testFile :: (Eq a, Show a) => (String -> Either a String) -> String -> SpecWith ()
testFile fun f = it f $ do
    sample <- readFile f
    expected <- readFile (replaceExtension f ".out")
    fun sample `shouldBe` Right expected