{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeOperators #-}

module UnitTests.Distribution.Solver.Modular.QuickCheck.Utils
  ( testPropertyWithSeed
  , ArbitraryOrd (..)
  ) where

import Data.Tagged (Tagged, retag)
import GHC.Generics
import System.Random (getStdRandom, random)

import qualified Test.QuickCheck as QC
import Test.Tasty (TestTree)
import Test.Tasty.Options (OptionDescription, lookupOption, setOption)
import Test.Tasty.Providers (IsTest (..), singleTest)
import Test.Tasty.QuickCheck
  ( QC (..)
  , QuickCheckReplay (..)
  , Testable
  , property
  )

import Distribution.Simple.Utils
import Distribution.Verbosity

-- | Create a QuickCheck test that prints the seed before testing the property.
-- The seed can be useful for debugging non-terminating test cases. This is
-- related to https://github.com/feuerbach/tasty/issues/86.
testPropertyWithSeed :: Testable a => String -> a -> TestTree
testPropertyWithSeed name = singleTest name . QCWithSeed . QC . property

newtype QCWithSeed = QCWithSeed QC

instance IsTest QCWithSeed where
  testOptions = retag (testOptions :: Tagged QC [OptionDescription])

  run options (QCWithSeed test) progress = do
    replay <- case lookupOption options of
      QuickCheckReplayLegacy override -> return override
      _ -> getStdRandom random
    notice normal $ "Using --quickcheck-replay=" ++ show replay
    run (setOption (QuickCheckReplayLegacy replay) options) test progress

-- | Typeclass for doing arbitrary (but law-abiding) comparisons.  See also
-- 'ArbitraryOrd', this is the version that works with 'GHC.Generics'.
class GArbitraryOrd f where
  garbitraryCompare :: QC.Gen (f p -> f p -> Ordering)

instance GArbitraryOrd V1 where
  garbitraryCompare = pure $ \_ _ -> EQ

instance GArbitraryOrd U1 where
  garbitraryCompare = pure $ \_ _ -> EQ

instance (GArbitraryOrd f, GArbitraryOrd g) => GArbitraryOrd (f :+: g) where
  garbitraryCompare = do
    bias <- QC.arbitrary
    lcompare <- garbitraryCompare
    rcompare <- garbitraryCompare
    pure $ \l r ->
      let args = if bias then (l, r) else (r, l)
       in case args of
            (L1 x, L1 y) -> lcompare x y
            (L1 _, R1 _) -> LT
            (R1 x, R1 y) -> rcompare x y
            (R1 _, L1 _) -> GT

instance (GArbitraryOrd f, GArbitraryOrd g) => GArbitraryOrd (f :*: g) where
  garbitraryCompare = do
    bias <- QC.arbitrary
    xcompare <- garbitraryCompare
    ycompare <- garbitraryCompare
    pure $ \l r ->
      let (x1 :*: y1, x2 :*: y2) = if bias then (l, r) else (r, l)
       in case xcompare x1 x2 of
            LT -> LT
            EQ -> ycompare y1 y2
            GT -> GT

instance GArbitraryOrd f => GArbitraryOrd (M1 i t f) where
  garbitraryCompare = (\c (M1 l) (M1 r) -> c l r) <$> garbitraryCompare

instance ArbitraryOrd c => GArbitraryOrd (K1 i c) where
  garbitraryCompare = (\c (K1 l) (K1 r) -> c l r) <$> arbitraryCompare

-- | Typeclass for doing arbitrary (but law-abiding) comparisons.
class ArbitraryOrd a where
  arbitraryCompare :: QC.Gen (a -> a -> Ordering)
  default arbitraryCompare
    :: (Generic a, GArbitraryOrd (Rep a)) => QC.Gen (a -> a -> Ordering)
  arbitraryCompare = (\c l r -> c (from l) (from r)) <$> garbitraryCompare

instance ArbitraryOrd Char where
  arbitraryCompare = arbitraryCompareReverseSection

-- | Construct an arbitrary comparison by (conceptually) laying out all values
-- in a list, picking two values (since we are using arbitrary these should
-- be "good" values), and then reversing the section between these two values.
arbitraryCompareReverseSection
  :: (QC.Arbitrary a, Ord a) => QC.Gen (a -> a -> Ordering)
arbitraryCompareReverseSection = do
  x <- QC.arbitrary
  y <- QC.arbitrary
  let inside n = n >= min x y && n <= max x y
  pure $ \l r -> if inside l && inside r then compare r l else compare l r

instance ArbitraryOrd a => ArbitraryOrd [a] where
  arbitraryCompare = do
    shorterIsLess <- QC.arbitrary
    cmp <- arbitraryCompare
    let go [] [] = EQ
        go [] (_ : _) = if shorterIsLess then LT else GT
        go (_ : _) [] = if shorterIsLess then GT else LT
        go (x : xs) (y : ys) = case cmp x y of
          LT -> LT
          EQ -> go xs ys
          GT -> GT
    pure go
