packages feed

penny-0.32.0.2: tests/Lincoln.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE TemplateHaskell, GeneralizedNewtypeDeriving #-}

module Lincoln where

import Control.Applicative ((<$>), (<*>))
import Control.Arrow (first)
import Control.Monad (liftM2, liftM5, liftM4, liftM3, replicateM, guard)
import Data.List (foldl1')
import Data.Maybe (isJust, isNothing, catMaybes)
import qualified Data.List.NonEmpty as NE
import Data.List.NonEmpty (NonEmpty((:|)))
import Data.Monoid (mempty, (<>))
import qualified Data.Time as T
import qualified Test.QuickCheck as Q
import qualified Test.QuickCheck.Gen as QG
import qualified Test.QuickCheck.Property as QCP
import qualified Test.QuickCheck.All as A
import Test.QuickCheck (Gen, Arbitrary, arbitrary, (==>))
import qualified Penny.Lincoln as L
import Penny.Lincoln.Equivalent ((==~))
import Data.Text (Text)
import qualified Data.Text as X
import System.Random.Shuffle (shuffle')
import qualified Data.Sums as Su

--
-- # Qty
--

failMsg :: Monad m => String -> m a
failMsg s = fail $ s ++ ": generation failed"

-- | Generates Qty with the exponent restricted to a reasonable
-- size. Currently this means it is between 0 and 5, inclusive. Big
-- significands are not a problem, but big exponents quickly make the
-- tests practically un-runnable.
genReasonableExp :: Gen L.Qty
genReasonableExp = do
  m <- Q.suchThat Q.arbitrarySizedBoundedIntegral (> (0 :: Int))
  p <- Q.choose (0 :: Int, 5)
  maybe (failMsg "genSmallSized") return
    $ L.newQty (fromIntegral m) (fromIntegral p)

maxExponent :: Integer
maxExponent = 5

genExponent :: Gen Integer
genExponent = Q.choose (0, maxExponent)

-- | Mutates a Qty so that it is equivalent, but possibly with a
-- different significand and exponent.
genEquivalent :: L.Qty -> Gen L.Qty
genEquivalent q = do
  let (m, p) = (L.signif q, L.places q)
  expo <- genExponent
  let m' = m * (10 ^ expo)
      p' = p + (fromIntegral expo)
  maybe (failMsg "genEquivalent") return $ L.newQty m' p'

-- | Mutates a Qty so that it is not equivalent. Changes either the
-- significand or the exponent or both.
genMutate :: L.Qty -> Gen L.Qty
genMutate q = do
  let (m, p) = (L.signif q, L.places q)
  (changeSignif, changeExp) <-
    Q.suchThat (liftM2 (,) arbitrary arbitrary)
    (/= (False, False))
  m' <- if changeSignif then mutateAtLeast1 m else return m
  p' <- if changeExp then mutateExponent p else return p
  maybe (failMsg "genMutate") return $ L.newQty m' p'

-- | genMutate generates non-equivalent quantities
prop_genMutate :: L.Qty -> Gen Bool
prop_genMutate q = fmap f $ genMutate q
  where
    f q' = not $ q ==~ q'

-- | genEquivalent generates equivalent quantities
prop_genEquivalent :: L.Qty -> Gen Bool
prop_genEquivalent q = fmap f $ genEquivalent q
  where
    f q' = q ==~ q'

-- | Mutates an Integer.  The result is always at least one.
mutateAtLeast1 :: Integer -> Gen Integer
mutateAtLeast1 i =
  fmap fromIntegral $ Q.suchThat Q.arbitrarySizedBoundedIntegral pdct
  where
    pdct = if i > (fromIntegral (maxBound :: Int))
              || i < (fromIntegral (minBound :: Int))
           then (>= (1 :: Int))
           else (\r -> r >= 1 && r /= (fromIntegral i))

-- | Mutates an Integer. The result is always at least zero.
mutateExponent :: Integer -> Gen Integer
mutateExponent i = Q.suchThat (Q.choose (0, maxExponent)) (/= i)

-- | Generates one, with different exponents.
genOne :: Gen L.Qty
genOne = do
  p <- Q.choose (0, maxExponent)
  maybe (failMsg "genOne") return $ L.newQty (1 * 10 ^ p) p

-- | Chooses one of 'genSized' or 'genRangeInt' or 'genSmallExp'.
instance Arbitrary L.Qty where
  arbitrary = Q.oneof [ genReasonableExp ]

-- | Significands are always greater than zero.
prop_significand :: L.Qty -> Bool
prop_significand q = L.signif q > 0

-- | Exponent is always at least zero
prop_exponent :: L.Qty -> Bool
prop_exponent q = L.places q >= 0

-- | newQty passes if exponent is at least zero and if significand is
-- greater than zero.

prop_newQtySucceeds :: L.Signif -> L.Places -> Q.Property
prop_newQtySucceeds m p =
  m > 0 ==> p >= 0 ==> isJust (L.newQty m p)

-- | True if this is a valid Qty; that is, the significand is greater
-- than 0 and the number of places is greater than or equal to 0.
validQty :: L.Qty -> Bool
validQty q = L.signif q > 0 && L.places q >= 0


maxSizeList :: Arbitrary a => Int -> Gen [a]
maxSizeList i = Q.sized $ \s -> do
  len <- Q.choose (0, min s i)
  Q.vector len

-- | Changing a Qty to a QtyRep and back gives the same Qty.
prop_qtyToRep
  :: Su.S3 L.Radix L.PeriodGrp L.CommaGrp
  -> L.Qty
  -> Bool
prop_qtyToRep s q = L.toQty (L.qtyToRep s q) == q

-- | Generates a group of balanced quantities.
genBalQtys :: Gen (L.Qty, [L.Qty], [L.Qty])
genBalQtys = maxSize 5 $ do
  total <- arbitrary
  group1alloc1 <- arbitrary
  group1allocRest <- arbitrary
  group2alloc1 <- arbitrary
  group2allocRest <- arbitrary
  let (g1r1, g1rs) = L.allocate total (group1alloc1, group1allocRest)
      (g2r1, g2rs) = L.allocate total (group2alloc1, group2allocRest)
  return $ (total, g1r1 : g1rs, g2r1 : g2rs)

-- | genBalQtys generates first qty list that sum up to the given total.
prop_genBalQtysTotalX :: Q.Property
prop_genBalQtysTotalX = Q.forAll genBalQtys $ \(tot, g1, _) ->
  let sx = foldl1 L.add g1
  in if sx ==~ tot
     then QCP.succeeded
     else let r = "planned sum: " ++ show tot ++ " actual sum: "
                  ++ show sx
          in QCP.failed { QCP.reason = r }

-- | genBalQtys generates a balanced group of quantities.
prop_genBalQtys :: Q.Property
prop_genBalQtys = Q.forAll genBalQtys $ \(tot, g1, g2) ->
  case (g1, g2) of
    (x:xs, y:ys) ->
      let sx = foldl1' L.add (x:xs)
          sy = foldl1' L.add (y:ys)
      in if sx ==~ sy
         then QCP.succeeded
         else let r = "Different sums. X sum: " ++ show sx
                      ++ " Y sum: " ++ show sy ++
                      " planned total: " ++ show tot
              in QCP.failed { QCP.reason = r }
    _ -> QCP.failed { QCP.reason = "empty quantities list" }

-- | > x + y == y + x

prop_commutative :: L.Qty -> L.Qty -> Bool
prop_commutative q1 q2 = q1 `L.add` q2 == q2 `L.add` q1

-- | Adding q2 to q1 and then taking the difference of q2 gives a
-- LeftBiggerBy q1

prop_addSubtract :: L.Qty -> L.Qty -> Bool
prop_addSubtract q1 q2 =
  let diff = (q1 `L.add` q2) `L.difference` q2
  in case diff of
      L.LeftBiggerBy d -> d ==~ q1
      _ -> False

-- | add generates valid Qtys
prop_addValid :: L.Qty -> L.Qty -> Bool
prop_addValid q1 q2 = validQty $ q1 `L.add` q2

-- | mult generates valid Qtys
prop_multValid :: L.Qty -> L.Qty -> Bool
prop_multValid q1 q2 = validQty $ q1 `L.mult` q2

newtype One = One { unOne :: L.Qty }
  deriving (Eq, Show)

instance Arbitrary One where arbitrary = fmap One genOne

-- | genOne generates valid Qtys
prop_genOneValid :: One -> Bool
prop_genOneValid = validQty . unOne

-- | (x `mult` 1) `equivalent` x
prop_multIdentity :: L.Qty -> One -> Bool
prop_multIdentity x (One q1) = (x `L.mult` q1) ==~ x

-- | newQty fails if significand is less than one
prop_newQtyBadSignificand :: L.Signif -> L.Places -> Q.Property
prop_newQtyBadSignificand m p =
  m < 1 ==> isNothing (L.newQty m p)

-- | newQty fails if places is less than zero
prop_newQtyBadPlaces :: L.Signif -> L.Places -> Q.Property
prop_newQtyBadPlaces m p =
  m < 0 ==> isNothing (L.newQty m p)

-- | difference returns valid L.Qty
prop_differenceValid :: L.Qty -> L.Qty -> Bool
prop_differenceValid q1 q2 = case L.difference q1 q2 of
  L.LeftBiggerBy r -> validQty r
  L.RightBiggerBy r -> validQty r
  L.Equal -> True

-- | allocate returns valid Qty
prop_allocateValid :: L.Qty -> (L.Qty, [L.Qty]) -> Bool
prop_allocateValid q1 q2 =
  let (r1, r2) = L.allocate q1 q2
  in validQty r1 && all validQty r2

-- | 'equivalent' fails on different Qty
prop_genNotEquivalent :: L.Qty -> Gen Bool
prop_genNotEquivalent q1 = do
  q2 <- genMutate q1
  return . not $ q1 ==~ q2

-- | newQty succeeds and fails as it should, and generates valid Qty
prop_newQty :: L.Signif -> L.Places -> Bool
prop_newQty m p = case (m > 0, p >= 0) of
  (True, True) -> case L.newQty m p of
    Nothing -> False
    Just q -> L.signif q == m && L.places q == p
  _ -> isNothing (L.newQty m p)

-- | Sum of allocation adds up to original Qty

prop_sumAllocate :: L.Qty -> (L.Qty, [L.Qty]) -> Bool
prop_sumAllocate tot ls =
  let (r1, rs) = L.allocate tot ls
  in foldl1' L.add (r1:rs) ==~ tot

-- | Number of allocations is same as number requested

prop_numAllocate :: L.Qty -> (L.Qty, [L.Qty]) -> Bool
prop_numAllocate tot ls =
  let (_, rs) = L.allocate tot ls
  in length rs == length (snd ls)

-- | Sum of largest remainder method is equal to total number of seats
prop_sumLargestRemainder
  :: Q.Positive Integer
  -> Q.NonEmptyList (Q.NonNegative Integer)
  -> QCP.Property

prop_sumLargestRemainder tot ls =
  let t = Q.getPositive tot
      l = map Q.getNonNegative . Q.getNonEmpty $ ls
      r = L.largestRemainderMethod t l
  in sum l > 0 ==> sum r == t

--
-- # QtyRep
--

instance Arbitrary L.Digit where
  arbitrary = Q.elements [minBound..maxBound]

instance Arbitrary L.PeriodGrp where
  arbitrary = Q.elements [minBound..maxBound]

instance Arbitrary L.CommaGrp where
  arbitrary = Q.elements [minBound..maxBound]

instance Arbitrary a => Arbitrary (NonEmpty a) where
  arbitrary = (:|) <$> arbitrary <*> arbitrary

instance Arbitrary L.DigitList where
  arbitrary = fmap L.DigitList arbitrary

instance Arbitrary a => Arbitrary (L.GroupedDigits a) where
  arbitrary = L.GroupedDigits <$> arbitrary <*> arbitrary

digitsHasNonZero :: L.Digits a => a -> Bool
digitsHasNonZero = any (/= L.D0) . NE.toList . L.unDigitList . L.digits

instance (Arbitrary a, L.Digits a) => Arbitrary (L.WholeFrac a) where
  arbitrary = do
    let hasNonZero (x, y) = digitsHasNonZero x || digitsHasNonZero y
    (w, f) <- arbitrary `Q.suchThat` hasNonZero
    case L.wholeFrac w f of
      Nothing -> error "failed to generate WholeFrac"
      Just wf -> return wf

instance (Arbitrary a, L.Digits a) => Arbitrary (L.WholeOnly a) where
  arbitrary = do
    w <- arbitrary `Q.suchThat` digitsHasNonZero
    case L.wholeOnly w of
      Nothing -> error "failed to generate WholeOnly"
      Just x -> return x

instance (Arbitrary a, L.Digits a) => Arbitrary (L.WholeOrFrac a) where
  arbitrary = fmap L.WholeOrFrac arbitrary

instance Arbitrary L.Radix where
  arbitrary = Q.elements [minBound..maxBound]

instance Arbitrary L.QtyRep where
  arbitrary = Q.oneof [grp, noGrp]
    where
      grp = liftM2 L.QNoGrouping arbitrary arbitrary
      noGrp = fmap L.QGrouped arbitrary

instance (Arbitrary a, Arbitrary b, Arbitrary c)
         => Arbitrary (Su.S3 a b c) where
  arbitrary = Q.oneof [ fmap Su.S3a arbitrary
                      , fmap Su.S3b arbitrary
                      , fmap Su.S3c arbitrary ]

--
-- # DateTime
--

instance Arbitrary L.TimeZoneOffset where
  arbitrary = Q.choose (-840, 840)
    >>= maybe (failMsg "timeZoneOffset") return . L.minsToOffset

instance Arbitrary L.Hours where
  arbitrary = Q.choose (0, 23)
    >>= maybe (failMsg "hours") return . L.intToHours

instance Arbitrary L.Minutes where
  arbitrary = Q.choose (0, 59)
    >>= maybe (failMsg "minutes") return . L.intToMinutes

instance Arbitrary L.Seconds where
  arbitrary = Q.choose (0, 60)
    >>= maybe (failMsg "seconds") return . L.intToSeconds

genDay :: Q.Gen T.Day
genDay = fmap T.ModifiedJulianDay $ Q.choose (b, e)
  where
    b = T.toModifiedJulianDay $ T.fromGregorian 1000 01 01
    e = T.toModifiedJulianDay $ T.fromGregorian 3000 01 01

instance Arbitrary L.DateTime where
  arbitrary = liftM5 L.DateTime genDay
    arbitrary arbitrary arbitrary arbitrary

--
-- # Open
--

maxSize :: Int -> Gen a -> Gen a
maxSize i g = Q.sized $ \s -> Q.resize (min i s) g

-- | Generates a Text from valid Unicode chars.
genText :: Gen Text
genText = maxSize 5
  $ fmap X.pack $ Q.oneof [ Q.listOf ascii, Q.listOf rest ]
  where
    ascii = Q.choose (toEnum 32, toEnum 126)
    rest = Q.suchThat (Q.choose (minBound, maxBound))
                       (\c -> c < '\xd800' || c > '\xdfff')

instance Arbitrary L.SubAccount where
  arbitrary = fmap L.SubAccount genText

instance Arbitrary L.Account where
  arbitrary = fmap L.Account arbitrary

instance Arbitrary q => Arbitrary (L.Amount q) where
  arbitrary = liftM2 L.Amount arbitrary arbitrary

instance Arbitrary L.Commodity where
  arbitrary = fmap L.Commodity genText

instance Arbitrary L.DrCr where
  arbitrary = Q.elements [L.Debit, L.Credit]

instance Arbitrary q => Arbitrary (L.Entry q) where
  arbitrary = liftM2 L.Entry arbitrary arbitrary

instance Arbitrary L.Flag where
  arbitrary = fmap L.Flag genText

instance Arbitrary L.Memo where
  arbitrary = fmap L.Memo $ Q.listOf genText

instance Arbitrary L.Number where
  arbitrary = fmap L.Number genText

instance Arbitrary L.Payee where
  arbitrary = fmap L.Payee genText

instance Arbitrary L.Tag where
  arbitrary = fmap L.Tag genText

instance Arbitrary L.Tags where
  arbitrary = fmap L.Tags $ Q.listOf arbitrary

instance Arbitrary L.TopLineLine where
  arbitrary = fmap L.TopLineLine Q.arbitrarySizedBoundedIntegral

instance Arbitrary L.TopMemoLine where
  arbitrary = fmap L.TopMemoLine Q.arbitrarySizedBoundedIntegral

instance Arbitrary L.Side where
  arbitrary = Q.elements [L.CommodityOnLeft, L.CommodityOnRight]

instance Arbitrary L.SpaceBetween where
  arbitrary = Q.elements [L.SpaceBetween, L.NoSpaceBetween]

instance Arbitrary L.Filename where
  arbitrary = fmap L.Filename genText

instance Arbitrary L.PriceLine where
  arbitrary = fmap L.PriceLine Q.arbitrarySizedBoundedIntegral

instance Arbitrary L.PostingLine where
  arbitrary = fmap L.PostingLine Q.arbitrarySizedBoundedIntegral

instance Arbitrary L.GlobalPosting where
  arbitrary = fmap L.GlobalPosting arbitrary

instance Arbitrary L.FilePosting where
  arbitrary = fmap L.FilePosting arbitrary

instance Arbitrary L.GlobalTransaction where
  arbitrary = fmap L.GlobalTransaction arbitrary

instance Arbitrary L.FileTransaction where
  arbitrary = fmap L.FileTransaction arbitrary

instance Arbitrary L.Serial where
  arbitrary = do
    ls <- Q.listOf1 (return ())
    let sers = L.serialItems const ls
    fmap head $ shuffle sers

-- | Shuffles a list.
shuffle :: [a] -> Gen [a]
shuffle ls = QG.MkGen $ \g _ ->
  shuffle' ls (length ls) g

--
-- # Ents
--

-- | Generates restricted ents
genRestricted :: Arbitrary a => Gen (L.Ents a)
genRestricted = liftM5 L.rEnts arbitrary arbitrary arbitrary
                arbitrary arbitrary

-- | Generates a group of balanced entries.
genBalEntries :: Gen ([L.Entry L.Qty])
genBalEntries = do
  (_, qDeb, qCred) <- genBalQtys
  let qtysAndDrCrs = map (\en -> (L.Debit, en)) qDeb
                     ++ map (\en -> (L.Credit, en)) qCred
  cty <- arbitrary
  let mkEn (drCr, qty) = L.Entry drCr (L.Amount qty cty)
  shuffle $ map mkEn qtysAndDrCrs

newtype BalEntries = BalEntries
  { unBalEntries :: [L.Entry L.Qty] }
  deriving (Eq, Show)

instance Arbitrary BalEntries where
  arbitrary = fmap BalEntries genBalEntries

type Inferred = Bool

-- | Generates a list of entries. At most, one of these is Inferred.
genEntriesWithInfer :: Gen [(L.Entry L.Qty, Inferred)]
genEntriesWithInfer = do
  nGroups <- Q.suchThat Q.arbitrarySizedIntegral (> 0)
  entries <- fmap concat $ replicateM nGroups genBalEntries
  makeNothing <- arbitrary
  let entries' = if makeNothing
        then (head entries, True)
             : map (\en -> (en, False)) (tail entries)
        else map (\en -> (en, False)) entries
  shuffle entries'


-- | Gets a single inferred entry from a balance, if possible.
inferredVal :: [Maybe (L.Entry L.Qty)] -> Maybe (L.Entry L.Qty)
inferredVal ls = do
  guard ((length . filter id . map isNothing $ ls) == 1)
  case L.entriesToBalanced . catMaybes $ ls of
    L.Inferable e -> Just e
    _ -> Nothing

-- | genEntriesWithInfer is inferable
prop_genEntries :: Q.Property
prop_genEntries = Q.forAll genEntriesWithInfer $
  \ps -> True `elem` (map snd ps)
         ==> isJust (inferredVal (map toEn ps))
  where
    toEn (en, inf) = if inf then Nothing else Just en

-- | genBalEntries generates groups that are balanced.
prop_balEntries :: BalEntries -> Bool
prop_balEntries
  = (== L.Balanced)
  . L.entriesToBalanced
  . unBalEntries

-- | 'views' gives as many views as there were postings

prop_numViews :: L.Ents m -> Bool
prop_numViews t = (length . L.views $ t) == (length . L.unEnts $ t)

newtype NonRestricted a = NonRestricted
  { unNonRestricted :: [(Maybe (L.Entry L.Qty), a)] }
  deriving (Eq, Show)

instance Arbitrary a => Arbitrary (NonRestricted a) where
  arbitrary = do
    ls <- genEntriesWithInfer
    metas <- Q.vector (length ls)
    let mkPair (en, inf) mt = if inf
          then (Nothing, mt) else (Just en, mt)
    return . NonRestricted $ zipWith mkPair ls metas

genNonRestricted :: Arbitrary a => Gen (L.Ents a)
genNonRestricted =
  arbitrary
  >>= maybe (failMsg "genNonRestricted") return
      . L.ents
      . map (first (fmap Right))
      . unNonRestricted

instance Arbitrary a => Arbitrary (L.Ents a) where
  arbitrary = Q.oneof [ genNonRestricted
                       , genRestricted ]

-- | Ents always have at least two postings
prop_twoPostings :: L.Ents a -> Bool
prop_twoPostings e = length (L.unEnts e) > 1

-- | Ents are always balanced
prop_balanced :: L.Ents a -> Bool
prop_balanced
  = (== L.Balanced)
  . L.entriesToBalanced
  . map (either (fmap L.toQty) id)
  . map L.entry
  . L.unEnts

-- | Ents contain no more than one inferred posting
prop_inferred :: L.Ents a -> Bool
prop_inferred t =
  (length . filter id . map L.inferred . L.unEnts $ t)
  < 2

newtype BalQtys = BalQtys { _unBalQtys :: ([L.Qty], [L.Qty]) }
  deriving (Eq, Show)

-- | 'ents' makes ents as it should. Also tests whether
-- the 'Arbitrary' instance of 'NonRestricted' is behaving as it
-- should.

prop_ents :: NonRestricted a -> Bool
prop_ents (NonRestricted ls)
  = isJust
  . L.ents
  . map (first (fmap Right))
  $ ls

-- | NonRestricted makes ents with two postings
prop_entsTwoPostings :: NonRestricted a -> Bool
prop_entsTwoPostings (NonRestricted ls)
  = case L.ents . map (first (fmap Right)) $ ls of
      Nothing -> False
      Just t -> prop_twoPostings t

-- | 'rEnts' behaves as it should

prop_rEnts
  :: L.Commodity
  -> L.DrCr
  -> (L.Qty, a)
  -> [(L.Qty, a)]
  -> a
  -> Bool
prop_rEnts c dc pr ls mt =
  let t = L.rEnts c dc (first Right pr) (map (first Right) ls) mt
  in prop_twoPostings t && prop_balanced t && prop_inferred t

-- Testing that 'ents' fails when it should

-- | Generates a group of entries that are not balanced or inferable
genNotInferable :: Arbitrary a => Gen [(Maybe (L.Entry L.Qty), a)]
genNotInferable = QG.suchThat gen notInf
  where
    notInf ls =
      let bal = L.entriesToBalanced
                . catMaybes
                . map fst
                $ ls
      in bal == L.NotInferable
    gen = QG.listOf $ (,) <$> arbitrary <*> arbitrary


newtype NotInferable a = NotInferable
  { unNotBalanced :: [(Maybe (L.Entry L.Qty), a)] }
  deriving (Eq, Show)

instance Arbitrary a => Arbitrary (NotInferable a) where
  arbitrary = NotInferable <$> genNotInferable

-- | 'ents' fails when given non-inferable entries
prop_entsNonInferable :: Arbitrary a => NotInferable a -> Bool
prop_entsNonInferable (NotInferable ls) =
  isNothing . L.ents . map (first (fmap Right)) $ ls

--
-- # Price
--

instance Arbitrary L.From where
  arbitrary = fmap L.From arbitrary
instance Arbitrary L.To where
  arbitrary = fmap L.To arbitrary
instance Arbitrary L.CountPerUnit where
  arbitrary = fmap L.CountPerUnit arbitrary

instance Arbitrary L.Price where
  arbitrary = do
    (f, t) <- Q.suchThat arbitrary (\(f, t) -> L.unFrom f /= L.unTo t)
    c <- arbitrary
    maybe (failMsg "price") return $ L.newPrice f t c

-- | All Prices have from and to commodities that are different.
prop_price :: L.Price -> Bool
prop_price p = (L.unFrom . L.from $ p)
               /= (L.unTo . L.to $ p)

-- | newPrice succeeds if From and To are different
prop_newPriceDifferent :: L.CountPerUnit -> Q.Property
prop_newPriceDifferent cpu =
  Q.forAll (Q.suchThat arbitrary (\(L.From f, L.To t) -> f /= t)) $
  \(f, t) -> isJust (L.newPrice f t cpu)

-- | newPrice fails if From and To are the same
prop_newPriceSame :: L.From -> L.CountPerUnit -> Bool
prop_newPriceSame (L.From fr) cpu =
  isNothing (L.newPrice (L.From fr) (L.To fr) cpu)

--
-- # Bits
--
instance Arbitrary L.PricePoint where
  arbitrary = liftM5 L.PricePoint arbitrary arbitrary arbitrary
              arbitrary arbitrary

instance Arbitrary L.TopLineData where
  arbitrary = liftM3 L.TopLineData arbitrary arbitrary arbitrary

instance Arbitrary L.TopLineCore where
  arbitrary = liftM5 L.TopLineCore arbitrary arbitrary arbitrary
              arbitrary arbitrary

instance Arbitrary L.TopLineFileMeta where
  arbitrary = liftM4 L.TopLineFileMeta arbitrary arbitrary arbitrary
              arbitrary

instance Arbitrary L.PostingCore where
  arbitrary = L.PostingCore <$> arbitrary <*> arbitrary <*> arbitrary
              <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
              <*> arbitrary

instance Arbitrary L.PostingFileMeta where
  arbitrary = liftM2 L.PostingFileMeta arbitrary arbitrary

instance Arbitrary L.PostingData where
  arbitrary = liftM3 L.PostingData arbitrary arbitrary arbitrary

--
-- # Balance
--

instance Arbitrary L.Balance where
  arbitrary = L.entryToBalance <$> (arbitrary :: Gen (L.Entry L.Qty))

-- | Adding Balances is commutative
prop_addBalancesCommutative :: L.Balance -> L.Balance -> Bool
prop_addBalancesCommutative x y = (x <> y) == (y <> x)

-- | Adding Balances is associative
prop_addBalancesAssociative
  :: L.Balance -> L.Balance -> L.Balance -> Bool
prop_addBalancesAssociative x y z = (x <> (y <> z)) == ((x <> y) <> z)

-- | A mempty balance behaves as it should
prop_balMempty :: L.Balance -> Bool
prop_balMempty b = (b <> mempty) == b

-- | The Balanced of an empty Balance is always Balanced.
prop_emptyBalance :: QCP.Property
prop_emptyBalance = QCP.once $ L.balanced mempty == L.Balanced

-- | The Balanced of a list of Entry where all the commodities are the
-- same is always Balanced or Inferable.
prop_entriesSameCommodity
  :: [(L.Qty, L.DrCr)]
  -- ^ The Qty and DrCr of each Entry

  -> L.Commodity
  -- ^ Single Commodity for all Entry

  -> Bool

prop_entriesSameCommodity ls cy =
  let mkEntry (qt, dc) = L.Entry dc (L.Amount qt cy)
      entries = map mkEntry ls
  in case L.entriesToBalanced entries of
      L.Balanced -> True
      L.Inferable _ -> True
      _ -> False

-- | Two Commodities that are not the same.
newtype CommodityPair = CommodityPair
  { unCommodityPair :: (L.Commodity, L.Commodity) }
  deriving (Eq, Show)

instance Arbitrary CommodityPair where
  arbitrary =
    CommodityPair <$> QG.suchThat gen (\(c1, c2) -> c1 /= c2)
    where
      gen = (,) <$> arbitrary <*> arbitrary

-- | The Balanced where there is at least one Entry of one commodity
-- and exactly one Entry of another commodity is either Inferable or
-- NotInferable.

prop_entriesTwoCommodities
  :: Q.NonEmptyList (L.Qty, L.DrCr)
  -- ^ Qty and DrCr of the group of Entry that has at least one Entry

  -> (L.Qty, L.DrCr)
  -- ^ Qty and DrCr of the group that has exactly one Entry

  -> CommodityPair

  -> Bool

prop_entriesTwoCommodities (Q.NonEmpty qd1) qd2 cp =
  let mkEntry cy (q, dc) = L.Entry dc (L.Amount q cy)
      g1 = map (mkEntry (fst . unCommodityPair $ cp)) qd1
      g2 = mkEntry (snd . unCommodityPair $ cp) qd2
      balanced = L.entriesToBalanced $ g2:g1
  in case balanced of
      L.Balanced -> False
      _ -> True


-- | Mutates a Commodity.
mutateCommodity :: L.Commodity -> Gen L.Commodity
mutateCommodity (L.Commodity cy) =
  L.Commodity <$> QG.suchThat genText (\c -> c /= cy)



-- | mutateCommodity behaves as it should
prop_mutateCommodity :: L.Commodity -> Gen Bool
prop_mutateCommodity c = do
  c' <- mutateCommodity c
  return $ c /= c'

-- | Mutating the commodity of a balanced group of entries results in
-- an NotInferable balance.
newtype NotInferableFromBalanced = NotInferableFromBalanced
  { unNotInferableFromBalanced :: [L.Entry L.Qty] }
  deriving (Eq, Show)

instance Arbitrary NotInferableFromBalanced where
  arbitrary = do
    BalEntries ls <- arbitrary
    let en = head ls
    cy' <- mutateCommodity . L.commodity . L.amount $ en
    let en' = L.Entry (L.drCr en) (L.Amount (L.qty . L.amount $ en)
                                            cy')
    fmap NotInferableFromBalanced . shuffle $ en' : tail ls


-- | NotInferableFromBalanced behaves as it should
prop_notInferableFromBalanced :: NotInferableFromBalanced -> Bool
prop_notInferableFromBalanced
  = (== L.NotInferable)
  . L.entriesToBalanced
  . unNotInferableFromBalanced

-- | Mutating the DrCr of a Balanced group yields an Inferable.
newtype InferableMutatedDrCr = InferableMutatedDrCr
  { unInferableMutatedDrCr :: [L.Entry L.Qty] }
  deriving (Eq, Show)

instance Arbitrary InferableMutatedDrCr where
  arbitrary = do
    BalEntries ls <- arbitrary
    let en = head ls
        dc' = L.opposite . L.drCr $ en
        en' = L.Entry dc' (L.amount en)
    fmap InferableMutatedDrCr . shuffle $ en' : tail ls

-- | InferableMutatedDrCr behaves as it should
prop_inferableMutatedDrCr :: InferableMutatedDrCr -> Bool
prop_inferableMutatedDrCr
  = L.isInferable
  . L.entriesToBalanced
  . unInferableMutatedDrCr

-- | Mutating the Qty of a Balanced group yields an Inferable.
newtype InferableMutatedQty = InferableMutatedQty
  { unInferableMutatedQty :: [L.Entry L.Qty] }
  deriving (Eq, Show)

instance Arbitrary InferableMutatedQty where
  arbitrary = go `QG.suchThat` prop_inferableMutatedQty
    where
      go = do
        BalEntries ls <- arbitrary
        let en = head ls
            am = L.amount en
            cy = L.commodity am
        q <- genMutate . L.qty $ am
        let en' = L.Entry (L.drCr en) (L.Amount q cy)
        fmap InferableMutatedQty . shuffle $ en' : tail ls

-- | InferableMutatedQty behaves as it should
prop_inferableMutatedQty :: InferableMutatedQty -> Bool
prop_inferableMutatedQty
  = L.isInferable
  . L.entriesToBalanced
  . unInferableMutatedQty

-- | A mix of InferableMutatedQty and InferableMutatedDrCr
newtype InferableGroup = InferableGroup
  { unInferableGroup :: [L.Entry L.Qty] }
  deriving (Eq, Show)

instance Arbitrary InferableGroup where
  arbitrary
    = InferableGroup
    <$> Q.oneof [ fmap unInferableMutatedDrCr arbitrary
                , fmap unInferableMutatedQty arbitrary ]

-- | NotInferable groups, generated at random
newtype NotInferableRandom = NotInferableRandom
  { unNotInferableRandom :: [L.Entry L.Qty] }
  deriving (Eq, Show)

instance Arbitrary NotInferableRandom where
  arbitrary = fmap NotInferableRandom $ Q.suchThat arbitrary pd
    where
      pd = (== L.NotInferable) . L.entriesToBalanced

-- | A mix of NotInferableFromBalanced and NotInferableRandom
newtype NotInferableGroup = NotInferableGroup
  { unNotInferableGroup :: [L.Entry L.Qty] }
  deriving (Eq, Show)

instance Arbitrary NotInferableGroup where
  arbitrary = NotInferableGroup
    <$> Q.oneof [ fmap unNotInferableRandom arbitrary
                , fmap unNotInferableFromBalanced arbitrary ]

-- | Any number of BalEntries is Balanced
prop_balEntriesBalanced :: [BalEntries] -> Bool
prop_balEntriesBalanced
  = (== L.Balanced)
  . L.entriesToBalanced
  . concat
  . map unBalEntries

-- | Any number of BalEntries and one Inferable is Inferable
prop_balEntriesAndInferable :: [BalEntries] -> InferableGroup -> Bool
prop_balEntriesAndInferable bals inf
  = L.isInferable
  . L.entriesToBalanced
  . (++ unInferableGroup inf)
  . concat
  . map unBalEntries
  $ bals

-- | Any number of BalEntries and one NotInferable is not inferable
prop_balEntriesAndNotInferable
  :: [BalEntries] -> NotInferableGroup -> Bool
prop_balEntriesAndNotInferable bals notInf
  = (== L.NotInferable)
  . L.entriesToBalanced
  . (++ unNotInferableGroup notInf)
  . concat
  . map unBalEntries
  $ bals

--
-- # ents fails properly
--

pairWithInts :: [a] -> Gen [(a, Int)]
pairWithInts ls = fmap (zip ls) (Q.vector (length ls))

-- | 'ents' fails when given NonInferableGroup
prop_noEntsNotInferableGroup
  :: Q.NonEmptyList NotInferableGroup

  -> Maybe (Maybe (L.Entry L.Qty))
  -- ^ Optionally throws in another Maybe Entry; ents should fail
  -- regardless of whether another entry is present or not

  -> Gen Bool
prop_noEntsNotInferableGroup nib mayMayEnt = do
  let es = map Just . concat . map unNotInferableGroup
           . Q.getNonEmpty $ nib
      esWithExtra = maybe es (: es) mayMayEnt
  esWithInts <- pairWithInts esWithExtra
  return . isNothing . L.ents . map (first (fmap Right))
         $ esWithInts

--
-- # runTests
--
runTests :: (Q.Property -> IO Q.Result) -> IO Bool
runTests = $(A.forAllProperties)