{-# LANGUAGE MultiWayIf         #-}
{-# LANGUAGE NumericUnderscores #-}

module Test.MonadTimer (tests) where

import Control.Monad.Class.MonadTimer.SI
import GHC.Real

import Test.QuickCheck
import Test.Tasty
import Test.Tasty.QuickCheck (testProperty)

tests :: TestTree
tests =
    testGroup "Control.Monad.Class.MonadTimer"
    [ testProperty "diffTimeToMicroseconds left inverse"
        prop_diffTimeToMicrosecondsAsIntLeftInverse
    , testProperty "diffTimeToMicroseconds right inverse"
        prop_diffTimeToMicrosecondsAsIntRightInverse
    ]

newtype IntDistr = IntDistr Int
    deriving (Show, Eq)

instance Arbitrary IntDistr where
    arbitrary = oneof
      [ IntDistr <$> arbitrary
      , IntDistr . (maxBound - ) . getNonNegative <$> (arbitrary :: Gen (NonNegative Int))
      , IntDistr . (minBound + ) . getNonNegative <$> (arbitrary :: Gen (NonNegative Int))
      ]

    shrink (IntDistr a) = IntDistr `map` shrink a

prop_diffTimeToMicrosecondsAsIntLeftInverse :: IntDistr -> Bool
prop_diffTimeToMicrosecondsAsIntLeftInverse (IntDistr usec) =
    usec == diffTimeToMicrosecondsAsInt (microsecondsAsIntToDiffTime usec)


newtype DiffTimeDistr = DiffTimeDistr DiffTime
    deriving (Show, Eq)

instance Arbitrary DiffTimeDistr where
    arbitrary = frequency
        [ -- arbitrary DiffTime
          (6, DiffTimeDistr . fromRational <$> arbitrary)
          -- large positive DiffTimes, but smaller than `maxBound :: Int` microseconds
        , (3, DiffTimeDistr
               . (fromRational (toRational (maxBound :: Int) / 1_000_000) - )
               . fromRational
               . getNonNegative
              <$> resize 100 arbitrary
          )
          -- large negative DiffTimes, but larger than `minBound :: Int` microseconds
        , (3, DiffTimeDistr
               . (fromRational (toRational (minBound :: Int) / 1_000_000) + )
               . fromRational
               . getNonNegative
              <$> arbitrary
          )
          -- smaller than 1 :% 1_000_000
        , (1, DiffTimeDistr . fromRational . (/ 1_000_000) <$> resize 1 arbitrary)
        ]

    shrink (DiffTimeDistr a) = (DiffTimeDistr . fromRational) `map` shrink (toRational a)

prop_diffTimeToMicrosecondsAsIntRightInverse :: DiffTimeDistr -> Property
prop_diffTimeToMicrosecondsAsIntRightInverse (DiffTimeDistr a) =
    label (labelRational (toRational a)) $
      abs (toRational a - a') < (1 :% 1_000_000)
      .&&.
      r === microsecondsAsIntToDiffTime (diffTimeToMicrosecondsAsInt r)


  where
    a' = toRational (microsecondsAsIntToDiffTime (diffTimeToMicrosecondsAsInt a))

    -- 'a' rounded to microseconds
    r :: DiffTime
    r = fromRational (toRational x / 1_000_000)
      where
        x :: Integer
        x = round $ (toRational a * 1_000_000)

    labelRational x =
      if | abs x < 1 :% 1_000_000
         -> "small"
         | abs x > toRational (maxBound :: Int) / 1_000_000 - 100
         -> "large"
         | otherwise
         -> "average"