content: module Numeric.Units.Dimensional.DynamicSpec where
  
  import Numeric.Units.Dimensional.Prelude
  import Numeric.Units.Dimensional.Dynamic hiding ((*),(/),(^),(*~),(/~), recip)
  import Numeric.Units.Dimensional.Dimensions.TermLevel (hasSomeDimension)
  import qualified Numeric.Units.Dimensional.Dynamic as Dyn
  import qualified Prelude as P
  import Test.Hspec
  import Test.QuickCheck
  
  spec :: Spec
  spec = do
           describe "Dynamic quantity promotion and demotion" $ do
             it "round-trips through AnyQuantity" $ property $
               \x -> let x' = x *~ kilo newton
                         x'' = demoteQuantity x' :: AnyQuantity Double
                      in Just x' == promoteQuantity x''
             it "round-trips through DynQuantity" $ property $
               \x -> let x' = x *~ micro watt
                         x'' = demoteQuantity x' :: DynQuantity Rational
                      in Just x' == promoteQuantity x''
             it "round-trips through AnyQuantity then DynQuantity" $ property $
               \x -> let x' = x *~ gram
                         x'' = demoteQuantity x' :: AnyQuantity Double
                         x''' = demoteQuantity x'' :: DynQuantity Double
                      in Just x' == promoteQuantity x'''
             it "doesn't promote invalid quantities" $ do
               (promoteQuantity invalidQuantity :: Maybe (Length Double)) `shouldBe` Nothing
             it "doesn't promote AnyQuantity to the wrong dimension" $ do
               let x = 12.3 *~ meter
                   x' = demoteQuantity x :: AnyQuantity Double
               (promoteQuantity x' :: Maybe (Mass Double)) `shouldBe` Nothing
             it "doesn't promote DynQuantity to the wrong dimension" $ do
               let x = 12.3 *~ mole
                   x' = demoteQuantity x :: DynQuantity Double
               (promoteQuantity x' :: Maybe (Time Double)) `shouldBe` Nothing
             it "properly combines with dynamic units" $ do
               let meter' = demoteUnit' meter
               (promoteQuantity (139.4 Dyn.*~ meter' :: AnyQuantity Double)) `shouldBe` Just (139.4 *~ meter)
             it "properly eliminates dynamic units" $ do
               let ampere' = demoteUnit' ampere
                   i = demoteQuantity $ 47 *~ ampere :: AnyQuantity Double
               i Dyn./~ ampere' `shouldBe` Just 47
             it "doesn't eliminate dynamic units of the wrong dimension" $ do
               let ampere' = demoteUnit' ampere
                   i = demoteQuantity $ 47 *~ joule :: AnyQuantity Double
               i Dyn./~ ampere' `shouldBe` Nothing
           describe "DynQuantity arithmetic" $ do
             -- declare some static quantities and their dynamic counterparts for arithmetic tests
             let x1 = 12.3 *~ meter
                 x2 = (-7.9) *~ meter
                 a = 93 *~ square (kilo meter)
                 m = 147 *~ kilo gram
                 t = 14.9 *~ second
                 f = 87.2 *~ milli newton
                 phi = 1.61803398875 *~ one
                 x1' = demoteQuantity x1 :: DynQuantity Double
                 x2' = demoteQuantity x2 :: DynQuantity Double
                 a' = demoteQuantity a :: DynQuantity Double
                 m' = demoteQuantity m :: DynQuantity Double
                 t' = demoteQuantity t :: DynQuantity Double
                 f' = demoteQuantity f :: DynQuantity Double
                 phi' = demoteQuantity phi :: DynQuantity Double
             context "Num instance" $ do
               it "matches static addition" $ do
                 (x1' P.+ x2') `shouldBe` demoteQuantity (x1 + x2)
               it "allows addition with polydimensional zero" $ do
                 (t' P.+ polydimensionalZero) `shouldBe` t'
                 (polydimensionalZero P.+ t') `shouldBe` t'
                 (polydimensionalZero P.+ polydimensionalZero) `shouldBe` (polydimensionalZero :: DynQuantity Double)               
               it "propagates witnesses to zero during addition" $ do
                 -- We want to test that the witness for polymorphic zero was actually added to the other addend.
                 -- The reason for this property is that if the other addend is some element of the underlying type
                 -- which can't act as a divisor (such as a propagating nAn), then we want that information to still
                 -- be around when we go to promote the result.
                 let nan = 0 P./ 0 :: Double
                     x = demoteQuantity $ nan *~ meter
                     Just y = promoteQuantity (polydimensionalZero P.+ x) :: Maybe (Length Double)
                 (y /~ meter) `shouldSatisfy` P.isNaN
               it "matches static subtraction" $ do
                 (x2' P.- x1') `shouldBe` demoteQuantity (x2 - x1)
               it "allows subtraction with polydimensional zero" $ do
                 (m' P.- polydimensionalZero) `shouldBe` m'
                 (polydimensionalZero P.- m') `shouldBe` (P.negate m')
                 (polydimensionalZero P.- polydimensionalZero) `shouldBe` (polydimensionalZero :: DynQuantity Double)               
               it "matches static multiplication" $ do
                 promoteQuantity (x1' P.* f') `shouldBe` Just (x1 * f)
               it "allows multiplication with polydimensional zero" $ do
                 (f' P.* polydimensionalZero) `shouldBe` polydimensionalZero
                 (polydimensionalZero P.* m') `shouldBe` polydimensionalZero
                 (polydimensionalZero P.* polydimensionalZero) `shouldBe` (polydimensionalZero :: DynQuantity Double)
               it "matches static negation" $ do
                 (P.negate m') `shouldBe` demoteQuantity (negate m)
               it "negates polydimensional zero" $ do
                 (P.negate polydimensionalZero) `shouldBe` (polydimensionalZero :: DynQuantity Double)
               it "matches static absolute value" $ do
                 (P.abs x2') `shouldBe` demoteQuantity (abs x2)
               it "takes absolute value of polydimensional zero" $ do
                 (P.abs polydimensionalZero) `shouldBe` (polydimensionalZero :: DynQuantity Double)
               it "matches static signum" $ do
                 (P.signum x1') `shouldBe` demoteQuantity (signum x1)
                 (P.signum x2') `shouldBe` demoteQuantity (signum x2)
               it "takes signum of polydimensional zero" $ do
                 (P.signum polydimensionalZero) `shouldBe` demoteQuantity (_0 :: Dimensionless Double)
               it "implements fromInteger with dimensionless result" $ do
                 (P.fromInteger 7 :: DynQuantity Double) `shouldBe` demoteQuantity _7
             context "Fractional instance" $ do
               it "matches static division" $ do
                 ((f' P.* x1') P./ t') `shouldBe` demoteQuantity ((f * x1) / t)
               it "matches static reciprocal" $ do
                 (P.recip m') `shouldBe` demoteQuantity (recip m)
               it "implements fromRational with dimensionless result" $ do
                 let pi' = 22 P./ 7 :: Rational
                 (P.fromRational pi' :: DynQuantity Rational) `shouldBe` demoteQuantity (pi' *~ one)
               it "permits polydimensional zero as a dividend" $ do
                 (polydimensionalZero P./ m') `shouldBe` polydimensionalZero
               it "propagates witnesses to zero during division" $ do
                 -- We want to test that the witness for polymorphic zero was actually divided by the divisor.
                 -- The reason for this property is that if the divisor is itself zero (but not polydimensionalZero),
                 -- or some other element of the underlying type which can't act as a divisor (such as a propagating nAn),
                 -- then we want that information to still be around when we go to promote the result.
                 let nan = 0 P./ 0 :: Double
                     x = demoteQuantity $ nan *~ meter
                     y = polydimensionalZero P./ x
                     Just y' = promoteQuantity y :: Maybe (Length Double)
                 (y' /~ meter) `shouldSatisfy` P.isNaN
             context "Floating instance" $ do
               it "implements dimensionless pi" $ do
                 (P.pi :: DynQuantity Double) `shouldBe` demoteQuantity pi
               it "implements dimensionless sin" $ do
                 -- this will serve as a test for all the single-argument dimensionless functions
                 (P.sin phi') `shouldBe` demoteQuantity (sin phi)
               it "rejects non-dimensionless arguments to sin" $ do
                 (P.sin m') `shouldBe` invalidQuantity
               it "implements dimensionless sin of polydimensional zero" $ do
                 (P.sin polydimensionalZero) `shouldBe` (0 :: DynQuantity Double)
               it "matches static square root" $ do
                 (P.sqrt a') `shouldBe` demoteQuantity (sqrt a)
               it "rejects arguments to square root with non-square dimensions" $ do
                 (P.sqrt f') `shouldNotSatisfy` hasSomeDimension
               it "takes the square root of polydimensional zero" $ do
                 (P.sqrt polydimensionalZero) `shouldBe` (polydimensionalZero :: DynQuantity Double)
               it "matches static dimensionless exponentiation" $ do
                 (phi' P.** phi') `shouldBe` demoteQuantity (phi ** phi)
               it "rejects non-dimensionless arguments to dimensionless exponentiation" $ do
                 (phi' P.** m') `shouldNotSatisfy` hasSomeDimension
                 (x1' P.** phi') `shouldNotSatisfy` hasSomeDimension
               it "matches static logBase" $ do
                 (P.logBase 10 phi') `shouldBe` demoteQuantity (logBase (10 *~ one) phi)
               it "rejects non-dimensionless arguments to logBase" $ do
                 (P.logBase 10 x1') `shouldNotSatisfy` hasSomeDimension
                 (P.logBase x1' 10) `shouldNotSatisfy` hasSomeDimension
           describe "Dynamic units" $ do
             describe "Promotion and demotion" $ do
               return ()
             describe "Arithmetic" $ do
               return ()
  