{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE DeriveDataTypeable #-}

-- | Test.TestApproximate uses Test.QuickCheck (Arbitrary) to test
-- that the confidences of Approximates generated by a given function
-- are accurate given a function which generates the real value being
-- approximated.
module Test.TestApproximate
       ( testApproximate
       , testApproximateWith
       , testApproximateResult
       , testApproximateWithResult
       , ApproxTestArgs(..), stdArgs
       , ApproxTestResult(..)
       ) where

import Test.QuickCheck (Arbitrary(..), Gen, generate, resize)
import Data.Approximate (Approximate(..))
import Data.Conduit (($$))
import Numeric.Log.Signed (SignedLog)
import Test.ProbabilityCheck.Internal (DistributionTestResult(..), DistributionTestValue(..), empiricalBernstienStopping, monadicToSource)

-- | Arguments for testApproximate
data ApproxTestArgs = ApproxTestArgs
                      { ataDelta   :: SignedLog Double -- ^ The allowable frequency that the test returns the incorrect result.
                      , ataEpsilon :: SignedLog Double -- ^ Largest value for which differences between the confidence and
                                                       -- actual accuracy should be considered the same.
                      }

-- | Result of a tested approximate.
newtype ApproxTestResult = ApproxTestResult {atrResult :: DistributionTestResult (SignedLog Double)}

-- | Default arguments of 5% false positive/negative and confendences
-- off by less than 1% are considered 'close enough'. These are fairly
-- libral defaults.
stdArgs :: ApproxTestArgs
stdArgs = ApproxTestArgs {ataDelta = 0.05, ataEpsilon = 0.01}

-- | Tests the confidence of an Approximate, and prints results to
-- stdout.
testApproximate :: (Arbitrary a, Ord b) => (a -> Approximate b) -> (a -> b) -> IO ()
testApproximate = testApproximateWith stdArgs

-- | Tests the confidence of an Approximate, with the given arguments,
-- and prints the results to stdout.
testApproximateWith :: (Arbitrary a, Ord b) => ApproxTestArgs -> (a -> Approximate b) -> (a -> b) -> IO ()
testApproximateWith args cApp cAct = testApproximateWithResult args cApp cAct >> return ()

-- | Tests the confidence of an Approximate, producing an
-- ApproxTestResult, and prints the results to stdout.
testApproximateResult :: (Arbitrary a, Ord b) => (a -> Approximate b) -> (a -> b) -> IO ApproxTestResult
testApproximateResult = testApproximateWithResult stdArgs

-- | Tests the confidence of an Approximate, with the given arguments,
-- producing an ApproximateTestResult, and prints the results to
-- stdout.
testApproximateWithResult :: (Arbitrary a, Ord b) => ApproxTestArgs -> (a -> Approximate b) -> (a -> b) -> IO ApproxTestResult
testApproximateWithResult args cApp cAct = do
  let delta = ataDelta args
      epsilon = ataEpsilon args
      sample = arbitrary
      value :: Gen (SignedLog Double)
      value = sample >>= sampleToValue
      sampleToValue a = let (Approximate conf lo _ hi) = cApp a
                            act = cAct a
                            diff = (if lo <= act && act <= hi then 1 else 0) - (realToFrac conf)
                        in return diff
  r <- (monadicToSource $ generate $ resize 1000 value) $$ empiricalBernstienStopping 2 delta epsilon
  case dtrValue r of
    TestZero -> print "Confidence is accurate."
    TestPositive -> print "Confidence is lower than actual accuracy."
    TestNegative -> print "Confidence is incorrectly high."
    TestInsufficientSample -> print "Unable to generate sufficient samples. This should not be possible."
  return $ ApproxTestResult r

