# Copyright 2018 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import logging

from dateutil.relativedelta import relativedelta

from odoo import fields
from odoo.exceptions import UserError
from odoo.tests import TransactionCase, tagged

_logger = logging.getLogger(__name__)
try:
    import numpy_financial
except (ImportError, IOError) as err:
    _logger.error(err)


@tagged("post_install", "-at_install")
class TestLoan(TransactionCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.company = cls.env.ref("base.main_company")
        cls.company_02 = cls.env["res.company"].create({"name": "Auxiliar company"})
        cls.journal = cls.env["account.journal"].create(
            {
                "company_id": cls.company.id,
                "type": "purchase",
                "name": "Debts",
                "code": "DBT",
            }
        )
        cls.loan_account = cls.create_account(
            "DEP",
            "depreciation",
            "liability_current",
        )
        cls.payable_account = cls.create_account("PAY", "payable", "liability_payable")
        cls.asset_account = cls.create_account("ASSET", "asset", "liability_payable")
        cls.interests_account = cls.create_account("FEE", "Fees", "expense")
        cls.lt_loan_account = cls.create_account(
            "LTD",
            "Long term depreciation",
            "liability_non_current",
        )
        cls.partner = cls.env["res.partner"].create({"name": "Bank"})
        cls.product = cls.env["product.product"].create(
            {"name": "Payment", "type": "service"}
        )
        cls.interests_product = cls.env["product.product"].create(
            {"name": "Bank fee", "type": "service"}
        )

    def test_onchange(self):
        loan = self.env["account.loan"].new(
            {
                "name": "LOAN",
                "company_id": self.company.id,
                "journal_id": self.journal.id,
                "loan_type": "fixed-annuity",
                "loan_amount": 100,
                "rate": 1,
                "periods": 2,
                "short_term_loan_account_id": self.loan_account.id,
                "interest_expenses_account_id": self.interests_account.id,
                "product_id": self.product.id,
                "interests_product_id": self.interests_product.id,
                "partner_id": self.partner.id,
            }
        )
        journal = loan.journal_id.id
        loan.is_leasing = True
        loan._onchange_is_leasing()
        self.assertNotEqual(journal, loan.journal_id.id)
        loan.company_id = self.company_02
        loan._onchange_company()
        self.assertFalse(loan.interest_expenses_account_id)

    def test_partner_loans(self):
        self.assertFalse(self.partner.lended_loan_count)
        loan = self.create_loan("fixed-annuity", 500000, 1, 60)
        self.assertEqual(1, self.partner.lended_loan_count)
        action = self.partner.action_view_partner_lended_loans()
        self.assertEqual(loan, self.env[action["res_model"]].search(action["domain"]))

    def test_round_on_end(self):
        loan = self.create_loan("fixed-annuity", 500000, 1, 60)
        loan.round_on_end = True
        loan.compute_lines()
        line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
        for line in loan.line_ids:
            self.assertAlmostEqual(line_1.payment_amount, line.payment_amount, 2)
        loan.loan_type = "fixed-principal"
        loan.compute_lines()
        line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
        line_end = loan.line_ids.filtered(lambda r: r.sequence == 60)
        self.assertNotAlmostEqual(line_1.payment_amount, line_end.payment_amount, 2)
        loan.loan_type = "interest"
        loan.compute_lines()
        line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
        line_end = loan.line_ids.filtered(lambda r: r.sequence == 60)
        self.assertEqual(line_1.principal_amount, 0)
        self.assertEqual(line_end.principal_amount, 500000)

    def test_increase_amount_validation(self):
        amount = 10000
        periods = 24
        loan = self.create_loan("fixed-annuity", amount, 1, periods)
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
        )
        self.assertEqual(line.long_term_principal_amount, 0)
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertGreater(line.long_term_principal_amount, 0)
        self.post(loan)
        self.assertTrue(loan.start_date)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.move_ids)
        wzd = self.env["account.loan.generate.wizard"].create({})
        action = wzd.run()
        self.assertTrue(action)
        self.assertFalse(wzd.run())
        self.assertTrue(line.move_ids)
        self.assertIn(line.move_ids.id, action["domain"][0][2])
        self.assertTrue(line.move_ids)
        self.assertEqual(line.move_ids.state, "posted")
        with self.assertRaises(UserError):
            self.env["account.loan.increase.amount"].with_context(
                default_loan_id=loan.id
            ).create(
                {
                    "amount": (amount - amount / periods) / 2,
                    "date": line.date + relativedelta(months=-1),
                }
            ).run()
        with self.assertRaises(UserError):
            self.env["account.loan.increase.amount"].with_context(
                default_loan_id=loan.id
            ).create({"amount": 0, "date": line.date}).run()
        with self.assertRaises(UserError):
            self.env["account.loan.increase.amount"].with_context(
                default_loan_id=loan.id
            ).create({"amount": -100, "date": line.date}).run()

    def test_pay_amount_validation(self):
        amount = 10000
        periods = 24
        loan = self.create_loan("fixed-annuity", amount, 1, periods)
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
        )
        self.assertEqual(line.long_term_principal_amount, 0)
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertGreater(line.long_term_principal_amount, 0)
        self.post(loan)
        self.assertTrue(loan.start_date)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.move_ids)
        wzd = self.env["account.loan.generate.wizard"].create({})
        action = wzd.run()
        self.assertTrue(action)
        self.assertFalse(wzd.run())
        self.assertTrue(line.move_ids)
        self.assertIn(line.move_ids.id, action["domain"][0][2])
        self.assertTrue(line.move_ids)
        self.assertEqual(line.move_ids.state, "posted")
        with self.assertRaises(UserError):
            self.env["account.loan.pay.amount"].with_context(
                default_loan_id=loan.id
            ).create(
                {
                    "amount": (amount - amount / periods) / 2,
                    "fees": 100,
                    "date": line.date + relativedelta(months=-1),
                }
            ).run()
        with self.assertRaises(UserError):
            self.env["account.loan.pay.amount"].with_context(
                default_loan_id=loan.id
            ).create({"amount": amount, "fees": 100, "date": line.date}).run()
        with self.assertRaises(UserError):
            self.env["account.loan.pay.amount"].with_context(
                default_loan_id=loan.id
            ).create({"amount": 0, "fees": 100, "date": line.date}).run()
        with self.assertRaises(UserError):
            self.env["account.loan.pay.amount"].with_context(
                default_loan_id=loan.id
            ).create({"amount": -100, "fees": 100, "date": line.date}).run()

    def test_increase_amount_loan(self):
        amount = 10000
        periods = 24
        loan = self.create_loan("fixed-annuity", amount, 1, periods)
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
        )
        self.assertEqual(line.long_term_principal_amount, 0)
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertGreater(line.long_term_principal_amount, 0)
        self.post(loan)
        self.assertTrue(loan.start_date)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.move_ids)
        wzd = self.env["account.loan.generate.wizard"].create({})
        action = wzd.run()
        self.assertTrue(action)
        self.assertFalse(wzd.run())
        self.assertTrue(line.move_ids)
        self.assertIn(line.move_ids.id, action["domain"][0][2])
        self.assertTrue(line.move_ids)
        self.assertEqual(line.move_ids.state, "posted")
        pending_principal_amount = loan.pending_principal_amount
        action = (
            self.env["account.loan.increase.amount"]
            .with_context(default_loan_id=loan.id)
            .create(
                {
                    "amount": 1000,
                    "date": line.date,
                }
            )
            .run()
        )
        new_move = self.env[action["res_model"]].search(action["domain"])
        new_move.ensure_one()
        self.assertFalse(new_move.is_invoice())
        self.assertEqual(loan, new_move.loan_id)
        self.assertEqual(loan.pending_principal_amount, pending_principal_amount + 1000)

    def test_increase_amount_leasing(self):
        amount = 10000
        periods = 24
        loan = self.create_loan("fixed-annuity", amount, 1, periods)
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
        )
        self.assertEqual(line.long_term_principal_amount, 0)
        loan.is_leasing = True
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertGreater(line.long_term_principal_amount, 0)
        self.post(loan)
        self.assertTrue(loan.start_date)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.move_ids)
        wzd = self.env["account.loan.generate.wizard"].create(
            {
                "date": fields.date.today() + relativedelta(days=1),
                "loan_type": "leasing",
            }
        )
        action = wzd.run()
        self.assertTrue(action)
        self.assertFalse(wzd.run())
        self.assertTrue(line.move_ids)
        self.assertIn(line.move_ids.id, action["domain"][0][2])
        self.assertTrue(line.move_ids)
        self.assertEqual(line.move_ids.state, "posted")
        pending_principal_amount = loan.pending_principal_amount
        action = (
            self.env["account.loan.increase.amount"]
            .with_context(default_loan_id=loan.id)
            .create(
                {
                    "amount": 1000,
                    "date": line.date,
                }
            )
            .run()
        )
        new_move = self.env[action["res_model"]].search(action["domain"])
        new_move.ensure_one()
        self.assertFalse(new_move.is_invoice())
        self.assertEqual(loan, new_move.loan_id)
        self.assertEqual(loan.pending_principal_amount, pending_principal_amount + 1000)

    def test_fixed_annuity_begin_loan(self):
        amount = 10000
        periods = 24
        loan = self.create_loan("fixed-annuity-begin", amount, 1, periods)
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, 24, 10000, when="begin"),
            line.payment_amount,
            2,
        )
        self.assertEqual(line.long_term_principal_amount, 0)
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertGreater(line.long_term_principal_amount, 0)
        self.post(loan)
        self.assertTrue(loan.start_date)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.move_ids)
        wzd = self.env["account.loan.generate.wizard"].create({})
        action = wzd.run()
        self.assertTrue(action)
        self.assertFalse(wzd.run())
        self.assertTrue(line.move_ids)
        self.assertIn(line.move_ids.id, action["domain"][0][2])
        self.assertTrue(line.move_ids)
        self.assertEqual(line.move_ids.state, "posted")
        loan.rate = 2
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, periods, amount, when="begin"),
            line.payment_amount,
            2,
        )
        line = loan.line_ids.filtered(lambda r: r.sequence == 2)
        self.assertAlmostEqual(
            -numpy_financial.pmt(
                2 / 100 / 12, periods - 1, line.pending_principal_amount, when="begin"
            ),
            line.payment_amount,
            2,
        )
        line = loan.line_ids.filtered(lambda r: r.sequence == 3)
        with self.assertRaises(UserError):
            line.view_process_values()

    def test_fixed_annuity_loan(self):
        amount = 10000
        periods = 24
        loan = self.create_loan("fixed-annuity", amount, 1, periods)
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
        )
        self.assertEqual(line.long_term_principal_amount, 0)
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertGreater(line.long_term_principal_amount, 0)
        self.post(loan)
        self.assertTrue(loan.start_date)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.move_ids)
        wzd = self.env["account.loan.generate.wizard"].create({})
        action = wzd.run()
        self.assertTrue(action)
        self.assertFalse(wzd.run())
        self.assertTrue(line.move_ids)
        self.assertIn(line.move_ids.id, action["domain"][0][2])
        self.assertTrue(line.move_ids)
        self.assertEqual(line.move_ids.state, "posted")
        loan.rate = 2
        loan.compute_lines()
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertAlmostEqual(
            -numpy_financial.pmt(1 / 100 / 12, periods, amount), line.payment_amount, 2
        )
        line = loan.line_ids.filtered(lambda r: r.sequence == 2)
        self.assertAlmostEqual(
            -numpy_financial.pmt(
                2 / 100 / 12, periods - 1, line.pending_principal_amount
            ),
            line.payment_amount,
            2,
        )
        line = loan.line_ids.filtered(lambda r: r.sequence == 3)
        with self.assertRaises(UserError):
            line.view_process_values()

    def test_fixed_principal_loan_leasing(self):
        amount = 24000
        periods = 24
        loan = self.create_loan("fixed-principal", amount, 1, periods)
        self.partner.property_account_payable_id = self.payable_account
        self.assertEqual(loan.journal_type, "general")
        loan.is_leasing = True
        loan.post_invoice = False
        self.assertEqual(loan.journal_type, "purchase")
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.rate_type = "real"
        loan.compute_lines()
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertEqual(amount / periods, line.principal_amount)
        self.assertEqual(amount / periods, line.long_term_principal_amount)
        self.post(loan)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.has_invoices)
        self.assertFalse(line.has_moves)
        action = (
            self.env["account.loan.generate.wizard"]
            .create(
                {
                    "date": fields.date.today() + relativedelta(days=1),
                    "loan_type": "leasing",
                }
            )
            .run()
        )
        self.assertTrue(line.has_invoices)
        self.assertTrue(line.has_moves)
        self.assertEqual(
            line.move_ids, self.env[action["res_model"]].search(action["domain"])
        )
        loan.invalidate_recordset()
        with self.assertRaises(UserError):
            self.env["account.loan.pay.amount"].create(
                {
                    "loan_id": loan.id,
                    "amount": (amount - amount / periods) / 2,
                    "fees": 100,
                    "date": loan.line_ids.filtered(lambda r: r.sequence == 2).date,
                }
            ).run()
        with self.assertRaises(UserError):
            self.env["account.loan.pay.amount"].create(
                {
                    "loan_id": loan.id,
                    "amount": (amount - amount / periods) / 2,
                    "fees": 100,
                    "date": loan.line_ids.filtered(lambda r: r.sequence == 1).date
                    + relativedelta(months=-1),
                }
            ).run()
        self.assertTrue(line.move_ids)
        self.assertTrue(line.move_ids.filtered(lambda r: r.is_invoice()))
        self.assertTrue(line.move_ids.filtered(lambda r: not r.is_invoice()))
        self.assertTrue(all([m.state == "draft" for m in line.move_ids]))
        self.assertTrue(line.has_moves)
        line.move_ids.action_post()
        self.assertTrue(all([m.state == "posted" for m in line.move_ids]))
        for move in line.move_ids:
            self.assertIn(
                move,
                self.env["account.move"].search(loan.view_account_moves()["domain"]),
            )
        for move in line.move_ids.filtered(lambda r: r.is_invoice()):
            self.assertIn(
                move,
                self.env["account.move"].search(loan.view_account_invoices()["domain"]),
            )
        with self.assertRaises(UserError):
            self.env["account.loan.pay.amount"].create(
                {
                    "loan_id": loan.id,
                    "amount": (amount - amount / periods) / 2,
                    "fees": 100,
                    "date": loan.line_ids.filtered(
                        lambda r: r.sequence == periods
                    ).date,
                }
            ).run()
        self.env["account.loan.pay.amount"].create(
            {
                "loan_id": loan.id,
                "amount": (amount - amount / periods) / 2,
                "date": line.date,
                "fees": 100,
            }
        ).run()
        line = loan.line_ids.filtered(lambda r: r.sequence == 2)
        self.assertEqual(loan.periods, periods + 1)
        self.assertAlmostEqual(
            line.principal_amount, (amount - amount / periods) / 2, 2
        )
        line = loan.line_ids.filtered(lambda r: r.sequence == 3)
        self.assertEqual(amount / periods / 2, line.principal_amount)
        line = loan.line_ids.filtered(lambda r: r.sequence == 4)
        with self.assertRaises(UserError):
            line.view_process_values()

    def test_fixed_principal_loan_auto_post_leasing(self):
        amount = 24000
        periods = 24
        loan = self.create_loan("fixed-principal", amount, 1, periods)
        self.partner.property_account_payable_id = self.payable_account
        self.assertEqual(loan.journal_type, "general")
        loan.is_leasing = True
        self.assertEqual(loan.journal_type, "purchase")
        loan.long_term_loan_account_id = self.lt_loan_account
        loan.rate_type = "real"
        loan.compute_lines()
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertEqual(amount / periods, line.principal_amount)
        self.assertEqual(amount / periods, line.long_term_principal_amount)
        self.post(loan)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        self.assertTrue(line)
        self.assertFalse(line.has_invoices)
        self.assertFalse(line.has_moves)
        self.env["account.loan.generate.wizard"].create(
            {"date": fields.date.today(), "loan_type": "leasing"}
        ).run()
        self.assertTrue(line.has_invoices)
        self.assertTrue(line.has_moves)

    def test_interests_on_end_loan(self):
        amount = 10000
        periods = 10
        loan = self.create_loan("interest", amount, 1, periods)
        loan.payment_on_first_period = False
        loan.start_date = fields.Date.today()
        loan.rate_type = "ear"
        loan.compute_lines()
        self.assertTrue(loan.line_ids)
        self.assertEqual(len(loan.line_ids), periods)
        self.assertEqual(0, loan.line_ids[0].principal_amount)
        self.assertEqual(
            amount,
            loan.line_ids.filtered(lambda r: r.sequence == periods).principal_amount,
        )
        self.post(loan)
        self.assertEqual(loan.payment_amount, 0)
        self.assertEqual(loan.interests_amount, 0)
        self.assertEqual(loan.pending_principal_amount, amount)
        self.assertFalse(loan.line_ids.filtered(lambda r: r.date <= loan.start_date))
        for line in loan.line_ids:
            self.assertEqual(loan.state, "posted")
            line.view_process_values()
            self.assertTrue(line.move_ids)
            self.assertEqual(line.move_ids.state, "posted")
        self.assertEqual(loan.state, "closed")
        loan.invalidate_recordset()
        self.assertEqual(loan.payment_amount - loan.interests_amount, amount)
        self.assertEqual(loan.pending_principal_amount, 0)

    def test_cancel_loan(self):
        amount = 10000
        periods = 10
        loan = self.create_loan("fixed-annuity", amount, 1, periods)
        self.post(loan)
        line = loan.line_ids.filtered(lambda r: r.sequence == 1)
        line.view_process_values()
        self.assertTrue(line.move_ids)
        self.assertEqual(line.move_ids.state, "posted")
        pay = self.env["account.loan.pay.amount"].create(
            {"loan_id": loan.id, "amount": 0, "fees": 100, "date": line.date}
        )
        pay.cancel_loan = True
        pay._onchange_cancel_loan()
        self.assertEqual(pay.amount, line.final_pending_principal_amount)
        pay.run()
        self.assertEqual(loan.state, "cancelled")

    def post(self, loan):
        self.assertFalse(loan.move_ids)
        post = (
            self.env["account.loan.post"]
            .with_context(default_loan_id=loan.id)
            .create({})
        )
        post.run()
        self.assertTrue(loan.move_ids)
        with self.assertRaises(UserError):
            post.run()

    @classmethod
    def create_account(cls, code, name, account_type):
        return cls.env["account.account"].create(
            {
                "company_id": cls.company.id,
                "name": name,
                "code": code,
                "account_type": account_type,
                "reconcile": True,
            }
        )

    def create_loan(self, type_loan, amount, rate, periods):
        loan = self.env["account.loan"].create(
            {
                "journal_id": self.journal.id,
                "rate_type": "napr",
                "loan_type": type_loan,
                "loan_amount": amount,
                "payment_on_first_period": True,
                "rate": rate,
                "periods": periods,
                "leased_asset_account_id": self.asset_account.id,
                "short_term_loan_account_id": self.loan_account.id,
                "interest_expenses_account_id": self.interests_account.id,
                "product_id": self.product.id,
                "interests_product_id": self.interests_product.id,
                "partner_id": self.partner.id,
            }
        )
        loan.compute_lines()
        return loan