{-# LANGUAGE ViewPatterns #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

module QuadraticIrrational (tests) where

import Control.Applicative
import Data.Number.CReal
import Test.Tasty
import Test.Tasty.QuickCheck

import Numeric.QuadraticIrrational
import Numeric.QuadraticIrrational.Internal.Lens

-- Slow but precise.
type RefFloat = CReal

instance Arbitrary QI where
  arbitrary = consQI <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
    where
     consQI a b (NonNegative c) (NonZero d) = qi a b c d

  shrink (unQI -> ~(a,b,c,d)) =
    [ qi a' b  c  d  | a' <- shrink a ] ++
    [ qi a  b' c  d  | b' <- shrink b ] ++
    [ qi a  b  c' d  | NonNegative c' <- shrink (NonNegative c) ] ++
    [ qi a  b  c  d' | NonZero     d' <- shrink (NonZero     d) ]

tests :: TestTree
tests =
  testGroup "QuadraticIrrational"
    [ testGroup "Construction/destruction/conversion"
      [ testProperty "qi/runQI" $ \a b (NonNegative c) (NonZero d) ->
          runQI (qi a b c d) $ \a' b' c' d' ->
            approxEq' (approxQI a b c d) (approxQI a' b' c' d')

      , testProperty "qi/runQI'" $ \a b (NonNegative c) (NonZero d) ->
          runQI' (qi a b c d) $ \a' b' c' ->
            approxEq' (approxQI a b c d) (approxQI' a' b' c')

      , testProperty "qi'/runQI" $ \a b (NonNegative c) ->
          runQI (qi' a b c) $ \a' b' c' d' ->
            approxEq' (approxQI' a b c) (approxQI a' b' c' d')

      , testProperty "qi'/runQI'" $ \a b (NonNegative c) ->
          runQI' (qi' a b c) $ \a' b' c' ->
            approxEq' (approxQI' a b c) (approxQI' a' b' c')
      ]
    , testGroup "Lenses"
      [ testProperty "_qi" $ \n a' b' (NonNegative c') (NonZero d') ->
          let n'  = over _qi (\(a,b,c,d) -> (a+a',b-b',c*c',d*d')) n
              n'' = runQI n $ \a b c d -> qi (a+a') (b-b') (c*c') (d*d')
          in  approxEq (qiToFloat n') (qiToFloat n'')

      , testProperty "_qi'" $ \n a' b' (NonNegative c') ->
          let n'  = over _qi' (\(a,b,c) -> (a+a',b-b',c*c')) n
              n'' = runQI' n $ \a b c -> qi' (a+a') (b-b') (c*c')
          in  approxEq (qiToFloat n') (qiToFloat n'')

      , testProperty "_qiABD" $ \n a' b' (NonZero d') ->
          let n'  = over _qiABD (\(a,b,d) -> (a+a',b-b',d*d')) n
              n'' = runQI n $ \a b c d -> qi (a+a') (b-b') c (d*d')
          in  approxEq (qiToFloat n') (qiToFloat n'')

      , testProperty "_qiA" $ \n a' ->
          let n'  = over _qiA (+ a') n
              n'' = runQI n $ \a b c d -> qi (a+a') b c d
          in  approxEq (qiToFloat n') (qiToFloat n'')

      , testProperty "_qiB" $ \n b' ->
          let n'  = over _qiB (+ b') n
              n'' = runQI n $ \a b c d -> qi a (b+b') c d
          in  approxEq (qiToFloat n') (qiToFloat n'')

      , testProperty "_qiC" $ \n (NonNegative c') ->
          let n'  = over _qiC (* c') n
              n'' = runQI n $ \a b c d -> qi a b (c*c') d
          in  approxEq (qiToFloat n') (qiToFloat n'')

      , testProperty "_qiD" $ \n (NonZero d') ->
          let n'  = over _qiD (* d') n
              n'' = runQI n $ \a b c d -> qi a b c (d*d')
          in  approxEq (qiToFloat n') (qiToFloat n'')
      ]
    , testGroup "Numerical operations"
      [ testProperty "qiToFloat" $ \a b (NonNegative c) (NonZero d) ->
          approxEq' (qiToFloat (qi a b c d)) (approxQI a b c d)

      , testProperty "qiAddI" $ \n x ->
          approxEq' (qiToFloat (qiAddI n x)) (qiToFloat n + fromInteger x)

      , testProperty "qiSubI" $ \n x ->
          approxEq' (qiToFloat (qiSubI n x)) (qiToFloat n - fromInteger x)

      , testProperty "qiMulI" $ \n x ->
          approxEq' (qiToFloat (qiMulI n x)) (qiToFloat n * fromInteger x)

      , testProperty "qiDivI" $ \n x ->
          x /= 0 ==>
            approxEq' (qiToFloat (qiDivI n x)) (qiToFloat n / fromInteger x)

      , testProperty "qiAddR" $ \n x ->
          approxEq' (qiToFloat (qiAddR n x)) (qiToFloat n + fromRational x)

      , testProperty "qiSubR" $ \n x ->
          approxEq' (qiToFloat (qiSubR n x)) (qiToFloat n - fromRational x)

      , testProperty "qiMulR" $ \n x ->
          approxEq' (qiToFloat (qiMulR n x)) (qiToFloat n * fromRational x)

      , testProperty "qiDivR" $ \n x ->
          x /= 0 ==>
            approxEq' (qiToFloat (qiDivR n x)) (qiToFloat n / fromRational x)

      , testProperty "qiNegate" $ \n ->
          approxEq' (qiToFloat (qiNegate n)) (negate (qiToFloat n))

      , testProperty "qiRecip" $ \n ->
          not (approxEq (qiToFloat n) 0)
            ==> let ~(Just nr) = qiRecip n
                in  approxEq' (qiToFloat nr) (recip (qiToFloat n))

      , testProperty "qiAdd" . withCompatiblePair $ \n n' ->
          let ~(Just r) = qiAdd n n'
          in  approxEq' (qiToFloat r) (qiToFloat n + qiToFloat n')

      , testProperty "qiSub" . withCompatiblePair $ \n n' ->
          let ~(Just r) = qiSub n n'
          in  approxEq' (qiToFloat r) (qiToFloat n - qiToFloat n')

      , testProperty "qiMul" . withCompatiblePair $ \n n' ->
          let ~(Just r) = qiMul n n'
          in  approxEq' (qiToFloat r) (qiToFloat n * qiToFloat n')

      , testProperty "qiDiv" . withCompatiblePair $ \n n' ->
          let ~(Just r) = qiDiv n n'
          in  not (approxEq (qiToFloat n') 0)
                ==> approxEq' (qiToFloat r) (qiToFloat n / qiToFloat n')

      , testProperty "qiPow" $ \n (NonNegative p) ->
          -- Limit the power for speed.
          (p <= 10) ==>
            approxEq' (qiToFloat (qiPow n p))
                      -- CReal seems to diverge in 0 ** 1, use (^).
                      (qiToFloat n ^ p)

      , testProperty "qiFloor" $ \n ->
          qiFloor n === floor (qiToFloat n :: RefFloat)
      ]
    , testGroup "Continued fractions"
      [ testProperty "qiToContinuedFraction/continuedFractionToQI" $ \n ->
          let cf  = qiToContinuedFraction n
              len = case cf of
                      (_, NonCyc _)   -> 0
                      (_, Cyc _ _ xs) -> length xs
          -- Limit the length of the periodic part for speed.
          in (len <= 100) ==>
               approxEq' (qiToFloat n) (qiToFloat (continuedFractionToQI cf))
      ]
    ]

withCompatiblePair :: Testable p
                   => (QI -> QI -> p) -> QI -> QI -> Bool -> Bool -> Property
withCompatiblePair f n0_ n1_ c0Zero c1Zero =
  counterexample ("n0 = " ++ show n0) . counterexample ("n1 = " ++ show n1) $
    f n0 n1
  where
    n0 = runQI n0_ $ \a b c d ->
           qi a b (if c0Zero then 0 else c) d

    n1 = runQI n0_ $ \_ _ c _ -> runQI n1_ $ \a b _ d ->
           qi a b (if c1Zero then 0 else c) d

approxQI :: Integer -> Integer -> Integer -> Integer -> RefFloat
approxQI a b c d =
  (fromInteger a + fromInteger b * sqrt (fromInteger c)) / fromInteger d

approxQI' :: Rational -> Rational -> Integer -> RefFloat
approxQI' a b c =
  fromRational a + fromRational b * sqrt (fromInteger c)

approxEq :: RefFloat -> RefFloat -> Bool
approxEq a b = abs (b - a) < 1e-6 * maximum [ 1, abs a, abs b ]

approxEq' :: RefFloat -> RefFloat -> Property
approxEq' a b =
  counterexample (show a ++ " is not approximately " ++ show b ++ " (diff = "
                   ++ show (abs (b - a)) ++ ")")
                 (approxEq a b)