;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2 of
;; the License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, contact:
;;
;; Free Software Foundation           Voice:  +1-617-542-5942
;; 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
;; Boston, MA  02110-1301,  USA       gnu@gnu.org
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define-module (tests test-engine-extras))

(use-modules (gnucash app-utils))
(use-modules (gnucash engine))
(use-modules (gnucash utilities))
(use-modules (srfi srfi-1))
(use-modules (sw_app_utils))
(use-modules (sw_engine))

(export test)

(export with-account)
(export with-transaction)

(export create-test-env)
(export env-random-amount)
(export env-random)
(export env-counter-next)
(export env-string)
(export env-select-price-source)
(export env-any-date)
(export env-transfer)
(export env-transfer-foreign)
(export env-create-multisplit-transaction)
(export env-create-transaction)
(export env-create-account)
(export env-create-root-account)
(export env-create-test-accounts)
(export env-create-daily-transactions)
(export env-create-account-structure)
(export env-create-account-structure-alist)
(export env-expense-account-structure)
(export gnc-pricedb-create)

;;
;; Random test related syntax and the like
;;

(define (test the-test)
  (format #t "(Running ~a " the-test)
  (let ((result (the-test)))
    (format #t "~a Completed)\n" result)
    result))

;;
;; Gnucash specifics
;;

;; Really could do with generalising and making into a 'with' macro
(define (with-account account fn)
  (begin (xaccAccountBeginEdit account)
	 (let ((result (fn)))
	   (xaccAccountCommitEdit account)
	   result)))

(define (with-accounts accounts fn)
  (begin (map xaccAccountBeginEdit accounts)
	 (let ((result (fn)))
	   (map xaccAccountCommitEdit accounts)
	   result)))

(define (with-transaction txn fn)
  (begin (xaccTransBeginEdit txn)
	 (let ((result (fn)))
	   (xaccTransCommitEdit txn)
	   result)))

;; Test environments.. an environment is just an alist with some well
;; known names.  The idea is that we can use it to pass around
;; "suitable" sets of values for things

(define (make-counter)
  (let ((x 0))
    (lambda ()
      (begin (set! x (+ x 1))
	     x))))

(define (create-test-env)
  (list (cons 'random (seed->random-state (random 1000)))
	(cons 'counter (make-counter))))

(define (env-random-amount env n)
  (/ (env-random env n) 1))

(define (env-random env n)
  (random n (assoc-ref env 'random)))

(define (env-counter-next env)
  ((assoc-ref env 'counter)))

(define (env-string env prefix)
  (format #f "~a-~a" prefix (env-counter-next env)))

(define (env-select-price-source env)
  'pricedb-nearest)

(define (env-any-date env) (gnc:get-today))

(define (env-create-transaction env date credit debit aaa)
  (let ((txn (xaccMallocTransaction (gnc-get-current-book)))
	(split-1 (xaccMallocSplit  (gnc-get-current-book)))
	(split-2 (xaccMallocSplit  (gnc-get-current-book)))
	(datevec (gnc-localtime date)))
    (with-transaction txn
		      (lambda ()
			(xaccTransSetDescription txn (env-string env "ponies"))
			(xaccTransSetCurrency txn (gnc-default-report-currency))
			(xaccTransSetDate txn
					  (gnc:date-get-month-day datevec)
					  (gnc:date-get-month datevec)
					  (gnc:date-get-year datevec))
			(xaccSplitSetParent split-1 txn)
			(xaccSplitSetParent split-2 txn)
			(xaccSplitSetAccount split-1 credit)
			(xaccSplitSetAccount split-2 debit)
			(xaccSplitSetAmount split-1 aaa)
			(xaccSplitSetAmount split-2 (gnc-numeric-neg aaa))
			(xaccSplitSetValue split-1 aaa)
			(xaccSplitSetValue split-2 (gnc-numeric-neg aaa))
			))
    ;(format #t "tx ~a\n" (map xaccSplitGetAmount (list split-1 split-2)))
    ;(format #t "tx ~a\n" (map xaccSplitGetValue (list split-1 split-2)))
    txn))

(define (gnc-pricedb-create currency commodity time64 value)
  ;; does not check for pre-existing pricedb data on date
  (unless (gnc-commodity-equiv currency commodity)
    (let ((price (gnc-price-create (gnc-get-current-book)))
          (pricedb (gnc-pricedb-get-db (gnc-get-current-book))))
      (gnc-price-begin-edit price)
      (gnc-price-set-commodity price commodity)
      (gnc-price-set-currency price currency)
      (gnc-price-set-time64 price time64)
      (gnc-price-set-source price PRICE-SOURCE-XFER-DLG-VAL)
      (gnc-price-set-source-string price "test-price")
      (gnc-price-set-typestr price "test")
      (gnc-price-set-value price value)
      (gnc-price-commit-edit price)
      (gnc-pricedb-add-price pricedb price))))

;; When creating stock transactions always put the stock account and the number
;; of shares second, using negative numbers for a sale. e.g., to buy 100 shares
;; of IBM:
;;    (env-transfer-foreign env 15 01 2012 cash-a ibm-a 3583200/100 200
;;                          #:description "Buy IBM 200") ;;200 @ $179.16
;; and to sell them:
;;    (env-transfer-foreign env 8 8 2014 cash-a ibm-a -3732600/100 -200
;;                          #:description "Sell IBM 200") ;;-200 @ $186.63
;;    (env-transfer-foreign env 8 8 2014 capgain ibm-a 149400/100 0
;;                          #:description "IBM 200 G/L")

(define* (env-transfer-foreign
          env
          DD MM YY         ; day/month/year
          debit            ; account-from
          credit           ; account-to
          amount1          ; amount-from
          amount2          ; amount-to
          #:key            ; - the following are optional -
          description      ; string: description (def = "ponies")
          void-reason      ; string: void-reason (def = not-voided)
          reconcile        ; pair  : (cons reconciled reconciled-date)
          num              ; string: num field   (def = null)
          notes            ; string: notes       (def = null)
          memo             ; string: memo        (def = null)
          )
  (env-create-multisplit-transaction
   env
   DD MM YY
   (list (vector debit (- amount1) (- amount1))
         (vector credit amount1 amount2))
   #:description description
   #:void-reason void-reason
   #:reconcile reconcile
   #:num num
   #:memo memo
   #:notes notes))

(define* (env-transfer
          env
          DD MM YY         ; day/month/year
          debit            ; account-from
          credit           ; account-to
          amount           ; amount
          #:key            ; - the following are optional -
          description      ; string: description (def = "ponies")
          void-reason      ; string: void-reason (def = not-voided)
          reconcile        ; char  : reconciled  (default = n)
          num              ; string: num field   (def = null)
          notes            ; string: notes       (def = null)
          memo)            ; string: memo        (def = null)
  (env-create-multisplit-transaction
   env
   DD MM YY
   (list
    (vector debit (- amount) (- amount))
    (vector credit amount amount))
   #:description description
   #:void-reason void-reason
   #:reconcile reconcile
   #:num num
   #:memo memo
   #:notes notes))

;; creates multisplit transaction.
;;
;; input: DD/MM/YY - posting date of transaction
;;
;;        list-of-splits, a list of vectors, whose components are
;;        (vector account value amount). The total of sum of values
;;        must be zero, otherwise an imbalance split will be
;;        created. It must contain at least 1 split. The transaction
;;        currency is set as currency of the first split.
;;
;; returns: the transaction created, or #f
(define* (env-create-multisplit-transaction
          env DD MM YY list-of-splits
          #:key            ; - the following are optional -
          description      ; string: description (def = "ponies")
          (pricedb? #t)    ; boolean: add pricedb entry?
          void-reason      ; string: void-reason (def = not-voided)
          reconcile        ; pair  : (cons reconciled reconciled-date)
          num              ; string: num field   (def = null)
          notes            ; string: notes       (def = null)
          memo)            ; string: memo        (def = null)
  (and (pair? list-of-splits)
       (let* ((book (gnc-get-current-book))
              (txn (xaccMallocTransaction book))
              (first-split (vector-ref (car list-of-splits) 0)))
         (xaccTransBeginEdit txn)
         (xaccTransSetDescription txn (or description (env-string env "ponies")))
         (xaccTransSetCurrency txn (xaccAccountGetCommodity first-split))
         (xaccTransSetDate txn DD MM YY)
         (for-each
          (lambda (split)
            (let ((acc (vector-ref split 0))
                  (val (vector-ref split 1))
                  (amt (vector-ref split 2))
                  (newsplit (xaccMallocSplit book)))
              (xaccSplitSetParent newsplit txn)
              (xaccSplitSetAccount newsplit acc)
              (xaccSplitSetValue newsplit val)
              (xaccSplitSetAmount newsplit amt)
              (if num (gnc-set-num-action txn newsplit num num))
              (if memo (xaccSplitSetMemo newsplit memo))
              (when reconcile
                (xaccSplitSetReconcile newsplit (car reconcile))
                (xaccSplitSetDateReconciledSecs newsplit (cdr reconcile)))
              (if (and pricedb?
                       (not (zero? amt))
                       (not (gnc-commodity-equiv
                             (xaccAccountGetCommodity first-split)
                             (xaccAccountGetCommodity acc))))
                  (gnc-pricedb-create (xaccAccountGetCommodity first-split)
                                      (xaccAccountGetCommodity acc)
                                      (gnc-dmy2time64 DD MM YY)
                                      (/ val amt)))))
          list-of-splits)
         (if void-reason (xaccTransVoid txn void-reason))
         (if notes (xaccTransSetNotes txn notes))
         (xaccTransCommitEdit txn)
         txn)))

(define (env-create-root-account env type commodity)
  (env-create-account env type commodity (gnc-get-current-root-account)))

(define (env-create-account env type commodity parent-account)
  (let ((new-account (xaccMallocAccount (gnc-get-current-book))))
    (with-accounts (list new-account parent-account)
		  (lambda ()
		    (xaccAccountSetCommodity new-account commodity)
		    (xaccAccountSetName new-account (env-string env "account"))
		    (xaccAccountSetType new-account type)
		    (gnc-account-append-child parent-account new-account)
		    new-account))))

;; Spend '1' on the 1st, '2' on the 2nd, etc.  Makes for pretty graphs
(define (env-create-daily-transactions env start-date end-date to-account from-account)
  (let ((dates-this-month (gnc:make-date-list start-date
					      end-date
					      DayDelta)))
      (for-each (lambda (date)
		  (env-create-transaction env date to-account
					  from-account
					  (/
					   (gnc:date-get-month-day (gnc-localtime date))
					   1)))
		(cdr (reverse dates-this-month)))))

(define (env-create-account-structure env account-structure)
  (define (lookup-options list)
    (if (null? list) (cons '() '())
	(if (not (pair? (car list)))
	    (cons '() list)
	    (if (not (pair? (car (car list))))
		(cons '() list)
		list))))

  (define (create-substructure parent options account-structure)
    ;;(format #t "Creating subaccounts for ~a ~a\n"
    ;; (xaccAccountGetName parent) account-structure)
    (let* ((account-name (car account-structure))
	   (options-pair (lookup-options (cdr account-structure)))
	   (options (append (car options-pair) options)))
      ;;(format #t "New Account ~a\n" account-name)
      ;;(format #t "Options ~a\n" (car options-pair))
      ;;(format #t "Child list ~a\n" (cdr options-pair))
      (let ((new-account (env-create-account env (assoc-ref options 'type)
					     (assoc-ref options 'commodity)
					     parent)))
	(with-accounts (list new-account)
		       (lambda ()
			 (xaccAccountSetName new-account account-name)))

	(cons new-account
	      (map (lambda (child)
		     (create-substructure new-account options child))
		   (cdr options-pair))))))
  (let ((options (list (cons 'commodity (gnc-default-report-currency))
		       (cons 'type '()))))
    (create-substructure (gnc-get-current-root-account)
			 options
			 account-structure)))

(define (env-create-account-structure-alist env account-structure)
  (let ((accounts (env-create-account-structure env account-structure)))
    (map (lambda (acct) (cons (xaccAccountGetName acct) acct))
	 (gnc:list-flatten accounts))))

(define (env-expense-account-structure env)
  (env-create-account-structure
   env
   (list "Expenses"
	 (list (cons 'type ACCT-TYPE-EXPENSE))
	 (list "Groceries")
	 (list "Rent")
	 (list "Auto"
	       (list "Tax")
	       (list "Parking")
	       (list "Petrol")))))

(define (env-create-test-accounts env)
  (env-create-account-structure-alist
   env
   (list "Root"
	 (list (cons 'type ACCT-TYPE-ASSET))
	 (list "Bank")
	 (list "Wallet")
	 (list "Other")
	 (list "Expenses"
	       (list (cons 'type ACCT-TYPE-EXPENSE))))))

(define (mnemonic->commodity sym)
    (gnc-commodity-table-lookup
     (gnc-commodity-table-get-table (gnc-get-current-book))
     (gnc-commodity-get-namespace (gnc-default-report-currency))
     sym))

(define-public (create-test-data)
  (let* ((env (create-test-env))
         (GBP (mnemonic->commodity "GBP"))
         (structure
          (list "Root" (list
                        (cons 'type ACCT-TYPE-ASSET))
                (list "Asset"
                      (list "Bank")
                      (list "GBP Bank" (list
                                        (cons 'commodity GBP)))
                      (list "Wallet"))
                (list "Income" (list
                                (cons 'type ACCT-TYPE-INCOME)))
                (list "Income-GBP" (list
                                    (cons 'type ACCT-TYPE-INCOME)
                                    (cons 'commodity GBP)))
                (list "Expenses" (list
                                  (cons 'type ACCT-TYPE-EXPENSE)))
                (list "Liabilities" (list (cons 'type ACCT-TYPE-LIABILITY)))
                (list "Equity" (list
                                (cons 'type ACCT-TYPE-EQUITY)))))
         (account-alist (env-create-account-structure-alist env structure))
         (bank (cdr (assoc "Bank" account-alist)))
         (gbp-bank (cdr (assoc "GBP Bank" account-alist)))
         (wallet (cdr (assoc "Wallet" account-alist)))
         (income (cdr (assoc "Income" account-alist)))
         (gbp-income (cdr (assoc "Income-GBP" account-alist)))
         (expense (cdr (assoc "Expenses" account-alist)))
         (liability (cdr (assoc "Liabilities" account-alist)))
         (equity (cdr (assoc "Equity" account-alist))))

    ;; populate datafile with old transactions
    (env-transfer env 01 01 1970 bank expense       5
                  #:description "desc-1" #:num "trn1"
                  #:memo "memo-3")

    (env-transfer env 31 12 1969 income bank       10
                  #:description "desc-2" #:num "trn2"
                  #:void-reason "void" #:notes "notes3")

    (env-transfer env 31 12 1969 income bank       29
                  #:description "desc-3" #:num "trn3"
                  #:reconcile (cons #\c (gnc-dmy2time64 01 03 1970)))

    (env-transfer env 01 02 1970 bank expense      15
                  #:description "desc-4" #:num "trn4"
                  #:notes "notes2" #:memo "memo-1")

    (env-transfer env 10 01 1970 liability expense 10
                  #:description "desc-5" #:num "trn5"
                  #:void-reason "any")

    (env-transfer env 10 01 1970 liability expense 11
                  #:description "desc-6" #:num "trn6"
                  #:notes "notes1")

    (env-transfer env 10 02 1970 bank liability     8
                  #:description "desc-7" #:num "trn7"
                  #:notes "notes1" #:memo "memo-2"
                  #:reconcile (cons #\y (gnc-dmy2time64 01 03 1970)))

    (env-create-multisplit-transaction
     env 14 02 1971
     (list (vector bank  -100 -100)
           (vector expense 80   80)
           (vector wallet  20   20))
     #:description "$100bank -> $80expenses + $20wallet"
     #:notes "multisplit")

    (let ((closing-txn (env-transfer
                        env 31 12 1977 expense equity
                        111 #:description "Closing")))
      (xaccTransSetIsClosingTxn closing-txn #t))

    (env-transfer-foreign env 15 01 2000 gbp-bank bank
                          10 14 #:description "GBP 10 to USD 14")

    (env-transfer-foreign env 15 02 2000 bank gbp-bank
                          9  6 #:description "USD 9 to GBP 6")

    (for-each
     (lambda (m)
       (env-transfer env 08 (1+ m) 1978
                     gbp-income gbp-bank 51 #:description "#51 income")
       (env-transfer env 03 (1+ m) 1978
                     income bank  103 #:description "$103 income")
       (env-transfer env 15 (1+ m) 1978
                     bank expense  22 #:description "$22 expense")
       (env-transfer env 09 (1+ m) 1978
                     income bank  109 #:description "$109 income"))
     (iota 12))
    account-alist))

;; creates 8 invoices. (1) customer-invoice (2) customer's job's
;; invoice (3) vendor bill (4) employee bill (5) customer credit-note
;; (6) vendor credit-note (7) employee credit-note (8)
;; customer-invoice with various combinations of entries. in addition,
;; this function will return the vector-list of invoices created.
(define-public (create-test-invoice-data)
  (define USD (mnemonic->commodity "USD"))
  (define structure
    (list "Root" (list (cons 'type ACCT-TYPE-ASSET)
                       (cons 'commodity USD))
          (list "Asset"
                (list "Bank"))
          (list "VAT"
                (list "VAT-on-Purchases")
                (list "VAT-on-Sales" (list (cons 'type ACCT-TYPE-LIABILITY))))
          (list "Income" (list (cons 'type ACCT-TYPE-INCOME)))
          (list "Expense" (list (cons 'type ACCT-TYPE-EXPENSE)))
          (list "A/Receivable" (list (cons 'type ACCT-TYPE-RECEIVABLE)))
          (list "A/Payable" (list (cons 'type ACCT-TYPE-PAYABLE)))))
  (let* ((env (create-test-env))
         (account-alist (env-create-account-structure-alist env structure))
         (bank (cdr (assoc "Bank" account-alist)))
         (income (cdr (assoc "Income" account-alist)))
         (expense (cdr (assoc "Expense" account-alist)))
         (vat-sales (cdr (assoc "VAT-on-Sales" account-alist)))
         (vat-purchases (cdr (assoc "VAT-on-Purchases" account-alist)))
         (receivable (cdr (assoc "A/Receivable" account-alist)))
         (payable (cdr (assoc "A/Payable" account-alist)))
         (YEAR (gnc:time64-get-year (gnc:get-today)))

         (cust-1 (let ((cust-1 (gncCustomerCreate (gnc-get-current-book))))
                   (gncCustomerSetID cust-1 "cust-1-id")
                   (gncCustomerSetName cust-1 "cust-1-name")
                   (gncCustomerSetNotes cust-1 "cust-1-notes")
                   (gncCustomerSetCurrency cust-1 USD)
                   (gncCustomerSetTaxIncluded cust-1 1) ;1 = GNC-TAXINCLUDED-YES
                   cust-1))

         (owner-1 (let ((owner-1 (gncOwnerNew)))
                    (gncOwnerInitCustomer owner-1 cust-1)
                    owner-1))

         ;; inv-1 is generated for a customer
         (inv-1 (let ((inv-1 (gncInvoiceCreate (gnc-get-current-book))))
                  (gncInvoiceSetOwner inv-1 owner-1)
                  (gncInvoiceSetNotes inv-1 "inv-1-notes")
                  (gncInvoiceSetBillingID inv-1 "inv-1-billing-id")
                  (gncInvoiceSetCurrency inv-1 USD)
                  inv-1))

         (job-1 (let ((job-1 (gncJobCreate (gnc-get-current-book))))
                  (gncJobSetID job-1 "job-1-id")
                  (gncJobSetName job-1 "job-1-name")
                  (gncJobSetOwner job-1 owner-1)
                  job-1))

         (owner-2 (let ((owner-2 (gncOwnerNew)))
                    (gncOwnerInitJob owner-2 job-1)
                    owner-2))

         ;; inv-2 is generated from a customer's job
         (inv-2 (let ((inv-2 (gncInvoiceCreate (gnc-get-current-book))))
                  (gncInvoiceSetOwner inv-2 owner-2)
                  (gncInvoiceSetNotes inv-2 "inv-2-notes")
                  (gncInvoiceSetCurrency inv-2 USD)
                  inv-2))

         (vend-1 (let ((vend-1 (gncVendorCreate (gnc-get-current-book))))
                   (gncVendorSetID vend-1 "vend-1-id")
                   (gncVendorSetName vend-1 "vend-1-name")
                   (gncVendorSetNotes vend-1 "vend-1-notes")
                   (gncVendorSetCurrency vend-1 USD)
                   (gncVendorSetTaxIncluded vend-1 1) ;1 = GNC-TAXINCLUDED-YES
                   vend-1))

         (owner-3 (let ((owner-3 (gncOwnerNew)))
                    (gncOwnerInitVendor owner-3 vend-1)
                    owner-3))

         ;; inv-3 is generated from a vendor
         (inv-3 (let ((inv-3 (gncInvoiceCreate (gnc-get-current-book))))
                  (gncInvoiceSetOwner inv-3 owner-3)
                  (gncInvoiceSetNotes inv-3 "inv-3-notes")
                  (gncInvoiceSetCurrency inv-3 USD)
                  inv-3))

         (emp-1 (let ((emp-1 (gncEmployeeCreate (gnc-get-current-book))))
                  (gncEmployeeSetID emp-1 "emp-1-id")
                  (gncEmployeeSetCurrency emp-1 USD)
                  (gncEmployeeSetName emp-1 "emp-1-name")
                  emp-1))

         (owner-4 (let ((owner-4 (gncOwnerNew)))
                    (gncOwnerInitEmployee owner-4 emp-1)
                    owner-4))

         ;; inv-4 is generated for an employee
         (inv-4 (let ((inv-4 (gncInvoiceCreate (gnc-get-current-book))))
                  (gncInvoiceSetOwner inv-4 owner-4)
                  (gncInvoiceSetNotes inv-4 "inv-4-notes")
                  (gncInvoiceSetCurrency inv-4 USD)
                  inv-4))

         ;; inv-5 cust-credit-note
         (inv-5 (let ((inv-5 (gncInvoiceCopy inv-1)))
                  (gncInvoiceSetIsCreditNote inv-5 #t)
                  (gncInvoiceSetCurrency inv-5 USD)
                  inv-5))

         ;; inv-6 vend-credit-note
         (inv-6 (let ((inv-6 (gncInvoiceCopy inv-3)))
                  (gncInvoiceSetIsCreditNote inv-6 #t)
                  (gncInvoiceSetCurrency inv-6 USD)
                  inv-6))

         ;; inv-7 emp-credit-note
         (inv-7 (let ((inv-7 (gncInvoiceCopy inv-4)))
                  (gncInvoiceSetIsCreditNote inv-7 #t)
                  (gncInvoiceSetCurrency inv-7 USD)
                  inv-7))

         (inv-8 (let ((inv-8 (gncInvoiceCreate (gnc-get-current-book))))
                  (gncInvoiceSetOwner inv-8 owner-1)
                  (gncInvoiceSetCurrency inv-8 USD)
                  inv-8))

         (standard-vat-sales-tt
          (let ((tt (gncTaxTableCreate (gnc-get-current-book))))
            (gncTaxTableIncRef tt)
            (gncTaxTableSetName tt "10% vat on sales")
            (let ((entry (gncTaxTableEntryCreate)))
              (gncTaxTableEntrySetAccount entry vat-sales)
              (gncTaxTableEntrySetType entry GNC-AMT-TYPE-PERCENT)
              (gncTaxTableEntrySetAmount entry 10)
              (gncTaxTableAddEntry tt entry))
            tt))

         (standard-vat-purchases-tt
          (let ((tt (gncTaxTableCreate (gnc-get-current-book))))
            (gncTaxTableIncRef tt)
            (gncTaxTableSetName tt "10% vat on purchases")
            (let ((entry (gncTaxTableEntryCreate)))
              (gncTaxTableEntrySetAccount entry vat-purchases)
              (gncTaxTableEntrySetType entry GNC-AMT-TYPE-PERCENT)
              (gncTaxTableEntrySetAmount entry 10)
              (gncTaxTableAddEntry tt entry))
            tt)))

    ;; entry-1  2 widgets of $3 = $6
    (let ((entry-1 (gncEntryCreate (gnc-get-current-book))))
      (gncEntrySetDateGDate entry-1 (time64-to-gdate (current-time)))
      (gncEntrySetDescription entry-1 "entry-1-desc")
      (gncEntrySetAction entry-1 "entry-1-action")
      (gncEntrySetNotes entry-1 "entry-1-notes")
      (gncEntrySetInvAccount entry-1 income)
      (gncEntrySetDocQuantity entry-1 2 #f)
      (gncEntrySetInvPrice entry-1 3)
      (gncInvoiceAddEntry inv-1 entry-1))

    ;; entry-inv-2  2 widgets of $3 = $6
    (let ((entry-inv-2 (gncEntryCreate (gnc-get-current-book))))
      (gncEntrySetDateGDate entry-inv-2 (time64-to-gdate (current-time)))
      (gncEntrySetDescription entry-inv-2 "entry-inv-2-desc")
      (gncEntrySetAction entry-inv-2 "entry-inv-2-action")
      (gncEntrySetNotes entry-inv-2 "entry-inv-2-notes")
      (gncEntrySetInvAccount entry-inv-2 income)
      (gncEntrySetDocQuantity entry-inv-2 2 #f)
      (gncEntrySetInvPrice entry-inv-2 3)
      (gncInvoiceAddEntry inv-2 entry-inv-2))

    ;; entry-inv-3  2 widgets of $3 = $6
    (let ((entry-inv-3 (gncEntryCreate (gnc-get-current-book))))
      (gncEntrySetDateGDate entry-inv-3 (time64-to-gdate (current-time)))
      (gncEntrySetDescription entry-inv-3 "entry-inv-3-desc")
      (gncEntrySetAction entry-inv-3 "entry-inv-3-action")
      (gncEntrySetNotes entry-inv-3 "entry-inv-3-notes")
      (gncEntrySetBillAccount entry-inv-3 expense)
      (gncEntrySetDocQuantity entry-inv-3 2 #f)
      (gncEntrySetBillPrice entry-inv-3 3)
      (gncInvoiceAddEntry inv-3 entry-inv-3))

    ;; entry-inv-4  2 widgets of $3 = $6
    (let ((entry-inv-4 (gncEntryCreate (gnc-get-current-book))))
      (gncEntrySetDateGDate entry-inv-4 (time64-to-gdate (current-time)))
      (gncEntrySetDescription entry-inv-4 "entry-inv-4-desc")
      (gncEntrySetAction entry-inv-4 "entry-inv-4-action")
      (gncEntrySetNotes entry-inv-4 "entry-inv-4-notes")
      (gncEntrySetBillAccount entry-inv-4 expense)
      (gncEntrySetDocQuantity entry-inv-4 2 #f)
      (gncEntrySetBillPrice entry-inv-4 3)
      (gncInvoiceAddEntry inv-4 entry-inv-4))

    ;; entry-5  2 widgets of $3 = $6
    (let ((entry-5 (gncEntryCreate (gnc-get-current-book))))
      (gncEntrySetDateGDate entry-5 (time64-to-gdate (current-time)))
      (gncEntrySetDescription entry-5 "entry-5-desc")
      (gncEntrySetAction entry-5 "entry-5-action")
      (gncEntrySetNotes entry-5 "entry-5-notes")
      (gncEntrySetInvAccount entry-5 income)
      (gncEntrySetDocQuantity entry-5 2 #t)
      (gncEntrySetInvPrice entry-5 3)
      (gncInvoiceAddEntry inv-5 entry-5))

    (let ((entry-inv-6 (gncEntryCreate (gnc-get-current-book))))
      (gncEntrySetDateGDate entry-inv-6 (time64-to-gdate (current-time)))
      (gncEntrySetDescription entry-inv-6 "entry-inv-6-desc")
      (gncEntrySetAction entry-inv-6 "entry-inv-6-action")
      (gncEntrySetNotes entry-inv-6 "entry-inv-6-notes")
      (gncEntrySetBillAccount entry-inv-6 expense)
      (gncEntrySetDocQuantity entry-inv-6 2 #t)
      (gncEntrySetBillPrice entry-inv-6 3)
      (gncInvoiceAddEntry inv-6 entry-inv-6))

    ;; entry-inv-7  2 widgets of $3 = $6
    (let ((entry-inv-7 (gncEntryCreate (gnc-get-current-book))))
      (gncEntrySetDateGDate entry-inv-7 (time64-to-gdate (current-time)))
      (gncEntrySetDescription entry-inv-7 "entry-inv-7-desc")
      (gncEntrySetAction entry-inv-7 "entry-inv-7-action")
      (gncEntrySetNotes entry-inv-7 "entry-inv-7-notes")
      (gncEntrySetBillAccount entry-inv-7 expense)
      (gncEntrySetDocQuantity entry-inv-7 2 #t)
      (gncEntrySetBillPrice entry-inv-7 3)
      (gncInvoiceAddEntry inv-7 entry-inv-7))

    (gncInvoicePostToAccount inv-1 receivable
                             (gnc-dmy2time64 1 9 1980)
                             (gnc-dmy2time64 1 9 1980)
                             "cust-invoice"
                             #t #f)

    (gncInvoicePostToAccount inv-2 receivable
                             (gnc-dmy2time64 2 9 1980)
                             (gnc-dmy2time64 3 9 1980)
                             "job-invoice"
                             #t #f)

    (gncInvoicePostToAccount inv-3 payable
                             (gnc-dmy2time64 3 9 1980)
                             (gnc-dmy2time64 3 9 1980)
                             "vendor-bill"
                             #t #f)

    (gncInvoicePostToAccount inv-4 payable
                             (gnc-dmy2time64 4 9 1980)
                             (gnc-dmy2time64 4 9 1980)
                             "emp-bill"
                             #t #f)

    (gncInvoicePostToAccount inv-5 receivable
                             (gnc-dmy2time64 5 9 1980)
                             (gnc-dmy2time64 5 9 1980)
                             "cust-credit-note"
                             #t #f)

    (gncInvoicePostToAccount inv-6 payable
                             (gnc-dmy2time64 6 9 1980)
                             (gnc-dmy2time64 6 9 1980)
                             "vend-credit-note"
                             #t #f)

    (gncInvoicePostToAccount inv-7 payable
                             (gnc-dmy2time64 7 9 1980)
                             (gnc-dmy2time64 7 9 1980)
                             "emp-credit-note"
                             #t #f)

    (let* ((taxrate 109/10)
           (discount 7/2)
           (unitprice 777/4)
           (quantity 11)
           (combo-vat-sales-tt
            (let ((tt (gncTaxTableCreate (gnc-get-current-book))))
              (gncTaxTableIncRef tt)
              (gncTaxTableSetName tt (format #f "~a% vat on sales" taxrate))
              (let ((entry (gncTaxTableEntryCreate)))
                (gncTaxTableEntrySetAccount entry vat-sales)
                (gncTaxTableEntrySetType entry GNC-AMT-TYPE-PERCENT)
                (gncTaxTableEntrySetAmount entry taxrate)
                (gncTaxTableAddEntry tt entry))
              tt))
           (order (let ((order (gncOrderCreate (gnc-get-current-book))))
                    (gncOrderSetID order "order-id")
                    (gncOrderSetOwner order owner-1)
                    (gncOrderSetReference order "order-ref")
                    (gncOrderSetActive order #t)
                    order))
           (billterm (let ((term (gncBillTermCreate (gnc-get-current-book))))
                       (gncBillTermSetName term "billterm-name")
                       (gncBillTermSetDescription term "billterm-desc")
                       (gncBillTermSetType term 1) ;1 = GNC-TERM-TYPE-DAYS
                       (gncBillTermSetDueDays term 8)
                       term)))
      (gncInvoiceSetTerms inv-8 billterm)
      (for-each
       (lambda (combo)
         (let* ((each-entry (gncEntryCreate (gnc-get-current-book)))
                (taxable? (= (vector-ref combo 0) 1))
                (tax-included? (= (vector-ref combo 1) 1))
                (discount-type (vector-ref combo 2))
                (discount-how (vector-ref combo 3))
                (desc (format #f "taxable=~a tax-included=~a discount-type=~a discount-how=~a"
                              (if taxable? "Y" "N")
                              (if tax-included? "Y" "N")
                              (gncAmountTypeToString discount-type)
                              (gncEntryDiscountHowToString discount-how))))
           (gncEntrySetDateGDate each-entry (time64-to-gdate (current-time)))
           (gncEntrySetDescription each-entry desc)
           (gncEntrySetAction each-entry "action")
           (gncEntrySetInvAccount each-entry income)
           (gncEntrySetDocQuantity each-entry quantity #f)
           (gncEntrySetInvPrice each-entry unitprice)
           (gncEntrySetInvDiscount each-entry discount)
           (gncEntrySetInvDiscountType each-entry discount-type)
           (gncEntrySetInvDiscountHow each-entry discount-how)
           (gncEntrySetInvTaxable each-entry taxable?)
           (gncEntrySetInvTaxIncluded each-entry tax-included?)
           (gncEntrySetInvTaxTable each-entry combo-vat-sales-tt)
           (gncOrderAddEntry order each-entry)
           ;; FIXME: Note: The following function hides a subtle
           ;; bug. It aims to retrieve & dump the entry description
           ;; and amount. Unfortunately the (gncEntryGetDocValue)
           ;; function will subtly modify the entry amounts by a
           ;; fraction; this means that the subsequent invoice payment
           ;; will not make the invoice amount completely zero. If the
           ;; following statement is uncommented, test-invoice will
           ;; fail because the (gncInvoiceIsPaid) final test will
           ;; fail.
           ;; (format #t "inv-8: adding ~a to invoice, entry amount is ~a\n"
           ;;         desc
           ;;         (exact->inexact (gncEntryGetDocValue each-entry #f #t #f)))
           (gncInvoiceAddEntry inv-8 each-entry)))
       (list
        ;; the following list specifies combinations to test gncEntry options
        ;; thanks to rgmerk and to jenny for idea how to generate list of options
        ;; (vector Taxable?(1=#t) Tax-included?(1=#t) DiscountType DiscountHow)
        (vector 1 2 1 1)
        (vector 2 1 2 2)
        (vector 1 1 2 3)
        (vector 2 2 1 3)
        (vector 2 1 1 1)
        (vector 1 2 2 2)
        (vector 1 2 1 2)
        (vector 1 1 2 1)))

      (gncInvoiceSetNotes
       inv-8 (format #f "tax=~a%, discount=~a, qty=~a, price=~a"
                     taxrate discount quantity unitprice))

      (gncInvoicePostToAccount inv-8 receivable
                               (gnc-dmy2time64 8 9 1980)
                               (gnc-dmy2time64 8 9 1980)
                               "trans-posting-memo"
                               #t #f)

      (gncInvoiceApplyPayment inv-8 '() bank 1747918/100 1
                              (gnc-dmy2time64 10 9 1980)
                              "trans-payment-memo-1"
                              "trans-payment-num-1"))

    (vector inv-1 inv-2 inv-3 inv-4 inv-5 inv-6 inv-7 inv-8)))

(define-public (gnc:create-budget-and-transactions env account-alist)
  (let* ((book (gnc-get-current-book))
         (budget (gnc-budget-new book))
         (bank (cdr (assoc "Bank" account-alist)))
         (income (cdr (assoc "Income" account-alist)))
         (equity (cdr (assoc "Equity" account-alist)))
         (expense (cdr (assoc "Expenses" account-alist))))
    (gnc-budget-set-name budget "test budget")
    (gnc-budget-begin-edit budget)
    (gnc-budget-set-num-periods budget 6)
    (gnc-budget-set-account-period-value budget bank 0 20)
    (gnc-budget-set-account-period-value budget bank 1 40)
    (gnc-budget-set-account-period-value budget bank 3 60)
    (gnc-budget-set-account-period-value budget expense 1 30)
    (gnc-budget-set-account-period-value budget expense 2 20)
    (gnc-budget-set-account-period-value budget expense 3 40)
    (gnc-budget-set-account-period-value budget income 0 -55)
    (gnc-budget-set-account-period-value budget income 2 -65)
    (gnc-budget-set-account-period-value budget income 3 -75)
    (gnc-budget-commit-edit budget)
    (let ((midperiod (lambda (period)
                       (floor (/ (+ (gnc-budget-get-period-start-date budget period)
                                    (gnc-budget-get-period-end-date budget period))
                                 2)))))
      (env-create-transaction env (midperiod 0) bank income 55)
      (env-create-transaction env (midperiod 2) bank income 67)
      (env-create-transaction env (midperiod 3) bank income 77)
      (env-create-transaction env (midperiod 0) expense bank 20)
      (env-create-transaction env (midperiod 1) expense bank 20)
      (let ((clos (env-create-transaction env (midperiod 1) income equity 55)))
        (xaccTransSetIsClosingTxn clos #t)))
    (gnc-budget-set-account-period-note budget income 0 "income-0 -$60")
    (gnc-budget-set-account-period-note budget expense 1 "expense-1 $25")
    budget))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; various stock transactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; This function aims to replicate the stock-split process in
;; gnc_stock_split_assistant_finish in assistant-stock-split.c. It
;; creates a 1 or 3-split transaction, and possibly a pricedb entry.
(define (stock-split account date shares description
                     ;; price-amount may be #f
                     price-amount pricecurrency
                     ;; cash-in-lieu, cash-amount may be #f
                     cash-amount cash-memo cash-income cash-asset)
  (let* ((book (gnc-get-current-book))
         (accounts '())
         (trans (xaccMallocTransaction book)))
    (xaccTransBeginEdit trans)
    (xaccTransSetCurrency trans (gnc-default-currency))
    (xaccTransSetDatePostedSecsNormalized trans date)
    (xaccTransSetDescription trans description)

    (let ((stocksplit (xaccMallocSplit book)))
      (xaccAccountBeginEdit account)
      (set! accounts (cons account accounts))
      (xaccSplitSetAccount stocksplit account)
      (xaccSplitSetAmount stocksplit shares)
      (xaccSplitMakeStockSplit stocksplit)
      (xaccSplitSetAction stocksplit "Split")
      (xaccSplitSetParent stocksplit trans))

    ;; add pricedb
    (when price-amount
      (let ((price (gnc-price-create book)))
        (gnc-price-begin-edit price)
        (gnc-price-set-commodity price (xaccAccountGetCommodity account))
        (gnc-price-set-currency price pricecurrency)
        (gnc-price-set-time64 price date)
        (gnc-price-set-source price PRICE-SOURCE-STOCK-SPLIT)
        (gnc-price-set-typestr price "unknown")
        (gnc-price-set-value price price-amount)
        (gnc-price-commit-edit price)
        (gnc-pricedb-add-price (gnc-pricedb-get-db book) price)))

    ;; cash-in-lieu
    (when cash-amount
      (let ((asset-split (xaccMallocSplit book)))
        (xaccAccountBeginEdit cash-asset)
        (set! accounts (cons cash-asset accounts))
        (xaccSplitSetAccount asset-split cash-asset)
        (xaccSplitSetParent asset-split trans)
        (xaccSplitSetAmount asset-split cash-amount)
        (xaccSplitSetValue asset-split cash-amount)
        (xaccSplitSetMemo asset-split cash-memo))

      (let ((income-split (xaccMallocSplit book)))
        (xaccAccountBeginEdit cash-income)
        (set! accounts (cons cash-income accounts))
        (xaccSplitSetAccount income-split cash-income)
        (xaccSplitSetParent income-split trans)
        (xaccSplitSetAmount income-split (- cash-amount))
        (xaccSplitSetValue income-split (- cash-amount))
        (xaccSplitSetMemo income-split cash-memo)))

    (xaccTransCommitEdit trans)
    (for-each xaccAccountCommitEdit accounts)
    trans))

(define-public (create-stock-test-data)
  (define structure
    (list "Root" (list (cons 'type ACCT-TYPE-ASSET))
          (list "Asset"
                (list "Bank"))
          (list "Income" (list (cons 'type ACCT-TYPE-INCOME)))
          (list "Expenses" (list (cons 'type ACCT-TYPE-EXPENSE)))
          (list "Equity" (list (cons 'type ACCT-TYPE-EQUITY)))
          (list "Broker"
                (list "AAPL" (list (cons 'type ACCT-TYPE-STOCK)))
                (list "MSFT" (list (cons 'type ACCT-TYPE-STOCK)))
                (list "TSLA" (list (cons 'type ACCT-TYPE-STOCK))))))
  (let* ((env (create-test-env))
         (book (gnc-get-current-book))
         (comm-table (gnc-commodity-table-get-table book))
         (AAPL (gnc-commodity-new book "Apple" "NASDAQ" "AAPL" "" 1))
         (MSFT (gnc-commodity-new book "Microsoft" "NASDAQ" "MSFT" "" 1))
         (TSLA (gnc-commodity-new book "Tesla Motors" "NASDAQ" "TSLA" "" 1))
         (account-alist (env-create-account-structure-alist env structure))
         (bank (cdr (assoc "Bank" account-alist)))
         (inco (cdr (assoc "Income" account-alist)))
         (expe (cdr (assoc "Expenses" account-alist)))
         (equity (cdr (assoc "Equity" account-alist)))
         (aapl (cdr (assoc "AAPL" account-alist)))
         (msft (cdr (assoc "MSFT" account-alist)))
         (tsla (cdr (assoc "TSLA" account-alist)))
         (YEAR (gnc:time64-get-year (gnc:get-today))))

    ;; Set account commodities
    (gnc-commodity-table-insert comm-table AAPL)
    (gnc-commodity-table-insert comm-table MSFT)
    (gnc-commodity-table-insert comm-table TSLA)
    (xaccAccountSetCommodity aapl AAPL)
    (xaccAccountSetCommodity msft MSFT)
    (xaccAccountSetCommodity tsla TSLA)

    (env-transfer env 01 01 1980 equity bank 10000 #:description "seed money")

    (env-create-multisplit-transaction
     env 01 02 1980
     (list (vector bank  -100 -100)
           (vector aapl   100    1))
     #:description "buy 1 AAPL @ $100")

    (env-create-multisplit-transaction
     env 01 03 1980
     (list (vector bank  -200 -200)
           (vector aapl   200    1))
     #:description "buy 1 AAPL @ $200")

    (env-create-multisplit-transaction
     env 01 05 1980
     (list (vector bank   390  390)
           (vector aapl  -400   -1)
           (vector inco  -300 -300)
           (vector expe    10   10)
           (vector aapl   300    0))
     #:description "sell 1 AAPL @ $400 FIFO, brokerage fee = $10, into bank = $390")

    ;; until 1.5.1980 the account has usual buy/sell txns only, no stock splits
    ;; there's only 1 AAPL left, price $400

    ;; on 1.10.1980: stock split, 1 AAPL -> 10 AAPL
    ;; prev price was $400, now is $40
    (stock-split aapl
                 (gnc-dmy2time64 1 10 1980)
                 9 "first 1:10 stock split"
                 40 (gnc-account-get-currency-or-parent aapl)
                 #f #f #f #f)

    ;; on 1.11.1980: another stock split, 10 AAPL -> 100 AAPL
    ;; prev price was $40, now is $4
    (stock-split aapl
                 (gnc-dmy2time64 1 11 1980)
                 90 "another 1:10 stock split"
                 4 (gnc-account-get-currency-or-parent aapl)
                 #f #f #f #f)

    ;; on 1.12.1980: 3:1 stock split, 100 AAPL -> 33 AAPL
    ;; prev price was $4, now is $12, with cash-in-lieu $4
    (stock-split aapl
                 (gnc-dmy2time64 1 12 1980)
                 -67 "3:1 stock split with cash-in-lieu $4"
                 12 (gnc-account-get-currency-or-parent aapl)
                 4 "cash-in-lieu" inco bank)

    (env-create-multisplit-transaction
     env 01 01 1981
     (list (vector bank -500 -500)
           (vector aapl  500   10))
     #:description "buy 10 AAPL @ $5")

    (env-create-multisplit-transaction
     env 1 3 1981
     (list (vector bank     3    3)
           (vector aapl    -3 -1/2)
           (vector inco  -5/2 -5/2)
           (vector aapl   5/2    0))
     #:description "sell 1/2 AAPL @ $6 FIFO, capgain = $2.50 into bank = $200")

    ;; FIXME: spin off $150 from AAPL is coded correctly? there's no
    ;; INCOME split?
    (env-create-multisplit-transaction
     env 1 4 1981
     (list (vector bank   150  150)
           (vector aapl  -150    0))
     #:description "spin-off $150")

    account-alist))

