    {-|

    Balance report, used by the balance command.

    -}

    {-# LANGUAGE FlexibleInstances, RecordWildCards, ScopedTypeVariables, OverloadedStrings #-}

    module Hledger.Reports.BalanceReport (
      BalanceReport,
      BalanceReportItem,
      balanceReport,
      flatShowsExclusiveBalance,

      -- * Tests
      tests_BalanceReport
    )
    where

    import Data.Time.Calendar

    import Hledger.Data
    import Hledger.Read (mamountp')
    import Hledger.Query
    import Hledger.Utils
    import Hledger.Reports.MultiBalanceReport (multiBalanceReportWith)
    import Hledger.Reports.ReportOptions
    import Hledger.Reports.ReportTypes

    -- | A simple balance report. It has:
    --
    -- 1. a list of items, one per account, each containing:
    --
    --   * the full account name
    --
    --   * the Ledger-style elided short account name
    --     (the leaf account name, prefixed by any boring parents immediately above);
    --     or with --flat, the full account name again
    --
    --   * the number of indentation steps for rendering a Ledger-style account tree,
    --     taking into account elided boring parents, --no-elide and --flat
    --
    --   * an amount
    --
    -- 2. the total of all amounts
    --
    type BalanceReport = ([BalanceReportItem], MixedAmount)
    type BalanceReportItem = (AccountName, AccountName, Int, MixedAmount)

    -- | When true (the default), this makes balance --flat reports and their implementation clearer.
    -- Single/multi-col balance reports currently aren't all correct if this is false.
    flatShowsExclusiveBalance :: Bool
    flatShowsExclusiveBalance    = Bool
    True

    -- | Enabling this makes balance --flat --empty also show parent accounts without postings,
    -- in addition to those with postings and a zero balance. Disabling it shows only the latter.
    -- No longer supported, but leave this here for a bit.
    -- flatShowsPostinglessAccounts = True

    -- | Generate a simple balance report, containing the matched accounts and
    -- their balances (change of balance) during the specified period.
    -- If the normalbalance_ option is set, it adjusts the sorting and sign of
    -- amounts (see ReportOpts and CompoundBalanceCommand).
    balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
    balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
    balanceReport ropts :: ReportOpts
    ropts q :: Query
    q j :: Journal
    j = ([(AccountName, AccountName, Int, MixedAmount)]
    rows, MixedAmount
    total)
      where
        report :: MultiBalanceReport
    report = ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport
    multiBalanceReportWith ReportOpts
    ropts Query
    q Journal
    j (Bool -> Journal -> PriceOracle
    journalPriceOracle (ReportOpts -> Bool
    infer_value_ ReportOpts
    ropts) Journal
    j)
        rows :: [(AccountName, AccountName, Int, MixedAmount)]
    rows = [( PeriodicReportRow DisplayName MixedAmount -> AccountName
    forall a. PeriodicReportRow DisplayName a -> AccountName
    prrFullName PeriodicReportRow DisplayName MixedAmount
    row
                , PeriodicReportRow DisplayName MixedAmount -> AccountName
    forall a. PeriodicReportRow DisplayName a -> AccountName
    prrDisplayName PeriodicReportRow DisplayName MixedAmount
    row
                , PeriodicReportRow DisplayName MixedAmount -> Int
    forall a. PeriodicReportRow DisplayName a -> Int
    prrDepth PeriodicReportRow DisplayName MixedAmount
    row Int -> Int -> Int
    forall a. Num a => a -> a -> a
    - 1  -- BalanceReport uses 0-based account depths
                , PeriodicReportRow DisplayName MixedAmount -> MixedAmount
    forall a b. PeriodicReportRow a b -> b
    prrTotal PeriodicReportRow DisplayName MixedAmount
    row
                ) | PeriodicReportRow DisplayName MixedAmount
    row <- MultiBalanceReport -> [PeriodicReportRow DisplayName MixedAmount]
    forall a b. PeriodicReport a b -> [PeriodicReportRow a b]
    prRows MultiBalanceReport
    report]
        total :: MixedAmount
    total = PeriodicReportRow () MixedAmount -> MixedAmount
    forall a b. PeriodicReportRow a b -> b
    prrTotal (PeriodicReportRow () MixedAmount -> MixedAmount)
    -> PeriodicReportRow () MixedAmount -> MixedAmount
    forall a b. (a -> b) -> a -> b
    $ MultiBalanceReport -> PeriodicReportRow () MixedAmount
    forall a b. PeriodicReport a b -> PeriodicReportRow () b
    prTotals MultiBalanceReport
    report

    -- tests

    Right samplejournal2 :: Journal
    samplejournal2 =
      Bool -> Journal -> Either String Journal
    journalBalanceTransactions Bool
    False
        Journal
    nulljournal{
          jtxns :: [Transaction]
    jtxns = [
            Transaction -> Transaction
    txnTieKnot Transaction :: Integer
    -> AccountName
    -> GenericSourcePos
    -> Day
    -> Maybe Day
    -> Status
    -> AccountName
    -> AccountName
    -> AccountName
    -> [Tag]
    -> [Posting]
    -> Transaction
    Transaction{
              tindex :: Integer
    tindex=0,
              tsourcepos :: GenericSourcePos
    tsourcepos=GenericSourcePos
    nullsourcepos,
              tdate :: Day
    tdate=Integer -> Int -> Int -> Day
    fromGregorian 2008 01 01,
              tdate2 :: Maybe Day
    tdate2=Day -> Maybe Day
    forall a. a -> Maybe a
    Just (Day -> Maybe Day) -> Day -> Maybe Day
    forall a b. (a -> b) -> a -> b
    $ Integer -> Int -> Int -> Day
    fromGregorian 2009 01 01,
              tstatus :: Status
    tstatus=Status
    Unmarked,
              tcode :: AccountName
    tcode="",
              tdescription :: AccountName
    tdescription="income",
              tcomment :: AccountName
    tcomment="",
              ttags :: [Tag]
    ttags=[],
              tpostings :: [Posting]
    tpostings=
                [Posting
    posting {paccount :: AccountName
    paccount="assets:bank:checking", pamount :: MixedAmount
    pamount=[Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 1]}
                ,Posting
    posting {paccount :: AccountName
    paccount="income:salary", pamount :: MixedAmount
    pamount=MixedAmount
    missingmixedamt}
                ],
              tprecedingcomment :: AccountName
    tprecedingcomment=""
            }
          ]
        }

    tests_BalanceReport :: TestTree
    tests_BalanceReport = String -> [TestTree] -> TestTree
    tests "BalanceReport" [

      let
        (opts :: ReportOpts
    opts,journal :: Journal
    journal) gives :: (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives` r :: BalanceReport
    r = do
          let (eitems :: [(AccountName, AccountName, Int, MixedAmount)]
    eitems, etotal :: MixedAmount
    etotal) = BalanceReport
    r
              (aitems :: [(AccountName, AccountName, Int, MixedAmount)]
    aitems, atotal :: MixedAmount
    atotal) = ReportOpts -> Query -> Journal -> BalanceReport
    balanceReport ReportOpts
    opts (Day -> ReportOpts -> Query
    queryFromOpts Day
    nulldate ReportOpts
    opts) Journal
    journal
              showw :: (a, b, c, MixedAmount) -> (a, b, c, String)
    showw (acct :: a
    acct,acct' :: b
    acct',indent :: c
    indent,amt :: MixedAmount
    amt) = (a
    acct, b
    acct', c
    indent, MixedAmount -> String
    showMixedAmountDebug MixedAmount
    amt)
          (((AccountName, AccountName, Int, MixedAmount)
     -> (AccountName, AccountName, Int, String))
    -> [(AccountName, AccountName, Int, MixedAmount)]
    -> [(AccountName, AccountName, Int, String)]
    forall a b. (a -> b) -> [a] -> [b]
    map (AccountName, AccountName, Int, MixedAmount)
    -> (AccountName, AccountName, Int, String)
    forall a b c. (a, b, c, MixedAmount) -> (a, b, c, String)
    showw [(AccountName, AccountName, Int, MixedAmount)]
    aitems) [(AccountName, AccountName, Int, String)]
    -> [(AccountName, AccountName, Int, String)] -> IO ()
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> IO ()
    @?= (((AccountName, AccountName, Int, MixedAmount)
     -> (AccountName, AccountName, Int, String))
    -> [(AccountName, AccountName, Int, MixedAmount)]
    -> [(AccountName, AccountName, Int, String)]
    forall a b. (a -> b) -> [a] -> [b]
    map (AccountName, AccountName, Int, MixedAmount)
    -> (AccountName, AccountName, Int, String)
    forall a b c. (a, b, c, MixedAmount) -> (a, b, c, String)
    showw [(AccountName, AccountName, Int, MixedAmount)]
    eitems)
          (MixedAmount -> String
    showMixedAmountDebug MixedAmount
    atotal) String -> String -> IO ()
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> IO ()
    @?= (MixedAmount -> String
    showMixedAmountDebug MixedAmount
    etotal)
      in
        String -> [TestTree] -> TestTree
    tests "balanceReport" [

         String -> IO () -> TestTree
    test "no args, null journal" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts, Journal
    nulljournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives` ([], 0)

        ,String -> IO () -> TestTree
    test "no args, sample journal" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([
            ("assets:bank:checking","assets:bank:checking",0, String -> MixedAmount
    mamountp' "$1.00")
           ,("assets:bank:saving","assets:bank:saving",0, String -> MixedAmount
    mamountp' "$1.00")
           ,("assets:cash","assets:cash",0, String -> MixedAmount
    mamountp' "$-2.00")
           ,("expenses:food","expenses:food",0, String -> MixedAmount
    mamountp' "$1.00")
           ,("expenses:supplies","expenses:supplies",0, String -> MixedAmount
    mamountp' "$1.00")
           ,("income:gifts","income:gifts",0, String -> MixedAmount
    mamountp' "$-1.00")
           ,("income:salary","income:salary",0, String -> MixedAmount
    mamountp' "$-1.00")
           ],
           [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

        ,String -> IO () -> TestTree
    test "with --tree" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts{accountlistmode_ :: AccountListMode
    accountlistmode_=AccountListMode
    ALTree}, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([
            ("assets","assets",0, String -> MixedAmount
    mamountp' "$0.00")
           ,("assets:bank","bank",1, String -> MixedAmount
    mamountp' "$2.00")
           ,("assets:bank:checking","checking",2, String -> MixedAmount
    mamountp' "$1.00")
           ,("assets:bank:saving","saving",2, String -> MixedAmount
    mamountp' "$1.00")
           ,("assets:cash","cash",1, String -> MixedAmount
    mamountp' "$-2.00")
           ,("expenses","expenses",0, String -> MixedAmount
    mamountp' "$2.00")
           ,("expenses:food","food",1, String -> MixedAmount
    mamountp' "$1.00")
           ,("expenses:supplies","supplies",1, String -> MixedAmount
    mamountp' "$1.00")
           ,("income","income",0, String -> MixedAmount
    mamountp' "$-2.00")
           ,("income:gifts","gifts",1, String -> MixedAmount
    mamountp' "$-1.00")
           ,("income:salary","salary",1, String -> MixedAmount
    mamountp' "$-1.00")
           ],
           [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

        ,String -> IO () -> TestTree
    test "with --depth=N" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts{depth_ :: Maybe Int
    depth_=Int -> Maybe Int
    forall a. a -> Maybe a
    Just 1}, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([
           ("expenses",    "expenses",    0, String -> MixedAmount
    mamountp'  "$2.00")
           ,("income",      "income",      0, String -> MixedAmount
    mamountp' "$-2.00")
           ],
           [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

        ,String -> IO () -> TestTree
    test "with depth:N" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts{query_ :: String
    query_="depth:1"}, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([
           ("expenses",    "expenses",    0, String -> MixedAmount
    mamountp'  "$2.00")
           ,("income",      "income",      0, String -> MixedAmount
    mamountp' "$-2.00")
           ],
           [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

        ,String -> IO () -> TestTree
    test "with date:" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts{query_ :: String
    query_="date:'in 2009'"}, Journal
    samplejournal2) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([], 0)

        ,String -> IO () -> TestTree
    test "with date2:" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts{query_ :: String
    query_="date2:'in 2009'"}, Journal
    samplejournal2) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([
            ("assets:bank:checking","assets:bank:checking",0,String -> MixedAmount
    mamountp' "$1.00")
           ,("income:salary","income:salary",0,String -> MixedAmount
    mamountp' "$-1.00")
           ],
           [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

        ,String -> IO () -> TestTree
    test "with desc:" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts{query_ :: String
    query_="desc:income"}, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([
            ("assets:bank:checking","assets:bank:checking",0,String -> MixedAmount
    mamountp' "$1.00")
           ,("income:salary","income:salary",0, String -> MixedAmount
    mamountp' "$-1.00")
           ],
           [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

        ,String -> IO () -> TestTree
    test "with not:desc:" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
         (ReportOpts
    defreportopts{query_ :: String
    query_="not:desc:income"}, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
          ([
            ("assets:bank:saving","assets:bank:saving",0, String -> MixedAmount
    mamountp' "$1.00")
           ,("assets:cash","assets:cash",0, String -> MixedAmount
    mamountp' "$-2.00")
           ,("expenses:food","expenses:food",0, String -> MixedAmount
    mamountp' "$1.00")
           ,("expenses:supplies","expenses:supplies",0, String -> MixedAmount
    mamountp' "$1.00")
           ,("income:gifts","income:gifts",0, String -> MixedAmount
    mamountp' "$-1.00")
           ],
           [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

        ,String -> IO () -> TestTree
    test "with period on a populated period" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
          (ReportOpts
    defreportopts{period_ :: Period
    period_= Day -> Day -> Period
    PeriodBetween (Integer -> Int -> Int -> Day
    fromGregorian 2008 1 1) (Integer -> Int -> Int -> Day
    fromGregorian 2008 1 2)}, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
           (
            [
             ("assets:bank:checking","assets:bank:checking",0, String -> MixedAmount
    mamountp' "$1.00")
            ,("income:salary","income:salary",0, String -> MixedAmount
    mamountp' "$-1.00")
            ],
            [Amount] -> MixedAmount
    Mixed [DecimalRaw Integer -> Amount
    usd 0])

         ,String -> IO () -> TestTree
    test "with period on an unpopulated period" (IO () -> TestTree) -> IO () -> TestTree
    forall a b. (a -> b) -> a -> b
    $
          (ReportOpts
    defreportopts{period_ :: Period
    period_= Day -> Day -> Period
    PeriodBetween (Integer -> Int -> Int -> Day
    fromGregorian 2008 1 2) (Integer -> Int -> Int -> Day
    fromGregorian 2008 1 3)}, Journal
    samplejournal) (ReportOpts, Journal) -> BalanceReport -> IO ()
    `gives`
           ([], 0)

      {-
          ,test "accounts report with account pattern o" ~:
           defreportopts{patterns_=["o"]} `gives`
           ["                  $1  expenses:food"
           ,"                 $-2  income"
           ,"                 $-1    gifts"
           ,"                 $-1    salary"
           ,"--------------------"
           ,"                 $-1"
           ]

          ,test "accounts report with account pattern o and --depth 1" ~:
           defreportopts{patterns_=["o"],depth_=Just 1} `gives`
           ["                  $1  expenses"
           ,"                 $-2  income"
           ,"--------------------"
           ,"                 $-1"
           ]

          ,test "accounts report with account pattern a" ~:
           defreportopts{patterns_=["a"]} `gives`
           ["                 $-1  assets"
           ,"                  $1    bank:saving"
           ,"                 $-2    cash"
           ,"                 $-1  income:salary"
           ,"                  $1  liabilities:debts"
           ,"--------------------"
           ,"                 $-1"
           ]

          ,test "accounts report with account pattern e" ~:
           defreportopts{patterns_=["e"]} `gives`
           ["                 $-1  assets"
           ,"                  $1    bank:saving"
           ,"                 $-2    cash"
           ,"                  $2  expenses"
           ,"                  $1    food"
           ,"                  $1    supplies"
           ,"                 $-2  income"
           ,"                 $-1    gifts"
           ,"                 $-1    salary"
           ,"                  $1  liabilities:debts"
           ,"--------------------"
           ,"                   0"
           ]

          ,test "accounts report with unmatched parent of two matched subaccounts" ~:
           defreportopts{patterns_=["cash","saving"]} `gives`
           ["                 $-1  assets"
           ,"                  $1    bank:saving"
           ,"                 $-2    cash"
           ,"--------------------"
           ,"                 $-1"
           ]

          ,test "accounts report with multi-part account name" ~:
           defreportopts{patterns_=["expenses:food"]} `gives`
           ["                  $1  expenses:food"
           ,"--------------------"
           ,"                  $1"
           ]

          ,test "accounts report with negative account pattern" ~:
           defreportopts{patterns_=["not:assets"]} `gives`
           ["                  $2  expenses"
           ,"                  $1    food"
           ,"                  $1    supplies"
           ,"                 $-2  income"
           ,"                 $-1    gifts"
           ,"                 $-1    salary"
           ,"                  $1  liabilities:debts"
           ,"--------------------"
           ,"                  $1"
           ]

          ,test "accounts report negative account pattern always matches full name" ~:
           defreportopts{patterns_=["not:e"]} `gives`
           ["--------------------"
           ,"                   0"
           ]

          ,test "accounts report negative patterns affect totals" ~:
           defreportopts{patterns_=["expenses","not:food"]} `gives`
           ["                  $1  expenses:supplies"
           ,"--------------------"
           ,"                  $1"
           ]

          ,test "accounts report with -E shows zero-balance accounts" ~:
           defreportopts{patterns_=["assets"],empty_=True} `gives`
           ["                 $-1  assets"
           ,"                  $1    bank"
           ,"                   0      checking"
           ,"                  $1      saving"
           ,"                 $-2    cash"
           ,"--------------------"
           ,"                 $-1"
           ]

          ,test "accounts report with cost basis" $
             j <- (readJournal def Nothing $ unlines
                    [""
                    ,"2008/1/1 test           "
                    ,"  a:b          10h @ $50"
                    ,"  c:d                   "
                    ]) >>= either error' return
             let j' = journalCanonicaliseAmounts $ journalToCost j -- enable cost basis adjustment
             balanceReportAsText defreportopts (balanceReport defreportopts Any j') `is`
               ["                $500  a:b"
               ,"               $-500  c:d"
               ,"--------------------"
               ,"                   0"
               ]
      -}
         ]

     ]