# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/24

#  Copyright (c) 2023 imacat.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
"""The test for the journal entry management.

"""
import datetime as dt
import unittest
from decimal import Decimal

import httpx
from flask import Flask

from accounting.utils.next_uri import encode_next
from test_site import db
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
    get_csrf_token, add_journal_entry, match_journal_entry_detail
from testlib_journal_entry import NON_EMPTY_NOTE, EMPTY_NOTE, \
    get_add_form, get_unchanged_update_form, get_update_form, \
    set_negative_amount, remove_debit_in_a_currency, \
    remove_credit_in_a_currency

PREFIX: str = "/accounting/journal-entries"
"""The URL prefix for the journal entry management."""
RETURN_TO_URI: str = "/accounting"
"""The URL to return to after the operation."""


class CashReceiptJournalEntryTestCase(unittest.TestCase):
    """The cash receipt journal entry test case."""

    def setUp(self) -> None:
        """Sets up the test.
        This is run once per test.

        :return: None.
        """
        self.__app: Flask = create_test_app()
        """The Flask application."""

        with self.__app.app_context():
            from accounting.models import JournalEntry, JournalEntryLineItem
            JournalEntry.query.delete()
            JournalEntryLineItem.query.delete()
            self.__encoded_next_uri: str = encode_next(NEXT_URI)
            """The encoded next URI."""

        self.__client: httpx.Client = get_client(self.__app, "editor")
        """The user client."""
        self.__csrf_token: str = get_csrf_token(self.__client)
        """The CSRF token."""

    def test_nobody(self) -> None:
        """Test the permission as nobody.

        :return: None.
        """
        client: httpx.Client = get_client(self.__app, "nobody")
        csrf_token: str = get_csrf_token(client)
        journal_entry_id: int = add_journal_entry(self.__client,
                                                  self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        add_form["csrf_token"] = csrf_token
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        update_form["csrf_token"] = csrf_token
        response: httpx.Response

        response = client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/create/receipt")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/store/receipt", data=add_form)
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/update",
                               data=update_form)
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/delete",
                               data={"csrf_token": csrf_token})
        self.assertEqual(response.status_code, 403)

    def test_viewer(self) -> None:
        """Test the permission as viewer.

        :return: None.
        """
        client: httpx.Client = get_client(self.__app, "viewer")
        csrf_token: str = get_csrf_token(client)
        journal_entry_id: int = add_journal_entry(self.__client,
                                                  self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        add_form["csrf_token"] = csrf_token
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        update_form["csrf_token"] = csrf_token
        response: httpx.Response

        response = client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 200)

        response = client.get(f"{PREFIX}/create/receipt")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/store/receipt", data=add_form)
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/update",
                               data=update_form)
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/delete",
                               data={"csrf_token": csrf_token})
        self.assertEqual(response.status_code, 403)

    def test_editor(self) -> None:
        """Test the permission as editor.

        :return: None.
        """
        journal_entry_id: int = add_journal_entry(self.__client,
                                                  self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        response: httpx.Response

        response = self.__client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 200)

        response = self.__client.get(f"{PREFIX}/create/receipt")
        self.assertEqual(response.status_code, 200)

        response = self.__client.post(f"{PREFIX}/store/receipt",
                                      data=add_form)
        self.assertEqual(response.status_code, 302)
        match_journal_entry_detail(response.headers["Location"])

        response = self.__client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 200)

        response = self.__client.post(f"{PREFIX}/{journal_entry_id}/update",
                                      data=update_form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"],
                         f"{PREFIX}/{journal_entry_id}?"
                         f"next={self.__encoded_next_uri}")

        response = self.__client.post(f"{PREFIX}/{journal_entry_id}/delete",
                                      data={"csrf_token": self.__csrf_token})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], RETURN_TO_URI)

    def test_add(self) -> None:
        """Tests to add the journal entries.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        create_uri: str = (f"{PREFIX}/create/receipt?"
                           f"next={self.__encoded_next_uri}")
        store_uri: str = f"{PREFIX}/store/receipt"
        response: httpx.Response
        form: dict[str, str]
        journal_entry: JournalEntry | None

        # No currency content
        form = self.__get_add_form()
        form = {x: form[x] for x in form if not x.startswith("currency-")}
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Missing currency
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = ""
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-existing currency
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = "ZZZ"
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # No credit content in a currency
        form = self.__get_add_form()
        remove_credit_in_a_currency(form)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-existing account
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-account_code")][0]
        form[key] = "9999-999"
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-credit account
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.OFFICE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # A receivable line item cannot start from credit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.RECEIVABLE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Negative amount
        form = self.__get_add_form()
        set_negative_amount(form)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Success
        response = self.__client.post(store_uri,
                                      data=self.__get_add_form())
        self.assertEqual(response.status_code, 302)
        journal_entry_id: int \
            = match_journal_entry_detail(response.headers["Location"])

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies), 3)

            self.assertEqual(currencies[0].code, "JPY")
            self.assertEqual(len(currencies[0].debit), 1)
            self.assertEqual(currencies[0].debit[0].no, 1)
            self.assertEqual(currencies[0].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[0].debit[0].description)
            self.assertEqual(currencies[0].debit[0].amount,
                             sum([x.amount for x in currencies[0].credit]))
            self.assertEqual(len(currencies[0].credit), 2)
            self.assertEqual(currencies[0].credit[0].no, 1)
            self.assertEqual(currencies[0].credit[0].account.code,
                             Accounts.DONATION)
            self.assertEqual(currencies[0].credit[1].no, 2)
            self.assertEqual(currencies[0].credit[1].account.code,
                             Accounts.AGENCY)

            self.assertEqual(currencies[1].code, "USD")
            self.assertEqual(len(currencies[1].debit), 1)
            self.assertEqual(currencies[1].debit[0].no, 2)
            self.assertEqual(currencies[1].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[1].debit[0].description)
            self.assertEqual(currencies[1].debit[0].amount,
                             sum([x.amount for x in currencies[1].credit]))
            self.assertEqual(len(currencies[1].credit), 3)
            self.assertEqual(currencies[1].credit[0].no, 3)
            self.assertEqual(currencies[1].credit[0].account.code,
                             Accounts.SERVICE)
            self.assertEqual(currencies[1].credit[1].no, 4)
            self.assertEqual(currencies[1].credit[1].account.code,
                             Accounts.SALES)
            self.assertEqual(currencies[1].credit[2].no, 5)
            self.assertEqual(currencies[1].credit[2].account.code,
                             Accounts.INTEREST)

            self.assertEqual(currencies[2].code, "TWD")
            self.assertEqual(len(currencies[2].debit), 1)
            self.assertEqual(currencies[2].debit[0].no, 3)
            self.assertEqual(currencies[2].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[2].debit[0].description)
            self.assertEqual(currencies[2].debit[0].amount,
                             sum([x.amount for x in currencies[2].credit]))
            self.assertEqual(len(currencies[2].credit), 2)
            self.assertEqual(currencies[2].credit[0].no, 6)
            self.assertEqual(currencies[2].credit[0].account.code,
                             Accounts.RENT_INCOME)
            self.assertEqual(currencies[2].credit[1].no, 7)
            self.assertEqual(currencies[2].credit[1].account.code,
                             Accounts.DONATION)

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

        # Success, with empty note
        form = self.__get_add_form()
        form["note"] = EMPTY_NOTE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        journal_entry_id: int \
            = match_journal_entry_detail(response.headers["Location"])

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            self.assertIsNone(journal_entry.note)

    def test_basic_update(self) -> None:
        """Tests the basic rules to update a journal entry.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        edit_uri: str = (f"{PREFIX}/{journal_entry_id}/edit?"
                         f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        form_0: dict[str, str] = self.__get_update_form(journal_entry_id)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies0: list[JournalEntryCurrency] = journal_entry.currencies
            old_id: set[int] = set()
            for currency in currencies0:
                old_id.update({x.id for x in currency.debit})

        # No currency content
        form = form_0.copy()
        form = {x: form[x] for x in form if not x.startswith("currency-")}
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Missing currency
        form = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = ""
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-existing currency
        form = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = "ZZZ"
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # No credit content in a currency
        form = form_0.copy()
        remove_credit_in_a_currency(form)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-existing account
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-account_code")][0]
        form[key] = "9999-999"
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-credit account
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.OFFICE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # A receivable line item cannot start from credit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.RECEIVABLE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Negative amount
        form: dict[str, str] = form_0.copy()
        set_negative_amount(form)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Success
        response = self.__client.post(update_uri, data=form_0)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies1: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies1), 3)

            self.assertEqual(currencies1[0].code, "AUD")
            self.assertEqual(len(currencies1[0].debit), 1)
            self.assertNotIn(currencies1[0].debit[0].id, old_id)
            self.assertEqual(currencies1[0].debit[0].no, 1)
            self.assertEqual(currencies1[0].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[0].debit[0].description)
            self.assertEqual(currencies1[0].debit[0].amount,
                             sum([x.amount for x in currencies1[0].credit]))
            self.assertEqual(len(currencies1[0].credit), 2)
            self.assertNotIn(currencies1[0].credit[0].id, old_id)
            self.assertEqual(currencies1[0].credit[0].no, 1)
            self.assertEqual(currencies1[0].credit[0].account.code,
                             Accounts.DONATION)
            self.assertNotIn(currencies1[0].credit[1].id, old_id)
            self.assertEqual(currencies1[0].credit[1].no, 2)
            self.assertEqual(currencies1[0].credit[1].account.code,
                             Accounts.RENT_INCOME)

            self.assertEqual(currencies1[1].code, "EUR")
            self.assertEqual(len(currencies1[1].debit), 1)
            self.assertNotIn(currencies1[1].debit[0].id, old_id)
            self.assertEqual(currencies1[1].debit[0].no, 2)
            self.assertEqual(currencies1[1].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[1].debit[0].description)
            self.assertEqual(currencies1[1].debit[0].amount,
                             sum([x.amount for x in currencies1[1].credit]))
            self.assertEqual(len(currencies1[1].credit), 2)
            self.assertEqual(currencies1[1].credit[0].id,
                             currencies0[2].credit[0].id)
            self.assertEqual(currencies1[1].credit[0].no, 3)
            self.assertEqual(currencies1[1].credit[0].account.code,
                             Accounts.RENT_INCOME)
            self.assertEqual(currencies1[1].credit[1].id,
                             currencies0[2].credit[1].id)
            self.assertEqual(currencies1[1].credit[1].no, 4)
            self.assertEqual(currencies1[1].credit[1].account.code,
                             Accounts.DONATION)

            self.assertEqual(currencies1[2].code, "USD")
            self.assertEqual(len(currencies1[2].debit), 1)
            self.assertEqual(currencies1[2].debit[0].id,
                             currencies0[1].debit[0].id)
            self.assertEqual(currencies1[2].debit[0].no, 3)
            self.assertEqual(currencies1[2].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[2].debit[0].description)
            self.assertEqual(currencies1[2].debit[0].amount,
                             sum([x.amount for x in currencies1[2].credit]))
            self.assertEqual(len(currencies1[2].credit), 3)
            self.assertNotIn(currencies1[2].credit[0].id, old_id)
            self.assertEqual(currencies1[2].credit[0].no, 5)
            self.assertEqual(currencies1[2].credit[0].account.code,
                             Accounts.AGENCY)
            self.assertEqual(currencies1[2].credit[1].id,
                             currencies0[1].credit[2].id)
            self.assertEqual(currencies1[2].credit[1].no, 6)
            self.assertEqual(currencies1[2].credit[1].account.code,
                             Accounts.INTEREST)
            self.assertEqual(currencies1[2].credit[2].id,
                             currencies0[1].credit[0].id)
            self.assertEqual(currencies1[2].credit[2].no, 7)
            self.assertEqual(currencies1[2].credit[2].account.code,
                             Accounts.SERVICE)

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

    def test_update_not_modified(self) -> None:
        """Tests that the data is not modified.

        :return: None.
        """
        from accounting.models import JournalEntry
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        journal_entry: JournalEntry
        response: httpx.Response

        response = self.__client.post(
            update_uri,
            data=self.__get_unchanged_update_form(journal_entry_id))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            journal_entry.created_at \
                = journal_entry.created_at - dt.timedelta(seconds=5)
            journal_entry.updated_at = journal_entry.created_at
            db.session.commit()

        response = self.__client.post(
            update_uri, data=self.__get_update_form(journal_entry_id))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            self.assertLess(journal_entry.created_at, journal_entry.updated_at)

    def test_created_updated_by(self) -> None:
        """Tests the created-by and updated-by record.

        :return: None.
        """
        from accounting.models import JournalEntry
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        editor_username, admin_username = "editor", "admin"
        client: httpx.Client = get_client(self.__app, admin_username)
        csrf_token: str = get_csrf_token(client)
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        journal_entry: JournalEntry
        response: httpx.Response

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertEqual(journal_entry.created_by.username,
                             editor_username)
            self.assertEqual(journal_entry.updated_by.username,
                             editor_username)

        form: dict[str, str] = self.__get_update_form(journal_entry_id)
        form["csrf_token"] = csrf_token
        response = client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertEqual(journal_entry.created_by.username,
                             editor_username)
            self.assertEqual(journal_entry.updated_by.username,
                             admin_username)

    def test_delete(self) -> None:
        """Tests to delete a journal entry.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryLineItem
        journal_entry_id_1: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id_1}?"
                           f"next={self.__encoded_next_uri}")
        delete_uri: str = f"{PREFIX}/{journal_entry_id_1}/delete"
        response: httpx.Response

        form: dict[str, str] = self.__get_add_form()
        key: str = [x for x in form if x.endswith("-account_code")][0]
        form[key] = Accounts.PAYABLE
        journal_entry_id_2: int = add_journal_entry(self.__client, form)
        with self.__app.app_context():
            journal_entry: JournalEntry | None \
                = db.session.get(JournalEntry, journal_entry_id_2)
            self.assertIsNotNone(journal_entry)
            line_item: JournalEntryLineItem \
                = [x for x in journal_entry.line_items
                   if x.account_code == Accounts.PAYABLE][0]
        add_journal_entry(
            self.__client,
            form={"csrf_token": self.__csrf_token,
                  "next": self.__encoded_next_uri,
                  "date": dt.date.today().isoformat(),
                  "currency-1-code": line_item.currency_code,
                  "currency-1-debit-1-original_line_item_id": line_item.id,
                  "currency-1-debit-1-account_code": line_item.account_code,
                  "currency-1-debit-1-amount": "1"})

        # Cannot delete the journal entry that is in use
        response = self.__client.post(f"{PREFIX}/{journal_entry_id_2}/delete",
                                      data={"csrf_token": self.__csrf_token,
                                            "next": self.__encoded_next_uri})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"],
                         f"{PREFIX}/{journal_entry_id_2}?"
                         f"next={self.__encoded_next_uri}")

        # Success
        response = self.__client.get(detail_uri)
        self.assertEqual(response.status_code, 200)
        response = self.__client.post(delete_uri,
                                      data={"csrf_token": self.__csrf_token,
                                            "next": self.__encoded_next_uri})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], NEXT_URI)

        response = self.__client.get(detail_uri)
        self.assertEqual(response.status_code, 404)
        response = self.__client.post(delete_uri,
                                      data={"csrf_token": self.__csrf_token,
                                            "next": self.__encoded_next_uri})
        self.assertEqual(response.status_code, 404)

    def __get_add_form(self) -> dict[str, str]:
        """Returns the form data to add a new journal entry.

        :return: The form data to add a new journal entry.
        """
        form: dict[str, str] = get_add_form(self.__csrf_token,
                                            self.__encoded_next_uri)
        form = {x: form[x] for x in form if "-debit-" not in x}
        return form

    def __get_unchanged_update_form(self, journal_entry_id: int) \
            -> dict[str, str]:
        """Returns the form data to update a journal entry, where the data are
        not changed.

        :param journal_entry_id: The journal entry ID.
        :return: The form data to update the journal entry, where the data are
            not changed.
        """
        form: dict[str, str] = get_unchanged_update_form(
            journal_entry_id, self.__app, self.__csrf_token,
            self.__encoded_next_uri)
        form = {x: form[x] for x in form if "-debit-" not in x}
        return form

    def __get_update_form(self, journal_entry_id: int) -> dict[str, str]:
        """Returns the form data to update a journal entry, where the data are
        changed.

        :param journal_entry_id: The journal entry ID.
        :return: The form data to update the journal entry, where the data are
            changed.
        """
        form: dict[str, str] = get_update_form(
            journal_entry_id, self.__app, self.__csrf_token,
            self.__encoded_next_uri, False)
        form = {x: form[x] for x in form if "-debit-" not in x}
        return form


class CashDisbursementJournalEntryTestCase(unittest.TestCase):
    """The cash disbursement journal entry test case."""

    def setUp(self) -> None:
        """Sets up the test.
        This is run once per test.

        :return: None.
        """
        self.__app: Flask = create_test_app()
        """The Flask application."""

        with self.__app.app_context():
            from accounting.models import JournalEntry, JournalEntryLineItem
            JournalEntry.query.delete()
            JournalEntryLineItem.query.delete()
            self.__encoded_next_uri: str = encode_next(NEXT_URI)
            """The encoded next URI."""

        self.__client: httpx.Client = get_client(self.__app, "editor")
        """The user client."""
        self.__csrf_token: str = get_csrf_token(self.__client)
        """The CSRF token."""

    def test_nobody(self) -> None:
        """Test the permission as nobody.

        :return: None.
        """
        client: httpx.Client = get_client(self.__app, "nobody")
        csrf_token: str = get_csrf_token(client)
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        add_form["csrf_token"] = csrf_token
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        update_form["csrf_token"] = csrf_token
        response: httpx.Response

        response = client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/create/disbursement")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/store/disbursement", data=add_form)
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/update",
                               data=update_form)
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/delete",
                               data={"csrf_token": csrf_token})
        self.assertEqual(response.status_code, 403)

    def test_viewer(self) -> None:
        """Test the permission as viewer.

        :return: None.
        """
        client: httpx.Client = get_client(self.__app, "viewer")
        csrf_token: str = get_csrf_token(client)
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        add_form["csrf_token"] = csrf_token
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        update_form["csrf_token"] = csrf_token
        response: httpx.Response

        response = client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 200)

        response = client.get(f"{PREFIX}/create/disbursement")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/store/disbursement", data=add_form)
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/update",
                               data=update_form)
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/delete",
                               data={"csrf_token": csrf_token})
        self.assertEqual(response.status_code, 403)

    def test_editor(self) -> None:
        """Test the permission as editor.

        :return: None.
        """
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        response: httpx.Response

        response = self.__client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 200)

        response = self.__client.get(f"{PREFIX}/create/disbursement")
        self.assertEqual(response.status_code, 200)

        response = self.__client.post(f"{PREFIX}/store/disbursement",
                                      data=add_form)
        self.assertEqual(response.status_code, 302)
        match_journal_entry_detail(response.headers["Location"])

        response = self.__client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 200)

        response = self.__client.post(f"{PREFIX}/{journal_entry_id}/update",
                                      data=update_form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"],
                         f"{PREFIX}/{journal_entry_id}?"
                         f"next={self.__encoded_next_uri}")

        response = self.__client.post(f"{PREFIX}/{journal_entry_id}/delete",
                                      data={"csrf_token": self.__csrf_token})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], RETURN_TO_URI)

    def test_add(self) -> None:
        """Tests to add the journal entries.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        create_uri: str = (f"{PREFIX}/create/disbursement?"
                           f"next={self.__encoded_next_uri}")
        store_uri: str = f"{PREFIX}/store/disbursement"
        response: httpx.Response
        form: dict[str, str]
        journal_entry: JournalEntry | None

        # No currency content
        form = self.__get_add_form()
        form = {x: form[x] for x in form if not x.startswith("currency-")}
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Missing currency
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = ""
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-existing currency
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = "ZZZ"
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # No debit content in a currency
        form = self.__get_add_form()
        remove_debit_in_a_currency(form)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-existing account
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-account_code")][0]
        form[key] = "9999-999"
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-debit account
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.SERVICE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # A payable line item cannot start from debit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.PAYABLE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Negative amount
        form = self.__get_add_form()
        set_negative_amount(form)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Success
        response = self.__client.post(store_uri,
                                      data=self.__get_add_form())
        self.assertEqual(response.status_code, 302)
        journal_entry_id: int \
            = match_journal_entry_detail(response.headers["Location"])

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies), 3)

            self.assertEqual(currencies[0].code, "JPY")
            self.assertEqual(len(currencies[0].debit), 2)
            self.assertEqual(currencies[0].debit[0].no, 1)
            self.assertEqual(currencies[0].debit[0].account.code,
                             Accounts.CASH)
            self.assertEqual(currencies[0].debit[1].no, 2)
            self.assertEqual(currencies[0].debit[1].account.code,
                             Accounts.BANK)
            self.assertEqual(len(currencies[0].credit), 1)
            self.assertEqual(currencies[0].credit[0].no, 1)
            self.assertEqual(currencies[0].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[0].credit[0].description)
            self.assertEqual(currencies[0].credit[0].amount,
                             sum([x.amount for x in currencies[0].debit]))

            self.assertEqual(currencies[1].code, "USD")
            self.assertEqual(len(currencies[1].debit), 3)
            self.assertEqual(currencies[1].debit[0].no, 3)
            self.assertEqual(currencies[1].debit[0].account.code,
                             Accounts.BANK)
            self.assertEqual(currencies[1].debit[0].description, "Deposit")
            self.assertEqual(currencies[1].debit[1].no, 4)
            self.assertEqual(currencies[1].debit[1].account.code,
                             Accounts.OFFICE)
            self.assertEqual(currencies[1].debit[1].description, "Pens")
            self.assertEqual(currencies[1].debit[2].no, 5)
            self.assertEqual(currencies[1].debit[2].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[1].debit[2].description)
            self.assertEqual(len(currencies[1].credit), 1)
            self.assertEqual(currencies[1].credit[0].no, 2)
            self.assertEqual(currencies[1].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[1].credit[0].description)
            self.assertEqual(currencies[1].credit[0].amount,
                             sum([x.amount for x in currencies[1].debit]))

            self.assertEqual(currencies[2].code, "TWD")
            self.assertEqual(len(currencies[2].debit), 2)
            self.assertEqual(currencies[2].debit[0].no, 6)
            self.assertEqual(currencies[2].debit[0].account.code,
                             Accounts.CASH)
            self.assertEqual(currencies[2].debit[1].no, 7)
            self.assertEqual(currencies[2].debit[1].account.code,
                             Accounts.TRAVEL)
            self.assertEqual(len(currencies[2].credit), 1)
            self.assertEqual(currencies[2].credit[0].no, 3)
            self.assertEqual(currencies[2].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[2].credit[0].description)
            self.assertEqual(currencies[2].credit[0].amount,
                             sum([x.amount for x in currencies[2].debit]))

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

        # Success, with empty note
        form = self.__get_add_form()
        form["note"] = EMPTY_NOTE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        journal_entry_id: int \
            = match_journal_entry_detail(response.headers["Location"])

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            self.assertIsNone(journal_entry.note)

    def test_basic_update(self) -> None:
        """Tests the basic rules to update a journal entry.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        edit_uri: str = (f"{PREFIX}/{journal_entry_id}/edit?"
                         f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        form_0: dict[str, str] = self.__get_update_form(journal_entry_id)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies0: list[JournalEntryCurrency] = journal_entry.currencies
            old_id: set[int] = set()
            for currency in currencies0:
                old_id.update({x.id for x in currency.debit})

        # No currency content
        form = form_0.copy()
        form = {x: form[x] for x in form if not x.startswith("currency-")}
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Missing currency
        form = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = ""
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-existing currency
        form = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = "ZZZ"
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # No debit content in a currency
        form = form_0.copy()
        remove_debit_in_a_currency(form)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-existing account
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-account_code")][0]
        form[key] = "9999-999"
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-debit account
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.SERVICE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # A payable line item cannot start from debit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.PAYABLE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Negative amount
        form: dict[str, str] = form_0.copy()
        set_negative_amount(form)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Success
        response = self.__client.post(update_uri, data=form_0)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies1: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies1), 3)

            self.assertEqual(currencies1[0].code, "AUD")
            self.assertEqual(len(currencies1[0].debit), 2)
            self.assertNotIn(currencies1[0].debit[0].id, old_id)
            self.assertEqual(currencies1[0].debit[0].no, 1)
            self.assertEqual(currencies1[0].debit[0].account.code,
                             Accounts.OFFICE)
            self.assertNotIn(currencies1[0].debit[1].id, old_id)
            self.assertEqual(currencies1[0].debit[1].no, 2)
            self.assertEqual(currencies1[0].debit[1].account.code,
                             Accounts.CASH)
            self.assertEqual(len(currencies1[0].credit), 1)
            self.assertNotIn(currencies1[0].credit[0].id, old_id)
            self.assertEqual(currencies1[0].credit[0].no, 1)
            self.assertEqual(currencies1[0].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[0].credit[0].description)
            self.assertEqual(currencies1[0].credit[0].amount,
                             sum([x.amount for x in currencies1[0].debit]))

            self.assertEqual(currencies1[1].code, "EUR")
            self.assertEqual(len(currencies1[1].debit), 2)
            self.assertEqual(currencies1[1].debit[0].id,
                             currencies0[2].debit[0].id)
            self.assertEqual(currencies1[1].debit[0].no, 3)
            self.assertEqual(currencies1[1].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[1].debit[0].description)
            self.assertEqual(currencies1[1].debit[1].id,
                             currencies0[2].debit[1].id)
            self.assertEqual(currencies1[1].debit[1].no, 4)
            self.assertEqual(currencies1[1].debit[1].account.code,
                             Accounts.TRAVEL)
            self.assertEqual(len(currencies1[1].credit), 1)
            self.assertNotIn(currencies1[1].credit[0].id, old_id)
            self.assertEqual(currencies1[1].credit[0].no, 2)
            self.assertEqual(currencies1[1].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[1].credit[0].description)
            self.assertEqual(currencies1[1].credit[0].amount,
                             sum([x.amount for x in currencies1[1].debit]))

            self.assertEqual(currencies1[2].code, "USD")
            self.assertEqual(len(currencies1[2].debit), 3)
            self.assertNotIn(currencies1[2].debit[0].id, old_id)
            self.assertEqual(currencies1[2].debit[0].no, 5)
            self.assertEqual(currencies1[2].debit[0].account.code,
                             Accounts.TRAVEL)
            self.assertIsNone(currencies1[2].debit[0].description)
            self.assertEqual(currencies1[2].debit[1].id,
                             currencies0[1].debit[2].id)
            self.assertEqual(currencies1[2].debit[1].no, 6)
            self.assertEqual(currencies1[2].debit[1].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[2].debit[1].description)
            self.assertEqual(currencies1[2].debit[2].id,
                             currencies0[1].debit[0].id)
            self.assertEqual(currencies1[2].debit[2].no, 7)
            self.assertEqual(currencies1[2].debit[2].account.code,
                             Accounts.BANK)
            self.assertEqual(currencies1[2].debit[2].description, "Deposit")
            self.assertEqual(len(currencies1[2].credit), 1)
            self.assertEqual(currencies1[2].credit[0].id,
                             currencies0[1].credit[0].id)
            self.assertEqual(currencies1[2].credit[0].no, 3)
            self.assertEqual(currencies1[2].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[2].credit[0].description)
            self.assertEqual(currencies1[2].credit[0].amount,
                             sum([x.amount for x in currencies1[2].debit]))

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

    def test_update_not_modified(self) -> None:
        """Tests that the data is not modified.

        :return: None.
        """
        from accounting.models import JournalEntry
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        journal_entry: JournalEntry
        response: httpx.Response

        response = self.__client.post(
            update_uri,
            data=self.__get_unchanged_update_form(journal_entry_id))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            journal_entry.created_at \
                = journal_entry.created_at - dt.timedelta(seconds=5)
            journal_entry.updated_at = journal_entry.created_at
            db.session.commit()

        response = self.__client.post(
            update_uri, data=self.__get_update_form(journal_entry_id))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            self.assertLess(journal_entry.created_at, journal_entry.updated_at)

    def test_created_updated_by(self) -> None:
        """Tests the created-by and updated-by record.

        :return: None.
        """
        from accounting.models import JournalEntry
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        editor_username, admin_username = "editor", "admin"
        client: httpx.Client = get_client(self.__app, admin_username)
        csrf_token: str = get_csrf_token(client)
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        journal_entry: JournalEntry
        response: httpx.Response

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertEqual(journal_entry.created_by.username,
                             editor_username)
            self.assertEqual(journal_entry.updated_by.username,
                             editor_username)

        form: dict[str, str] = self.__get_update_form(journal_entry_id)
        form["csrf_token"] = csrf_token
        response = client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertEqual(journal_entry.created_by.username,
                             editor_username)
            self.assertEqual(journal_entry.updated_by.username,
                             admin_username)

    def test_delete(self) -> None:
        """Tests to delete a journal entry.

        :return: None.
        """
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete"
        response: httpx.Response

        response = self.__client.get(detail_uri)
        self.assertEqual(response.status_code, 200)
        response = self.__client.post(delete_uri,
                                      data={"csrf_token": self.__csrf_token,
                                            "next": self.__encoded_next_uri})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], NEXT_URI)

        response = self.__client.get(detail_uri)
        self.assertEqual(response.status_code, 404)
        response = self.__client.post(delete_uri,
                                      data={"csrf_token": self.__csrf_token,
                                            "next": self.__encoded_next_uri})
        self.assertEqual(response.status_code, 404)

    def __get_add_form(self) -> dict[str, str]:
        """Returns the form data to add a new journal entry.

        :return: The form data to add a new journal entry.
        """
        form: dict[str, str] = get_add_form(self.__csrf_token,
                                            self.__encoded_next_uri)
        form = {x: form[x] for x in form if "-credit-" not in x}
        return form

    def __get_unchanged_update_form(self, journal_entry_id: int) \
            -> dict[str, str]:
        """Returns the form data to update a journal entry, where the data are
        not changed.

        :param journal_entry_id: The journal entry ID.
        :return: The form data to update the journal entry, where the data are
            not changed.
        """
        form: dict[str, str] = get_unchanged_update_form(
            journal_entry_id, self.__app, self.__csrf_token,
            self.__encoded_next_uri)
        form = {x: form[x] for x in form if "-credit-" not in x}
        return form

    def __get_update_form(self, journal_entry_id: int) -> dict[str, str]:
        """Returns the form data to update a journal entry, where the data are
        changed.

        :param journal_entry_id: The journal entry ID.
        :return: The form data to update the journal entry, where the data are
            changed.
        """
        form: dict[str, str] = get_update_form(
            journal_entry_id, self.__app, self.__csrf_token,
            self.__encoded_next_uri, True)
        form = {x: form[x] for x in form if "-credit-" not in x}
        return form


class TransferJournalEntryTestCase(unittest.TestCase):
    """The transfer journal entry test case."""

    def setUp(self) -> None:
        """Sets up the test.
        This is run once per test.

        :return: None.
        """
        self.__app: Flask = create_test_app()
        """The Flask application."""

        with self.__app.app_context():
            from accounting.models import JournalEntry, \
                JournalEntryLineItem
            JournalEntry.query.delete()
            JournalEntryLineItem.query.delete()
            self.__encoded_next_uri: str = encode_next(NEXT_URI)
            """The encoded next URI."""

        self.__client: httpx.Client = get_client(self.__app, "editor")
        """The user client."""
        self.__csrf_token: str = get_csrf_token(self.__client)
        """The CSRF token."""

    def test_nobody(self) -> None:
        """Test the permission as nobody.

        :return: None.
        """
        client: httpx.Client = get_client(self.__app, "nobody")
        csrf_token: str = get_csrf_token(client)
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        add_form["csrf_token"] = csrf_token
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        update_form["csrf_token"] = csrf_token
        response: httpx.Response

        response = client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/create/transfer")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/store/transfer", data=add_form)
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/update",
                               data=update_form)
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/delete",
                               data={"csrf_token": csrf_token})
        self.assertEqual(response.status_code, 403)

    def test_viewer(self) -> None:
        """Test the permission as viewer.

        :return: None.
        """
        client: httpx.Client = get_client(self.__app, "viewer")
        csrf_token: str = get_csrf_token(client)
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        add_form["csrf_token"] = csrf_token
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        update_form["csrf_token"] = csrf_token
        response: httpx.Response

        response = client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 200)

        response = client.get(f"{PREFIX}/create/transfer")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/store/transfer", data=add_form)
        self.assertEqual(response.status_code, 403)

        response = client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/update",
                               data=update_form)
        self.assertEqual(response.status_code, 403)

        response = client.post(f"{PREFIX}/{journal_entry_id}/delete",
                               data={"csrf_token": csrf_token})
        self.assertEqual(response.status_code, 403)

    def test_editor(self) -> None:
        """Test the permission as editor.

        :return: None.
        """
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        add_form: dict[str, str] = self.__get_add_form()
        update_form: dict[str, str] = self.__get_update_form(journal_entry_id)
        response: httpx.Response

        response = self.__client.get(f"{PREFIX}/{journal_entry_id}")
        self.assertEqual(response.status_code, 200)

        response = self.__client.get(f"{PREFIX}/create/transfer")
        self.assertEqual(response.status_code, 200)

        response = self.__client.post(f"{PREFIX}/store/transfer",
                                      data=add_form)
        self.assertEqual(response.status_code, 302)
        match_journal_entry_detail(response.headers["Location"])

        response = self.__client.get(f"{PREFIX}/{journal_entry_id}/edit")
        self.assertEqual(response.status_code, 200)

        response = self.__client.post(f"{PREFIX}/{journal_entry_id}/update",
                                      data=update_form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"],
                         f"{PREFIX}/{journal_entry_id}?"
                         f"next={self.__encoded_next_uri}")

        response = self.__client.post(f"{PREFIX}/{journal_entry_id}/delete",
                                      data={"csrf_token": self.__csrf_token})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], RETURN_TO_URI)

    def test_add(self) -> None:
        """Tests to add the journal entries.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        create_uri: str = (f"{PREFIX}/create/transfer?"
                           f"next={self.__encoded_next_uri}")
        store_uri: str = f"{PREFIX}/store/transfer"
        response: httpx.Response
        form: dict[str, str]
        journal_entry: JournalEntry | None

        # No currency content
        form = self.__get_add_form()
        form = {x: form[x] for x in form if not x.startswith("currency-")}
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Missing currency
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = ""
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-existing currency
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = "ZZZ"
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # No debit content in a currency
        form = self.__get_add_form()
        remove_debit_in_a_currency(form)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # No credit content in a currency
        form = self.__get_add_form()
        remove_credit_in_a_currency(form)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-existing account
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-account_code")][0]
        form[key] = "9999-999"
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-debit account
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.SERVICE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Non-credit account
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.OFFICE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # A receivable line item cannot start from credit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.RECEIVABLE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # A payable line item cannot start from debit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.PAYABLE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Negative amount
        form = self.__get_add_form()
        set_negative_amount(form)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Not balanced
        form = self.__get_add_form()
        key: str = [x for x in form.keys() if x.endswith("-amount")][0]
        form[key] = str(Decimal(form[key]) + 1000)
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], create_uri)

        # Success
        response = self.__client.post(store_uri,
                                      data=self.__get_add_form())
        self.assertEqual(response.status_code, 302)
        journal_entry_id: int \
            = match_journal_entry_detail(response.headers["Location"])

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies), 3)

            self.assertEqual(currencies[0].code, "JPY")
            self.assertEqual(len(currencies[0].debit), 2)
            self.assertEqual(currencies[0].debit[0].no, 1)
            self.assertEqual(currencies[0].debit[0].account.code,
                             Accounts.CASH)
            self.assertEqual(currencies[0].debit[1].no, 2)
            self.assertEqual(currencies[0].debit[1].account.code,
                             Accounts.BANK)
            self.assertEqual(len(currencies[0].credit), 2)
            self.assertEqual(currencies[0].credit[0].no, 1)
            self.assertEqual(currencies[0].credit[0].account.code,
                             Accounts.DONATION)
            self.assertEqual(currencies[0].credit[1].no, 2)
            self.assertEqual(currencies[0].credit[1].account.code,
                             Accounts.AGENCY)

            self.assertEqual(currencies[1].code, "USD")
            self.assertEqual(len(currencies[1].debit), 3)
            self.assertEqual(currencies[1].debit[0].no, 3)
            self.assertEqual(currencies[1].debit[0].account.code,
                             Accounts.BANK)
            self.assertEqual(currencies[1].debit[0].description, "Deposit")
            self.assertEqual(currencies[1].debit[1].no, 4)
            self.assertEqual(currencies[1].debit[1].account.code,
                             Accounts.OFFICE)
            self.assertEqual(currencies[1].debit[1].description, "Pens")
            self.assertEqual(currencies[1].debit[2].no, 5)
            self.assertEqual(currencies[1].debit[2].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies[1].debit[2].description)
            self.assertEqual(len(currencies[1].credit), 3)
            self.assertEqual(currencies[1].credit[0].no, 3)
            self.assertEqual(currencies[1].credit[0].account.code,
                             Accounts.SERVICE)
            self.assertEqual(currencies[1].credit[1].no, 4)
            self.assertEqual(currencies[1].credit[1].account.code,
                             Accounts.SALES)
            self.assertEqual(currencies[1].credit[2].no, 5)
            self.assertEqual(currencies[1].credit[2].account.code,
                             Accounts.INTEREST)

            self.assertEqual(currencies[2].code, "TWD")
            self.assertEqual(len(currencies[2].debit), 2)
            self.assertEqual(currencies[2].debit[0].no, 6)
            self.assertEqual(currencies[2].debit[0].account.code,
                             Accounts.CASH)
            self.assertEqual(currencies[2].debit[1].no, 7)
            self.assertEqual(currencies[2].debit[1].account.code,
                             Accounts.TRAVEL)
            self.assertEqual(len(currencies[2].credit), 2)
            self.assertEqual(currencies[2].credit[0].no, 6)
            self.assertEqual(currencies[2].credit[0].account.code,
                             Accounts.RENT_INCOME)
            self.assertEqual(currencies[2].credit[1].no, 7)
            self.assertEqual(currencies[2].credit[1].account.code,
                             Accounts.DONATION)

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

        # Success, with empty note
        form = self.__get_add_form()
        form["note"] = EMPTY_NOTE
        response = self.__client.post(store_uri, data=form)
        self.assertEqual(response.status_code, 302)
        journal_entry_id: int \
            = match_journal_entry_detail(response.headers["Location"])

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            self.assertIsNone(journal_entry.note)

    def test_basic_update(self) -> None:
        """Tests the basic rules to update a journal entry.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        edit_uri: str = (f"{PREFIX}/{journal_entry_id}/edit?"
                         f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        form_0: dict[str, str] = self.__get_update_form(journal_entry_id)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies0: list[JournalEntryCurrency] = journal_entry.currencies
            old_id: set[int] = set()
            for currency in currencies0:
                old_id.update({x.id for x in currency.debit})

        # No currency content
        form = form_0.copy()
        form = {x: form[x] for x in form if not x.startswith("currency-")}
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Missing currency
        form = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = ""
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-existing currency
        form = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-code")][0]
        form[key] = "ZZZ"
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # No debit content in a currency
        form = form_0.copy()
        remove_debit_in_a_currency(form)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # No credit content in a currency
        form = form_0.copy()
        remove_credit_in_a_currency(form)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-existing account
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-account_code")][0]
        form[key] = "9999-999"
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-debit account
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.SERVICE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Non-credit account
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.OFFICE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # A receivable line item cannot start from credit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-credit-" in x][0]
        form[key] = Accounts.RECEIVABLE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # A payable line item cannot start from debit
        form = self.__get_add_form()
        key: str = [x for x in form.keys()
                    if x.endswith("-account_code") and "-debit-" in x][0]
        form[key] = Accounts.PAYABLE
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Negative amount
        form: dict[str, str] = form_0.copy()
        set_negative_amount(form)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Not balanced
        form: dict[str, str] = form_0.copy()
        key: str = [x for x in form.keys() if x.endswith("-amount")][0]
        form[key] = str(Decimal(form[key]) + 1000)
        response = self.__client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], edit_uri)

        # Success
        response = self.__client.post(update_uri, data=form_0)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies1: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies1), 3)

            self.assertEqual(currencies1[0].code, "AUD")
            self.assertEqual(len(currencies1[0].debit), 2)
            self.assertNotIn(currencies1[0].debit[0].id, old_id)
            self.assertEqual(currencies1[0].debit[0].no, 1)
            self.assertEqual(currencies1[0].debit[0].account.code,
                             Accounts.OFFICE)
            self.assertNotIn(currencies1[0].debit[1].id, old_id)
            self.assertEqual(currencies1[0].debit[1].no, 2)
            self.assertEqual(currencies1[0].debit[1].account.code,
                             Accounts.CASH)
            self.assertEqual(len(currencies1[0].credit), 2)
            self.assertNotIn(currencies1[0].credit[0].id, old_id)
            self.assertEqual(currencies1[0].credit[0].no, 1)
            self.assertEqual(currencies1[0].credit[0].account.code,
                             Accounts.DONATION)
            self.assertNotIn(currencies1[0].credit[1].id, old_id)
            self.assertEqual(currencies1[0].credit[1].no, 2)
            self.assertEqual(currencies1[0].credit[1].account.code,
                             Accounts.RENT_INCOME)

            self.assertEqual(currencies1[1].code, "EUR")
            self.assertEqual(len(currencies1[1].debit), 2)
            self.assertEqual(currencies1[1].debit[0].id,
                             currencies0[2].debit[0].id)
            self.assertEqual(currencies1[1].debit[0].no, 3)
            self.assertEqual(currencies1[1].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[1].debit[0].description)
            self.assertEqual(currencies1[1].debit[1].id,
                             currencies0[2].debit[1].id)
            self.assertEqual(currencies1[1].debit[1].no, 4)
            self.assertEqual(currencies1[1].debit[1].account.code,
                             Accounts.TRAVEL)
            self.assertEqual(len(currencies1[1].credit), 2)
            self.assertEqual(currencies1[1].credit[0].id,
                             currencies0[2].credit[0].id)
            self.assertEqual(currencies1[1].credit[0].no, 3)
            self.assertEqual(currencies1[1].credit[0].account.code,
                             Accounts.RENT_INCOME)
            self.assertEqual(currencies1[1].credit[1].id,
                             currencies0[2].credit[1].id)
            self.assertEqual(currencies1[1].credit[1].no, 4)
            self.assertEqual(currencies1[1].credit[1].account.code,
                             Accounts.DONATION)

            self.assertEqual(currencies1[2].code, "USD")
            self.assertEqual(len(currencies1[2].debit), 3)
            self.assertNotIn(currencies1[2].debit[0].id, old_id)
            self.assertEqual(currencies1[2].debit[0].no, 5)
            self.assertEqual(currencies1[2].debit[0].account.code,
                             Accounts.TRAVEL)
            self.assertIsNone(currencies1[2].debit[0].description)
            self.assertEqual(currencies1[2].debit[1].id,
                             currencies0[1].debit[2].id)
            self.assertEqual(currencies1[2].debit[1].no, 6)
            self.assertEqual(currencies1[2].debit[1].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[2].debit[1].description)
            self.assertEqual(currencies1[2].debit[2].id,
                             currencies0[1].debit[0].id)
            self.assertEqual(currencies1[2].debit[2].no, 7)
            self.assertEqual(currencies1[2].debit[2].account.code,
                             Accounts.BANK)
            self.assertEqual(currencies1[2].debit[2].description, "Deposit")
            self.assertEqual(len(currencies1[2].credit), 3)
            self.assertNotIn(currencies1[2].credit[0].id, old_id)
            self.assertEqual(currencies1[2].credit[0].no, 5)
            self.assertEqual(currencies1[2].credit[0].account.code,
                             Accounts.AGENCY)
            self.assertEqual(currencies1[2].credit[1].id,
                             currencies0[1].credit[2].id)
            self.assertEqual(currencies1[2].credit[1].no, 6)
            self.assertEqual(currencies1[2].credit[1].account.code,
                             Accounts.INTEREST)
            self.assertEqual(currencies1[2].credit[2].id,
                             currencies0[1].credit[0].id)
            self.assertEqual(currencies1[2].credit[2].no, 7)
            self.assertEqual(currencies1[2].credit[2].account.code,
                             Accounts.SERVICE)

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

    def test_update_not_modified(self) -> None:
        """Tests that the data is not modified.

        :return: None.
        """
        from accounting.models import JournalEntry
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        journal_entry: JournalEntry
        response: httpx.Response

        response = self.__client.post(
            update_uri,
            data=self.__get_unchanged_update_form(journal_entry_id))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            journal_entry.created_at \
                = journal_entry.created_at - dt.timedelta(seconds=5)
            journal_entry.updated_at = journal_entry.created_at
            db.session.commit()

        response = self.__client.post(
            update_uri, data=self.__get_update_form(journal_entry_id))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            self.assertLess(journal_entry.created_at, journal_entry.updated_at)

    def test_created_updated_by(self) -> None:
        """Tests the created-by and updated-by record.

        :return: None.
        """
        from accounting.models import JournalEntry
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        editor_username, admin_username = "editor", "admin"
        client: httpx.Client = get_client(self.__app, admin_username)
        csrf_token: str = get_csrf_token(client)
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update"
        journal_entry: JournalEntry
        response: httpx.Response

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertEqual(journal_entry.created_by.username,
                             editor_username)
            self.assertEqual(journal_entry.updated_by.username,
                             editor_username)

        form: dict[str, str] = self.__get_update_form(journal_entry_id)
        form["csrf_token"] = csrf_token
        response = client.post(update_uri, data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertEqual(journal_entry.created_by.username,
                             editor_username)
            self.assertEqual(journal_entry.updated_by.username,
                             admin_username)

    def test_save_as_receipt(self) -> None:
        """Tests to save a transfer journal entry as a cash receipt journal
        entry.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update?as=receipt"
        form_0: dict[str, str] = self.__get_update_form(journal_entry_id)
        form_0 = {x: form_0[x] for x in form_0 if "-debit-" not in x}

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies0: list[JournalEntryCurrency] = journal_entry.currencies
            old_id: set[int] = set()
            for currency in currencies0:
                old_id.update({x.id for x in currency.debit})

        # Success
        response = self.__client.post(update_uri, data=form_0)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies1: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies1), 3)

            self.assertEqual(currencies1[0].code, "AUD")
            self.assertEqual(len(currencies1[0].debit), 1)
            self.assertNotIn(currencies1[0].debit[0].id, old_id)
            self.assertEqual(currencies1[0].debit[0].no, 1)
            self.assertEqual(currencies1[0].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[0].debit[0].description)
            self.assertEqual(currencies1[0].debit[0].amount,
                             sum([x.amount for x in currencies1[0].credit]))
            self.assertEqual(len(currencies1[0].credit), 2)
            self.assertNotIn(currencies1[0].credit[0].id, old_id)
            self.assertEqual(currencies1[0].credit[0].no, 1)
            self.assertEqual(currencies1[0].credit[0].account.code,
                             Accounts.DONATION)
            self.assertNotIn(currencies1[0].credit[1].id, old_id)
            self.assertEqual(currencies1[0].credit[1].no, 2)
            self.assertEqual(currencies1[0].credit[1].account.code,
                             Accounts.RENT_INCOME)

            self.assertEqual(currencies1[1].code, "EUR")
            self.assertEqual(len(currencies1[1].debit), 1)
            self.assertNotIn(currencies1[1].debit[0].id, old_id)
            self.assertEqual(currencies1[1].debit[0].no, 2)
            self.assertEqual(currencies1[1].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[1].debit[0].description)
            self.assertEqual(currencies1[1].debit[0].amount,
                             sum([x.amount for x in currencies1[1].credit]))
            self.assertEqual(len(currencies1[1].credit), 2)
            self.assertEqual(currencies1[1].credit[0].id,
                             currencies0[2].credit[0].id)
            self.assertEqual(currencies1[1].credit[0].no, 3)
            self.assertEqual(currencies1[1].credit[0].account.code,
                             Accounts.RENT_INCOME)
            self.assertEqual(currencies1[1].credit[1].id,
                             currencies0[2].credit[1].id)
            self.assertEqual(currencies1[1].credit[1].no, 4)
            self.assertEqual(currencies1[1].credit[1].account.code,
                             Accounts.DONATION)

            self.assertEqual(currencies1[2].code, "USD")
            self.assertEqual(len(currencies1[2].debit), 1)
            self.assertEqual(currencies1[2].debit[0].id,
                             currencies0[1].debit[0].id)
            self.assertEqual(currencies1[2].debit[0].no, 3)
            self.assertEqual(currencies1[2].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[2].debit[0].description)
            self.assertEqual(currencies1[2].debit[0].amount,
                             sum([x.amount for x in currencies1[2].credit]))
            self.assertEqual(len(currencies1[2].credit), 3)
            self.assertNotIn(currencies1[2].credit[0].id, old_id)
            self.assertEqual(currencies1[2].credit[0].no, 5)
            self.assertEqual(currencies1[2].credit[0].account.code,
                             Accounts.AGENCY)
            self.assertEqual(currencies1[2].credit[1].id,
                             currencies0[1].credit[2].id)
            self.assertEqual(currencies1[2].credit[1].no, 6)
            self.assertEqual(currencies1[2].credit[1].account.code,
                             Accounts.INTEREST)
            self.assertEqual(currencies1[2].credit[2].id,
                             currencies0[1].credit[0].id)
            self.assertEqual(currencies1[2].credit[2].no, 7)
            self.assertEqual(currencies1[2].credit[2].account.code,
                             Accounts.SERVICE)

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

    def test_save_as_disbursement(self) -> None:
        """Tests to save a transfer journal entry as a cash disbursement
        journal entry.

        :return: None.
        """
        from accounting.models import JournalEntry, JournalEntryCurrency
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        update_uri: str = f"{PREFIX}/{journal_entry_id}/update?as=disbursement"
        form_0: dict[str, str] = self.__get_update_form(journal_entry_id)
        form_0 = {x: form_0[x] for x in form_0 if "-credit-" not in x}

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies0: list[JournalEntryCurrency] = journal_entry.currencies
            old_id: set[int] = set()
            for currency in currencies0:
                old_id.update({x.id for x in currency.debit})

        # Success
        response = self.__client.post(update_uri, data=form_0)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], detail_uri)

        with self.__app.app_context():
            journal_entry = db.session.get(JournalEntry, journal_entry_id)
            self.assertIsNotNone(journal_entry)
            currencies1: list[JournalEntryCurrency] = journal_entry.currencies
            self.assertEqual(len(currencies1), 3)

            self.assertEqual(currencies1[0].code, "AUD")
            self.assertEqual(len(currencies1[0].debit), 2)
            self.assertNotIn(currencies1[0].debit[0].id, old_id)
            self.assertEqual(currencies1[0].debit[0].no, 1)
            self.assertEqual(currencies1[0].debit[0].account.code,
                             Accounts.OFFICE)
            self.assertNotIn(currencies1[0].debit[1].id, old_id)
            self.assertEqual(currencies1[0].debit[1].no, 2)
            self.assertEqual(currencies1[0].debit[1].account.code,
                             Accounts.CASH)
            self.assertEqual(len(currencies1[0].credit), 1)
            self.assertNotIn(currencies1[0].credit[0].id, old_id)
            self.assertEqual(currencies1[0].credit[0].no, 1)
            self.assertEqual(currencies1[0].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[0].credit[0].description)
            self.assertEqual(currencies1[0].credit[0].amount,
                             sum([x.amount for x in currencies1[0].debit]))

            self.assertEqual(currencies1[1].code, "EUR")
            self.assertEqual(len(currencies1[1].debit), 2)
            self.assertEqual(currencies1[1].debit[0].id,
                             currencies0[2].debit[0].id)
            self.assertEqual(currencies1[1].debit[0].no, 3)
            self.assertEqual(currencies1[1].debit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[1].debit[0].description)
            self.assertEqual(currencies1[1].debit[1].id,
                             currencies0[2].debit[1].id)
            self.assertEqual(currencies1[1].debit[1].no, 4)
            self.assertEqual(currencies1[1].debit[1].account.code,
                             Accounts.TRAVEL)
            self.assertEqual(len(currencies1[1].credit), 1)
            self.assertNotIn(currencies1[1].credit[0].id, old_id)
            self.assertEqual(currencies1[1].credit[0].no, 2)
            self.assertEqual(currencies1[1].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[1].credit[0].description)
            self.assertEqual(currencies1[1].credit[0].amount,
                             sum([x.amount for x in currencies1[1].debit]))

            self.assertEqual(currencies1[2].code, "USD")
            self.assertEqual(len(currencies1[2].debit), 3)
            self.assertNotIn(currencies1[2].debit[0].id, old_id)
            self.assertEqual(currencies1[2].debit[0].no, 5)
            self.assertEqual(currencies1[2].debit[0].account.code,
                             Accounts.TRAVEL)
            self.assertIsNone(currencies1[2].debit[0].description)
            self.assertEqual(currencies1[2].debit[1].id,
                             currencies0[1].debit[2].id)
            self.assertEqual(currencies1[2].debit[1].no, 6)
            self.assertEqual(currencies1[2].debit[1].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[2].debit[1].description)
            self.assertEqual(currencies1[2].debit[2].id,
                             currencies0[1].debit[0].id)
            self.assertEqual(currencies1[2].debit[2].no, 7)
            self.assertEqual(currencies1[2].debit[2].account.code,
                             Accounts.BANK)
            self.assertEqual(currencies1[2].debit[2].description, "Deposit")
            self.assertEqual(len(currencies1[2].credit), 1)
            self.assertEqual(currencies1[2].credit[0].id,
                             currencies0[1].credit[0].id)
            self.assertEqual(currencies1[2].credit[0].no, 3)
            self.assertEqual(currencies1[2].credit[0].account.code,
                             Accounts.CASH)
            self.assertIsNone(currencies1[2].credit[0].description)
            self.assertEqual(currencies1[2].credit[0].amount,
                             sum([x.amount for x in currencies1[2].debit]))

            self.assertEqual(journal_entry.note, NON_EMPTY_NOTE)

    def test_delete(self) -> None:
        """Tests to delete a journal entry.

        :return: None.
        """
        journal_entry_id: int \
            = add_journal_entry(self.__client, self.__get_add_form())
        detail_uri: str = (f"{PREFIX}/{journal_entry_id}?"
                           f"next={self.__encoded_next_uri}")
        delete_uri: str = f"{PREFIX}/{journal_entry_id}/delete"
        response: httpx.Response

        response = self.__client.get(detail_uri)
        self.assertEqual(response.status_code, 200)
        response = self.__client.post(delete_uri,
                                      data={"csrf_token": self.__csrf_token,
                                            "next": self.__encoded_next_uri})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], NEXT_URI)

        response = self.__client.get(detail_uri)
        self.assertEqual(response.status_code, 404)
        response = self.__client.post(delete_uri,
                                      data={"csrf_token": self.__csrf_token,
                                            "next": self.__encoded_next_uri})
        self.assertEqual(response.status_code, 404)

    def __get_add_form(self) -> dict[str, str]:
        """Returns the form data to add a new journal entry.

        :return: The form data to add a new journal entry.
        """
        return get_add_form(self.__csrf_token, self.__encoded_next_uri)

    def __get_unchanged_update_form(self, journal_entry_id: int) \
            -> dict[str, str]:
        """Returns the form data to update a journal entry, where the data are
        not changed.

        :param journal_entry_id: The journal entry ID.
        :return: The form data to update the journal entry, where the data are
            not changed.
        """
        return get_unchanged_update_form(
            journal_entry_id, self.__app, self.__csrf_token,
            self.__encoded_next_uri)

    def __get_update_form(self, journal_entry_id: int) -> dict[str, str]:
        """Returns the form data to update a journal entry, where the data are
        changed.

        :param journal_entry_id: The journal entry ID.
        :return: The form data to update the journal entry, where the data are
            changed.
        """
        return get_update_form(
            journal_entry_id, self.__app, self.__csrf_token,
            self.__encoded_next_uri, None)


class JournalEntryReorderTestCase(unittest.TestCase):
    """The journal entry reorder test case."""

    def setUp(self) -> None:
        """Sets up the test.
        This is run once per test.

        :return: None.
        """
        self.__app: Flask = create_test_app()
        """The Flask application."""

        with self.__app.app_context():
            from accounting.models import JournalEntry, JournalEntryLineItem
            JournalEntry.query.delete()
            JournalEntryLineItem.query.delete()
            self.__encoded_next_uri: str = encode_next(NEXT_URI)
            """The encoded next URI."""

        self.__client: httpx.Client = get_client(self.__app, "editor")
        """The user client."""
        self.__csrf_token: str = get_csrf_token(self.__client)
        """The CSRF token."""

    def test_change_date(self) -> None:
        """Tests to change the date of a journal entry.

        :return: None.
        """
        from accounting.models import JournalEntry
        response: httpx.Response

        id_1: int = add_journal_entry(self.__client,
                                      self.__get_add_receipt_form())
        id_2: int = add_journal_entry(self.__client,
                                      self.__get_add_disbursement_form())
        id_3: int = add_journal_entry(self.__client,
                                      self.__get_add_transfer_form())
        id_4: int = add_journal_entry(self.__client,
                                      self.__get_add_receipt_form())
        id_5: int = add_journal_entry(self.__client,
                                      self.__get_add_disbursement_form())

        with self.__app.app_context():
            journal_entry_1: JournalEntry = db.session.get(JournalEntry, id_1)
            journal_entry_date_2: dt.date = journal_entry_1.date
            journal_entry_date_1: dt.date \
                = journal_entry_date_2 - dt.timedelta(days=1)
            journal_entry_1.date = journal_entry_date_1
            journal_entry_1.no = 3
            journal_entry_2: JournalEntry = db.session.get(JournalEntry, id_2)
            journal_entry_2.date = journal_entry_date_1
            journal_entry_2.no = 5
            journal_entry_3: JournalEntry = db.session.get(JournalEntry, id_3)
            journal_entry_3.date = journal_entry_date_1
            journal_entry_3.no = 8
            journal_entry_4: JournalEntry = db.session.get(JournalEntry, id_4)
            journal_entry_4.no = 2
            journal_entry_5: JournalEntry = db.session.get(JournalEntry, id_5)
            journal_entry_5.no = 6
            db.session.commit()

        form: dict[str, str] \
            = self.__get_disbursement_unchanged_update_form(id_2)
        form["date"] = journal_entry_date_2.isoformat()
        response = self.__client.post(f"{PREFIX}/{id_2}/update", data=form)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"],
                         f"{PREFIX}/{id_2}?next={self.__encoded_next_uri}")

        with self.__app.app_context():
            self.assertEqual(db.session.get(JournalEntry, id_1).no, 1)
            self.assertEqual(db.session.get(JournalEntry, id_2).no, 3)
            self.assertEqual(db.session.get(JournalEntry, id_3).no, 2)
            self.assertEqual(db.session.get(JournalEntry, id_4).no, 1)
            self.assertEqual(db.session.get(JournalEntry, id_5).no, 2)

    def test_reorder(self) -> None:
        """Tests to reorder the journal entries in a same day.

        :return: None.
        """
        from accounting.models import JournalEntry
        response: httpx.Response

        id_1: int = add_journal_entry(self.__client,
                                      self.__get_add_receipt_form())
        id_2: int = add_journal_entry(self.__client,
                                      self.__get_add_disbursement_form())
        id_3: int = add_journal_entry(self.__client,
                                      self.__get_add_transfer_form())
        id_4: int = add_journal_entry(self.__client,
                                      self.__get_add_receipt_form())
        id_5: int = add_journal_entry(self.__client,
                                      self.__get_add_disbursement_form())

        with self.__app.app_context():
            date: dt.date = db.session.get(JournalEntry, id_1).date

        response = self.__client.post(
            f"{PREFIX}/dates/{date.isoformat()}",
            data={"csrf_token": self.__csrf_token,
                  "next": self.__encoded_next_uri,
                  f"{id_1}-no": "4",
                  f"{id_2}-no": "1",
                  f"{id_3}-no": "5",
                  f"{id_4}-no": "2",
                  f"{id_5}-no": "3"})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], NEXT_URI)

        with self.__app.app_context():
            self.assertEqual(db.session.get(JournalEntry, id_1).no, 4)
            self.assertEqual(db.session.get(JournalEntry, id_2).no, 1)
            self.assertEqual(db.session.get(JournalEntry, id_3).no, 5)
            self.assertEqual(db.session.get(JournalEntry, id_4).no, 2)
            self.assertEqual(db.session.get(JournalEntry, id_5).no, 3)

        # Malformed orders
        with self.__app.app_context():
            db.session.get(JournalEntry, id_1).no = 3
            db.session.get(JournalEntry, id_2).no = 4
            db.session.get(JournalEntry, id_3).no = 6
            db.session.get(JournalEntry, id_4).no = 8
            db.session.get(JournalEntry, id_5).no = 9
            db.session.commit()

        response = self.__client.post(
            f"{PREFIX}/dates/{date.isoformat()}",
            data={"csrf_token": self.__csrf_token,
                  "next": self.__encoded_next_uri,
                  f"{id_2}-no": "3a",
                  f"{id_3}-no": "5",
                  f"{id_4}-no": "2"})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.headers["Location"], NEXT_URI)

        with self.__app.app_context():
            self.assertEqual(db.session.get(JournalEntry, id_1).no, 3)
            self.assertEqual(db.session.get(JournalEntry, id_2).no, 4)
            self.assertEqual(db.session.get(JournalEntry, id_3).no, 2)
            self.assertEqual(db.session.get(JournalEntry, id_4).no, 1)
            self.assertEqual(db.session.get(JournalEntry, id_5).no, 5)

    def __get_add_receipt_form(self) -> dict[str, str]:
        """Returns the form data to add a new cash receipt journal entry.

        :return: The form data to add a new cash receipt journal entry.
        """
        form: dict[str, str] = get_add_form(self.__csrf_token,
                                            self.__encoded_next_uri)
        form = {x: form[x] for x in form if "-debit-" not in x}
        return form

    def __get_add_disbursement_form(self) -> dict[str, str]:
        """Returns the form data to add a new cash disbursement journal entry.

        :return: The form data to add a new cash disbursement journal entry.
        """
        form: dict[str, str] = get_add_form(self.__csrf_token,
                                            self.__encoded_next_uri)
        form = {x: form[x] for x in form if "-credit-" not in x}
        return form

    def __get_disbursement_unchanged_update_form(self, journal_entry_id: int) \
            -> dict[str, str]:
        """Returns the form data to update a cash disbursement journal entry,
        where the data are not changed.

        :param journal_entry_id: The journal entry ID.
        :return: The form data to update the cash disbursement journal entry,
            where the data are not changed.
        """
        form: dict[str, str] = get_unchanged_update_form(
            journal_entry_id, self.__app, self.__csrf_token,
            self.__encoded_next_uri)
        form = {x: form[x] for x in form if "-credit-" not in x}
        return form

    def __get_add_transfer_form(self) -> dict[str, str]:
        """Returns the form data to add a new journal entry.

        :return: The form data to add a new journal entry.
        """
        return get_add_form(self.__csrf_token, self.__encoded_next_uri)
