import Control.Exception.Safe (SomeException)
 import Control.Monad (void)
 import Control.Monad.Error.Class (MonadError(..))
-import Control.Monad.IO.Class (MonadIO(..), liftIO)
 import Control.Monad.Reader (MonadReader(..))
 import Data.Bifunctor (first)
 import Data.ByteString.Lazy (ByteString(..))
 import Lucid
 import Sanitize
 import Servant
+import ServerMonad (MonadDirectory(..))
 import System.Directory (getDirectoryContents, getModificationTime)
 import System.FilePath.Posix ((</>))
 import qualified Data.Text as T
 import qualified Data.Text.IO as T
 
-htmlContainer :: (MonadIO m) => Maybe Theme -> Maybe BlogId -> Html a -> m (Html ())
+htmlContainer :: (MonadDirectory m) => Maybe Theme -> Maybe BlogId -> Html a -> m (Html ())
 htmlContainer theme maybeBlogId contents = do
   nav <- navigation theme
   themeConfig <- themeConfiguration theme maybeBlogId
       nav
       themeConfig
 
-themeConfiguration :: (MonadIO m) => Maybe Theme -> Maybe BlogId -> m (Html ())
+themeConfiguration :: (MonadDirectory m) => Maybe Theme -> Maybe BlogId -> m (Html ())
 themeConfiguration (fromMaybe defaultTheme -> theme) (fromMaybe defaultBlogId -> blogId) = pure $ do
   div_ [role_ "banner"] $ do
     h2_ "Theme:"
   label_ [for_ fieldId] $ toHtml label
   input_ [id_ fieldId, name_ fieldId, value_ $ T.pack $ show value, type_ "number", min_ "0", max_ "255", step_ "1"]
 
-navigation :: (MonadIO m) => Maybe Theme -> m (Html ())
+navigation :: (MonadDirectory m) => Maybe Theme -> m (Html ())
 navigation theme = do
   blogListItems <- blogList theme >>= pure . ul_ [class_ "blog-links"]
   pure $ div_ [role_ "navigation"] $ do
     h2_ "Articles:"
     blogListItems
 
-blogList :: (MonadIO m) => Maybe Theme -> m (Html ())
-blogList theme = liftIO $ getDirectoryContents staticPath >>= mapM blogModificationTime . fmap T.unpack . filter (T.isSuffixOf markdownExtension) . fmap T.pack >>= pure . foldMap (blogListItem theme) . sortOn snd . fmap (first T.pack)
+blogList :: (MonadDirectory m) => Maybe Theme -> m (Html ())
+blogList theme = readDirectory staticPath >>= mapM blogModificationTime . fmap T.unpack . filter (T.isSuffixOf markdownExtension) >>= pure . foldMap (blogListItem theme) . sortOn snd . fmap (first T.pack)
 
-blogModificationTime :: (MonadIO m) => FilePath -> m (FilePath, UTCTime)
-blogModificationTime filePath = liftIO $ getModificationTime (staticPath </> filePath) >>= pure . (,) filePath
+blogModificationTime :: (MonadDirectory m) => FilePath -> m (FilePath, UTCTime)
+blogModificationTime filePath = readModificationTime (staticPath </> filePath) >>= pure . (,) filePath
 
 blogListItem :: Maybe Theme -> (T.Text, UTCTime) -> Html ()
 blogListItem theme (first blogLink -> (Nothing, _)) = pure $ mempty
 imageNotFound :: (MonadError ServerError m) => SomeException -> m a
 imageNotFound _ = throwError $ err404 { errBody = "No image found." }
 
-blogNotFound :: (MonadIO m, MonadError ServerError m, MonadReader ServerConfiguration m) => Maybe Theme -> BlogId -> SomeException -> m a
+blogNotFound :: (MonadDirectory m, MonadError ServerError m, MonadReader ServerConfiguration m) => Maybe Theme -> BlogId -> SomeException -> m a
 blogNotFound theme blogId exceptionReason = do
   showExceptions <- ask >>= pure . configShowExceptions
   body <- htmlContainer theme Nothing $ do
 
 
 import Configuration (ServerConfiguration(..), MonadReadConfig(..))
 import Control.Exception.Safe (MonadCatch(..), MonadThrow(..))
+import Control.Monad ((<=<))
 import Control.Monad.Error.Class (MonadError(..))
 import Control.Monad.Except (ExceptT(..))
 import Control.Monad.IO.Class (MonadIO(..), liftIO)
 import Control.Monad.Reader (MonadReader(..), ReaderT(..))
+import Data.Time.Clock (UTCTime(..))
 import Servant (ServerError)
+import System.Directory (getDirectoryContents, getModificationTime)
 import qualified Data.ByteString as B
+import qualified Data.Text as T
 
 newtype ConfigMonad a = ConfigMonad { runConfigMonad :: IO a
                                     } deriving (Functor, Applicative, Monad, MonadIO)
 newtype ServerMonad a = ServerMonad { runServerMonad :: ReaderT ServerConfiguration (ExceptT ServerError IO) a
                                     } deriving (Functor, Applicative, Monad, MonadIO, MonadReader ServerConfiguration, MonadError ServerError, MonadThrow, MonadCatch)
 
+class (Monad m) => MonadDirectory m where
+  readDirectory :: FilePath -> m [T.Text]
+  readModificationTime :: FilePath -> m UTCTime
+
 instance MonadReadConfig ConfigMonad where
   readConfigFile = liftIO . B.readFile
+
+instance MonadDirectory ServerMonad where
+  readDirectory = liftIO . (pure . fmap T.pack <=< getDirectoryContents)
+  readModificationTime = liftIO . getModificationTime