{-# LANGUAGE NamedFieldPuns #-}
module Test.Options
( -- * Options
  Options(..)
, defaultOptions
, entries_
, args_
  -- * Args optics
, maxSuccess_
, maxSize_
, maxShrinks_
, replay_
  -- * CLI options
, parseOpts
, defaultOpts
, printErrors
, header
, withOptions
) where

import Control.Monad ((<=<))
import Data.Bool (bool)
import Data.Foldable (traverse_)
import Data.List (intercalate)
import Fresnel.Lens (Lens', lens)
import Fresnel.Setter (set, (%~))
import Numeric (readDec)
import System.Console.GetOpt
import System.Environment (getProgName)
import System.Exit (exitFailure, exitSuccess)
import System.IO (hPutStrLn, stderr)
import Test.QuickCheck (Args(..), stdArgs)
import Test.QuickCheck.Random (QCGen)

-- Options

data Options = Options
  { entries :: [String]
  , args    :: Args
  }

defaultOptions :: Options
defaultOptions = Options{ entries = [], args = stdArgs{ maxSuccess = 250, chatty = False }}

entries_ :: Lens' Options [String]
entries_ = lens entries (\ o entries -> o{ entries })

args_ :: Lens' Options Args
args_ = lens args (\ o args -> o{ args })


-- Args optics

maxSuccess_ :: Lens' Args Int
maxSuccess_ = lens maxSuccess (\ a maxSuccess -> a{ maxSuccess })

maxSize_ :: Lens' Args Int
maxSize_ = lens maxSize (\ a maxSize -> a{ maxSize })

maxShrinks_ :: Lens' Args Int
maxShrinks_ = lens maxShrinks (\ a maxShrinks -> a{ maxShrinks })

replay_ :: Lens' Args (Maybe (QCGen, Int))
replay_ = lens replay (\ a replay -> a{ replay })


-- CLI options

parseOpts :: [OptDescr (Options -> Options)] -> [String] -> Either [String] Options
parseOpts opts args
  | null other
  , null errs = Right options
  | otherwise = Left (map ("Unrecognized argument: " ++) other ++ errs)
  where
  options = foldr ($) defaultOptions mods
  (mods, other, errs) = getOpt RequireOrder opts args

defaultOpts :: [OptDescr (Options -> Options)]
defaultOpts =
  [ Option "n" ["successes"] (ReqArg (set (args_.maxSuccess_)    . int)  "N")    "require N successful tests before concluding the property passes"
  , Option "z" ["size"]      (ReqArg (set (args_.maxSize_)       . int)  "N")    "increase the size parameter to a maximum of N for successive tests of a property"
  , Option "s" ["shrinks"]   (ReqArg (set (args_.maxShrinks_)    . int)  "N")    "perform a maximum of N shrinks; setting this to 0 disables shrinking"
  , Option "m" ["match"]     (ReqArg (\ s -> entries_ %~ (s:))           "NAME") "include the named group or property; can be used multiple times to include multiple groups/properties"
  , Option "r" ["replay"]    (ReqArg (set (args_.replay_) . Just . read) "SEED") "the seed and size to repeat"
  ]
  where
  int = fst . head . readDec

printErrors :: [OptDescr a] -> [String] -> IO Bool
printErrors opts errs = do
  name <- getProgName
  False <$ traverse_ (hPutStrLn stderr) (errs ++ [usageInfo (header name opts) opts])

header :: String -> [OptDescr a] -> String
header name opts = "Usage: " ++ name ++ " " ++ unwords (map opt opts) where
  opt (Option short long a _) = bracket (intercalate "|" ([ arg a ['-',c] | c <- short ] ++ map (arg a . ("--" ++)) long))
  arg (NoArg _)    = id
  arg (ReqArg _ s) = (++ " " ++ s)
  arg (OptArg _ s) = (++ bracket s)
  bracket s = "[" ++ s ++ "]"

withOptions :: [OptDescr (Options -> Options)] -> (Options -> IO Bool) -> [String] -> IO ()
withOptions opts f = bool exitFailure exitSuccess <=< either (printErrors opts) f . parseOpts opts