{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}

module FSNotify.Test.Util where

import Control.Exception.Safe (Handler(..))
import Control.Monad.Logger
import Control.Retry
import Data.String.Interpolate
import System.FSNotify
import System.FilePath
import Test.Sandwich
import UnliftIO hiding (poll, Handler)
import UnliftIO.Concurrent
import UnliftIO.Directory

#if !MIN_VERSION_base(4,11,0)
import Data.Monoid
#endif

#ifdef mingw32_HOST_OS
import Data.Bits
import System.Win32.File (getFileAttributes, setFileAttributes, fILE_ATTRIBUTE_TEMPORARY)
import System.Win32.SymbolicLink (createSymbolicLinkFile)

-- Perturb the file's attributes, to check that a modification event is emitted
changeFileAttributes :: FilePath -> IO ()
changeFileAttributes file = do
  attrs <- getFileAttributes file
  setFileAttributes file (attrs `xor` fILE_ATTRIBUTE_TEMPORARY)

createSymLink :: FilePath -> FilePath -> IO ()
#if __GLASGOW_HASKELL__ < 900
createSymLink file1 file2 = createSymbolicLinkFile file1 file2
#else
createSymLink file1 file2 = createSymbolicLinkFile file1 file2 True
#endif

#else
import System.PosixCompat.Files (touchFile, createSymbolicLink)

changeFileAttributes :: FilePath -> IO ()
changeFileAttributes = touchFile

createSymLink :: FilePath -> FilePath -> IO ()
createSymLink = createSymbolicLink
#endif


isMac :: Bool
#ifdef darwin_HOST_OS
isMac = True
#else
isMac = False
#endif

isWin :: Bool
#ifdef mingw32_HOST_OS
isWin = True
#else
isWin = False
#endif

isLinux :: Bool
#ifdef linux_HOST_OS
isLinux = True
#else
isLinux = False
#endif

isFreeBSD :: Bool
#ifdef freebsd_HOST_OS
isFreeBSD = True
#else
isFreeBSD = False
#endif

haveNativeWatcher :: Bool
#ifdef HAVE_NATIVE_WATCHER
haveNativeWatcher = True
#else
haveNativeWatcher = False
#endif

waitUntil :: MonadUnliftIO m => Double -> m a -> m a
#if MIN_VERSION_retry(0, 7, 0)
waitUntil timeInSeconds action = withRunInIO $ \runInIO ->
  recovering policy [\_ -> Handler handleFn] (\_ -> runInIO action)
#else
waitUntil timeInSeconds action = withRunInIO $ \runInIO ->
  recovering policy [\_ -> Handler handleFn] (runInIO action)
#endif
  where
    handleFn :: SomeException -> IO Bool
    handleFn (fromException -> Just (_ :: FailureReason)) = return True
    handleFn _ = return False

    policy = limitRetriesByCumulativeDelay (round (timeInSeconds * 1000000.0)) $ capDelay 1000000 $ exponentialBackoff 1000


data TestFolderContext = TestFolderContext {
  watchedDir :: FilePath
  , filePath :: FilePath
  , getEvents :: IO [Event]
  , clearEvents :: IO ()
  }

data TestFolderGenerator = TestFolderGenerator {
  testFolderGeneratorRootDir :: FilePath
  , testFolderGeneratorId :: MVar Int
  }

newTestFolderGenerator :: MonadUnliftIO m => FilePath -> m TestFolderGenerator
newTestFolderGenerator dir = TestFolderGenerator dir <$> newMVar 0

withTestFolderGenerator :: MonadUnliftIO m => (TestFolderGenerator -> m a) -> m a
withTestFolderGenerator action = do
  withSystemTempDirectory "hfsnotify-tests" $ \dir ->
    newTestFolderGenerator dir >>= action

withRandomTempDirectory :: MonadUnliftIO m => TestFolderGenerator -> (FilePath -> m a) -> m a
withRandomTempDirectory (TestFolderGenerator {..}) action = do
  testId <- modifyMVar testFolderGeneratorId $ \x ->
    return (x + 1, x)
  let dir = testFolderGeneratorRootDir </> ("test_" <> show testId)
  bracket_ (createDirectory dir)
           (removePathForcibly dir)
           (action dir)

withTestFolder :: (
  MonadUnliftIO m, MonadLogger m
  )
  => TestFolderGenerator
  -> ThreadingMode
  -> Bool
  -> Bool
  -> Bool
  -> (FilePath -> m b)
  -> (b -> TestFolderContext -> m a)
  -> m a
withTestFolder testFolderGenerator threadingMode poll recursive nested setup action = do
  withRandomTempDirectory testFolderGenerator $ \watchedDir' -> do
    info [i|Got temp directory: #{watchedDir'}|]
    let fileName = "testfile"
    let baseDir = if nested then watchedDir' </> "subdir" else watchedDir'
    let watchFn = if recursive then watchTree else watchDir

    createDirectoryIfMissing True baseDir

    let p = normalise $ baseDir </> fileName

    setupResult <- setup p

    let pollInterval = 2 * 10^(5 :: Int)

    -- Delay before starting the watcher to make sure setup events picked up.
    --
    -- For MacOS, we can apparently get an event for the creation of "subdir" when doing nested tests,
    -- even though we create the watcher after this.
    --
    -- On Windows, we occasionally see a test flake when there's no pause here.
    --
    -- So, let's put a healthy sleep between the setup actions and the watcher initialization.
    --
    -- When polling, we want to ensure we wait at least as long as the effective filesystem modification
    -- time granularity (which on Linux can be on the order of 10 milliseconds), *or*
    -- the poll interval, whichever is greater.
    threadDelay (max 5_000_000 (3 * pollInterval))

    let conf = defaultConfig {
#ifndef HAVE_NATIVE_WATCHER
          confWatchMode = if poll then WatchModePoll pollInterval else error "No native watcher available."
#else
          confWatchMode = if poll then WatchModePoll pollInterval else WatchModeOS
#endif
          , confThreadingMode = threadingMode
          }

    withRunInIO $ \runInIO ->
      withManagerConf conf $ \mgr -> do
        eventsVar <- newIORef []
        bracket
          (watchFn mgr watchedDir' (const True) (\ev -> atomicModifyIORef eventsVar (\evs -> (ev:evs, ()))))
          (\stop -> stop)
          (\_ -> runInIO $ action setupResult $ TestFolderContext {
            watchedDir = watchedDir'
            , filePath = p
            , getEvents = readIORef eventsVar
            , clearEvents = atomicWriteIORef eventsVar []
            }
          )

parallelWithoutDirectory :: SpecFree context m () -> SpecFree context m ()
parallelWithoutDirectory = parallel' (defaultNodeOptions {
                                         nodeOptionsCreateFolder = False
                                         , nodeOptionsVisibilityThreshold = 70
                                         })
