module Main where

import Control.Concurrent
import Control.Exception
import Control.Monad
import Control.Monad.Loops
import Data.Either.Extra
import Data.Foldable.Extra
import Data.Functor
import Data.Maybe
import Data.Time
import Evdev
import Evdev.Codes
import qualified Evdev.Uinput as Uinput
import RawFilePath
import System.FilePath.ByteString
import System.IO.Error
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck

main :: IO ()
main = defaultMain $ testGroup "Tests" [smoke, inverses]

{- | Just checks that we can create a virtual device, find it, see that it has the expected properties,
and read the expected events.
-}
smoke :: TestTree
smoke = testCase "Smoke" do
    start <- newEmptyMVar
    let duName = "evdev-test-device"
        keys = [Key1 .. Key0]
        evs = concatMap ((<$> [Pressed, Released]) . KeyEvent) keys
    du <- Uinput.newDevice duName Uinput.defaultDeviceOpts{Uinput.keys}
    void $ forkIO do
        takeMVar start -- wait until reading device is initialised
        Uinput.writeBatch du evs
    listDirectory evdevDir
        >>= traverse (fmap eitherToMaybe . try @IOError . (retryIf isPermissionError . newDevice) . (evdevDir </>))
        >>= findM (fmap (== duName) . deviceName) . catMaybes
        >>= \case
            Nothing -> assertFailure "Couldn't find device with correct name"
            Just d -> do
                putMVar start ()
                (@?= [EvSyn, EvKey]) =<< deviceEventTypes d
                evs' <- whileJust ((\x -> guard (x /= last evs) $> x) . eventData <$> nextEvent d) pure
                filter (/= SyncEvent SynReport) evs' @?= init evs

inverses :: TestTree
inverses =
    localOption (QuickCheckTests 1000) . testGroup "Inverses" $
        [ testGroup
            "TimeVal"
            [ testProperty "1" \(s, us) ->
                let tv = CTimeVal s us
                 in s < 0 || us < 0 || us >= 1_000_000 || toCTimeVal (fromCTimeVal tv) == tv
            , testProperty "2" \n ->
                let -- 'toCTimeVal' goes from picoseconds to microseconds
                    resolutionFactor = 1_000_000
                 in abs (diffTimeToPicoseconds (fromCTimeVal . toCTimeVal $ picosecondsToDiffTime n) - n)
                        < resolutionFactor
            ]
        , testProperty "EventData" \x@(t, c, _v) ->
            let x'@(t', c', v') = toCEventData (fromCEventData x)
                syncValueZero =
                    -- 'toCEventData' takes all values for sync events to 0 - fine as they don't mean anything
                    and
                        [ t == t'
                        , fromEnum t == fromEnum EvSyn
                        , c == c'
                        , v' == 0
                        ]
             in x' == x || syncValueZero
        ]

--TODO make delay and max retries configurable, add to library?
retryIf :: forall a e. Exception e => (e -> Bool) -> IO a -> IO a
retryIf p x = go 100
  where
    go :: Word -> IO a
    go tries =
        x `catch` \e ->
            if p e && tries /= 0 then threadDelay 10_000 >> go (tries - 1) else throw e