module Handler.Sheet.PersonalisedFilesSpec where

import TestImport

import Utils.Files
import Handler.Sheet.PersonalisedFiles

import qualified Yesod.Persist as Yesod

import ModelSpec ()

import Data.Universe.Class

import Data.Conduit
import qualified Data.Conduit.Combinators as C
import Control.Lens.Extras
import Control.Monad.Trans.Maybe
  
import System.FilePath (dropDrive)

import Data.Time.Clock (diffUTCTime)
import Data.Char (chr)

import Database.Persist.Sql (transactionUndo)

import Data.Bitraversable

import qualified Data.Set as Set


instance Arbitrary (FileReferenceResidual PersonalisedSheetFile) where
  arbitrary = PersonalisedSheetFileResidual
    <$> arbitrary
    <*> arbitrary
    <*> elements [ sfType | sfType <- universeF, sfType /= SheetMarking ]

instance Arbitrary PersonalisedSheetFilesDownloadAnonymous where
  arbitrary = elements universeF


spec :: Spec
spec = withApp . describe "Personalised sheet file zip encoding" $ do
  it "roundtrips" . replicateM_ 10 . runHandler . Yesod.runDB $ do
    term <- liftIO $ generate arbitrary
    tid <- insert term
    school <- liftIO $ generate arbitrary
    ssh <- insert school
    course <- liftIO $ generate arbitrary <&> \c -> c { courseTerm = tid, courseSchool = ssh }
    cid <- insert course
    sheet <- liftIO $ generate arbitrary <&> \s -> s { sheetCourse = cid }
    shid <- insert sheet

    sheetFiles' <- liftIO . generate . listOf $ scale (`div` 2) arbitrary
    sheetFiles <- fmap catMaybes . forM sheetFiles' $ \(f', res') -> runMaybeT $ do
      let f = f' { fileTitle = filter (/= chr 0) $ fileTitle f' } -- PostgreSQL doesn't like to store NUL-bytes in text
      guard . not . null . dropDrive $ fileTitle f
      
      uid <-
        let userLoop = do
              user <- liftIO $ generate arbitrary
              lift (insertUnique user) >>= maybe userLoop return
         in userLoop
      let res = res' { personalisedSheetFileResidualSheet = shid, personalisedSheetFileResidualUser = uid }
      fRef <- lift (sinkFile (transFile generalize f) :: DB FileReference)
      now <- liftIO getCurrentTime
      void . lift . insert $ CourseParticipant cid (personalisedSheetFileResidualUser res) now Nothing CourseParticipantActive
      void . lift . insert $ _FileReference # (fRef, res)
      return (f, res)

    anonMode <- liftIO $ generate arbitrary

    let
      fpL :: forall m. Lens' (Either PersonalisedSheetFile (File m)) FilePath
      fpL = lens (either personalisedSheetFileTitle fileTitle) $ \f' path -> case f' of
        Left pf -> Left pf { personalisedSheetFileTitle = path }
        Right f -> Right f { fileTitle = path }
      isDirectory = either (is _Nothing . personalisedSheetFileContent) (is _Nothing . fileContent)

      loadFile :: Either (PersonalisedSheetFileUnresolved (Either PersonalisedSheetFile DBFile)) (Either PersonalisedSheetFile DBFile, FileReferenceResidual PersonalisedSheetFile)
               -> DB (Either (PersonalisedSheetFileUnresolved (Either PersonalisedSheetFile PureFile)) (Either PersonalisedSheetFile PureFile, FileReferenceResidual PersonalisedSheetFile))
      loadFile = bitraverse loadUnresolved loadResolved
        where
          loadUnresolved = traverse $ traverse toPureFile
          loadResolved (f, fRes) = (, fRes) <$> traverse toPureFile f

    recoveredFiles <- runConduit $
         sourcePersonalisedSheetFiles cid (Just shid) Nothing anonMode Set.empty
      .| resolvePersonalisedSheetFiles fpL isDirectory cid shid
      .| C.mapM loadFile
      .| C.foldMap pure

    let
      checkFile :: Either (PersonalisedSheetFileUnresolved (Either PersonalisedSheetFile PureFile)) (Either PersonalisedSheetFile PureFile, FileReferenceResidual PersonalisedSheetFile)
                -> (PureFile, FileReferenceResidual PersonalisedSheetFile)
                -> Bool
      checkFile (Left _) _
        = False
      checkFile (Right (recFile, recResidual)) (file, residual)
        =    recResidual == residual
          && case recFile of
               Right f -> f == file
               Left pf -> dropDrive (fileTitle file) == dropDrive (personalisedSheetFileTitle pf)
                       && abs (fileModified file `diffUTCTime` personalisedSheetFileModified pf) < 1e-6 -- Precision is a PostgreSQL limitation
                       && fileReferenceContent (pureFileToFileReference file) == personalisedSheetFileContent pf

      errors = go [] sheetFiles recoveredFiles
        where go acc xs [] = reverse acc ++ map Left xs
              go acc [] ys = reverse acc ++ map Right ys
              go acc xs (y:ys)
                | (xs', _ : xs'') <- break (checkFile y) xs
                = go acc (xs' ++ xs'') ys
                | is (_Left . _PSFUnresolved) y
                , fromMaybe False $ previews (_Left . _PSFUnresolved . _Right . _fileTitle) ("meta-informationen" `isInfixOf`) y -- DEBUG; remove once _PSFUnresolvedCollatable works
                = go acc xs ys
                | isn't (_Left . _PSFUnresolved) y
                , isn't _Right y
                = go acc xs ys
                | otherwise       = go (Right y : acc) xs ys

    unless (null errors) . liftIO $ 
      expectationFailure $ show recoveredFiles ++ " does not match " ++ show sheetFiles ++ ": " ++ show errors

    transactionUndo
