# Part of Odoo. See LICENSE file for full copyright and licensing details.

from unittest.mock import ANY, patch

from odoo.exceptions import AccessError
from odoo.tests import JsonRpcException, tagged
from odoo.tools import mute_logger

from odoo.addons.account_payment.tests.common import AccountPaymentCommon
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.addons.sale.controllers.portal import PaymentPortal
from odoo.addons.sale.tests.common import SaleCommon


@tagged('-at_install', 'post_install')
class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        # Replace PaymentCommon defaults by SaleCommon ones
        cls.currency = cls.sale_order.currency_id
        cls.partner = cls.sale_order.partner_invoice_id

        cls.provider.journal_id.inbound_payment_method_line_ids.filtered(lambda l:
            l.payment_provider_id == cls.provider
        ).payment_account_id = cls.inbound_payment_method_line.payment_account_id

    def test_11_so_payment_link(self):
        # test customized /payment/pay route with sale_order_id param
        self.amount = self.sale_order.amount_total
        route_values = self._prepare_pay_values()
        route_values['sale_order_id'] = self.sale_order.id

        with patch(
            'odoo.addons.payment.controllers.portal.PaymentPortal'
            '._compute_show_tokenize_input_mapping'
        ) as patched:
            tx_context = self._get_portal_pay_context(**route_values)
            patched.assert_called_once_with(ANY, sale_order_id=ANY)

        self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
        self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
        self.assertEqual(tx_context['amount'], self.sale_order.amount_total)

        # /my/orders/<id>/transaction/
        tx_route_values = {
            'provider_id': self.provider.id,
            'payment_method_id': self.payment_method_id,
            'token_id': None,
            'amount': tx_context['amount'],
            'flow': 'direct',
            'tokenization_requested': False,
            'landing_route': tx_context['landing_route'],
            'access_token': tx_context['access_token'],
        }
        with mute_logger('odoo.addons.payment.models.payment_transaction'):
            processing_values = self._get_processing_values(
                tx_route=tx_context['transaction_route'], **tx_route_values
            )
        tx_sudo = self._get_tx(processing_values['reference'])

        self.assertEqual(tx_sudo.sale_order_ids, self.sale_order)
        self.assertEqual(tx_sudo.amount, self.amount)
        self.assertEqual(tx_sudo.partner_id, self.sale_order.partner_invoice_id)
        self.assertEqual(tx_sudo.company_id, self.sale_order.company_id)
        self.assertEqual(tx_sudo.currency_id, self.sale_order.currency_id)
        self.assertEqual(tx_sudo.reference, self.sale_order.name)

        # Check validation of transaction correctly confirms the SO
        self.assertEqual(self.sale_order.state, 'draft')
        self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
        tx_sudo._set_done()
        tx_sudo._post_process()
        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(tx_sudo.payment_id)
        self.assertEqual(tx_sudo.payment_id.state, 'in_process')

    def test_so_payment_link_with_different_partner_invoice(self):
        # test customized /payment/pay route with sale_order_id param
        # partner_id and partner_invoice_id different on the so
        self.sale_order.partner_invoice_id = self.portal_partner
        self.partner = self.sale_order.partner_invoice_id
        route_values = self._prepare_pay_values()
        route_values['sale_order_id'] = self.sale_order.id

        tx_context = self._get_portal_pay_context(**route_values)
        self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)

    def test_12_so_partial_payment_link(self):
        # test customized /payment/pay route with sale_order_id param
        # partial amount specified
        self.amount = self.sale_order.amount_total / 2.0
        pay_route_values = self._prepare_pay_values()
        pay_route_values['sale_order_id'] = self.sale_order.id

        tx_context = self._get_portal_pay_context(**pay_route_values)

        self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
        self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
        self.assertEqual(tx_context['amount'], self.amount)

        tx_route_values = {
            'provider_id': self.provider.id,
            'payment_method_id': self.payment_method_id,
            'token_id': None,
            'amount': tx_context['amount'],
            'flow': 'direct',
            'tokenization_requested': False,
            'landing_route': tx_context['landing_route'],
            'access_token': tx_context['access_token'],
        }
        with mute_logger('odoo.addons.payment.models.payment_transaction'):
            processing_values = self._get_processing_values(
                tx_route=tx_context['transaction_route'], **tx_route_values
            )
        tx_sudo = self._get_tx(processing_values['reference'])

        self.assertEqual(tx_sudo.sale_order_ids, self.sale_order)
        self.assertEqual(tx_sudo.amount, self.amount)
        self.assertEqual(tx_sudo.partner_id, self.sale_order.partner_invoice_id)
        self.assertEqual(tx_sudo.company_id, self.sale_order.company_id)
        self.assertEqual(tx_sudo.currency_id, self.sale_order.currency_id)
        self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)

        tx_sudo._set_done()

        self.sale_order.require_payment = True
        self.assertTrue(self.sale_order._has_to_be_paid())
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx_sudo._post_process()
        self.assertEqual(self.sale_order.state, 'draft') # Only a partial amount was paid

        # Pay the remaining amount
        pay_route_values = self._prepare_pay_values()
        pay_route_values['sale_order_id'] = self.sale_order.id

        tx_context = self._get_portal_pay_context(**pay_route_values)

        self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
        self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
        self.assertEqual(tx_context['amount'], self.amount)

        tx_route_values = {
            'provider_id': self.provider.id,
            'payment_method_id': self.payment_method_id,
            'token_id': None,
            'amount': tx_context['amount'],
            'flow': 'direct',
            'tokenization_requested': False,
            'landing_route': tx_context['landing_route'],
            'access_token': tx_context['access_token'],
        }
        with mute_logger('odoo.addons.payment.models.payment_transaction'):
            processing_values = self._get_processing_values(
                tx_route=tx_context['transaction_route'], **tx_route_values
            )
        tx2_sudo = self._get_tx(processing_values['reference'])

        self.assertEqual(tx2_sudo.sale_order_ids, self.sale_order)
        self.assertEqual(tx2_sudo.amount, self.amount)
        self.assertEqual(tx2_sudo.partner_id, self.sale_order.partner_invoice_id)
        self.assertEqual(tx2_sudo.company_id, self.sale_order.company_id)
        self.assertEqual(tx2_sudo.currency_id, self.sale_order.currency_id)

        self.assertEqual(self.sale_order.state, 'draft')
        self.assertEqual(self.sale_order.transaction_ids, tx_sudo + tx2_sudo)

    def test_13_sale_automatic_partial_payment_link_delivery(self):
        """Test that with automatic invoice and invoicing policy based on delivered quantity, a transaction for the partial
        amount does not validate the SO."""
        # set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
        # invoicing policy is based on delivered quantity
        self.product.invoice_policy = 'delivery'

        self.amount = self.sale_order.amount_total / 2.0
        pay_route_values = self._prepare_pay_values()
        pay_route_values['sale_order_id'] = self.sale_order.id

        tx_context = self._get_portal_pay_context(**pay_route_values)

        tx_route_values = {
            'provider_id': self.provider.id,
            'payment_method_id': self.payment_method_id,
            'token_id': None,
            'amount': tx_context['amount'],
            'flow': 'direct',
            'tokenization_requested': False,
            'landing_route': tx_context['landing_route'],
            'access_token': tx_context['access_token'],
        }
        with mute_logger('odoo.addons.payment.models.payment_transaction'):
            processing_values = self._get_processing_values(
                tx_route=tx_context['transaction_route'], **tx_route_values
            )
        tx_sudo = self._get_tx(processing_values['reference'])

        tx_sudo._set_done()
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx_sudo._post_process()

        self.assertEqual(self.sale_order.state, 'draft', 'a partial transaction with automatic invoice and invoice_policy = delivery should not validate a quote')

    def test_confirmed_transactions_comfirms_so_with_multiple_transaction(self):
        """ Test that a confirmed transaction confirms a SO even if one or more non-confirmed
        transactions are linked. """
        # Create the payment
        self.amount = self.sale_order.amount_total
        self._create_transaction(
            flow='redirect',
            sale_order_ids=[self.sale_order.id],
            state='draft',
            reference='Test Transaction Draft 1',
        )
        self._create_transaction(
            flow='redirect',
            sale_order_ids=[self.sale_order.id],
            state='draft',
            reference='Test Transaction Draft 2',
        )
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')

    def test_auto_confirm_and_auto_invoice(self):
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Create the payment
        self.amount = self.sale_order.amount_total
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)

    def test_auto_done_and_auto_invoice(self):
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
        # Lock the sale orders when confirmed
        self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting')

        # Create the payment
        self.amount = self.sale_order.amount_total
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(self.sale_order.locked)
        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)
        self.assertTrue(tx.invoice_ids.is_move_sent)

    def test_so_partial_payment_no_invoice(self):
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Create the payment
        self.amount = self.sale_order.amount_total / 10.
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertEqual(self.sale_order.state, 'draft')
        self.assertFalse(tx.invoice_ids)
        self.assertFalse(self.sale_order.invoice_ids)

    def test_already_confirmed_so_payment(self):
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Confirm order before payment
        self.sale_order.action_confirm()

        # Create the payment
        self.amount = self.sale_order.amount_total
        tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
        tx._post_process()

        self.assertTrue(tx.invoice_ids)
        self.assertTrue(self.sale_order.invoice_ids)

    def test_invoice_is_final(self):
        """Test that invoice generated from a payment are always final"""
        # Set automatic invoice
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        # Create the payment
        self.amount = self.sale_order.amount_total
        tx = self._create_transaction(
            flow='redirect',
            sale_order_ids=[self.sale_order.id],
            state='done',
        )
        with mute_logger('odoo.addons.sale.models.payment_transaction'), patch(
            'odoo.addons.sale.models.sale_order.SaleOrder._create_invoices',
            return_value=self.env['account.move']
        ) as _create_invoices_mock:
            tx._post_process()

        self.assertTrue(_create_invoices_mock.call_args.kwargs['final'])

    def test_linked_transactions_when_invoicing(self):
        self.provider.support_manual_capture = 'partial'
        partial_amount = self.sale_order.amount_total - 2

        partial_tx_done = self._create_transaction(
            flow='direct',
            amount=partial_amount,
            sale_order_ids=[self.sale_order.id],
            state='done',
            reference='partial_tx_done',
        )
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            partial_tx_done._post_process()
        partial_tx_pending = self._create_transaction(
            flow='direct',
            amount=2,
            sale_order_ids=[self.sale_order.id],
            state='pending',
            reference='partial_tx_pending',
        )
        self.assertTrue(partial_tx_done.payment_id, msg="Account payment should have been created.")
        msg = "The created account payment shouldn't be reconciled as there are no invoice yet."
        self.assertFalse(partial_tx_pending.payment_id.is_reconciled, msg=msg)

        # Add some noisy transactions
        self._create_transaction(
            flow='direct', sale_order_ids=[self.sale_order.id], state='draft', reference='draft_tx'
        )
        self._create_transaction(
            flow='direct', sale_order_ids=[self.sale_order.id], state='error', reference='error_tx'
        )
        self._create_transaction(
            flow='direct', sale_order_ids=[self.sale_order.id], state='cancel', reference='cncl_tx'
        )

        msg = "The sale order should be linked to 5 transactions."
        self.assertEqual(len(self.sale_order.transaction_ids), 5, msg=msg)

        self.sale_order.action_confirm()
        self.sale_order._create_invoices()

        self.assertEqual(len(self.sale_order.invoice_ids), 1, msg="1 invoice should be created.")

        first_invoice = self.sale_order.invoice_ids
        linked_txs = first_invoice.transaction_ids
        msg = "The newly created invoice should be linked to the done and pending transactions."
        self.assertEqual(len(linked_txs), 2, msg=msg)
        expected_linked_tx = (partial_tx_done, partial_tx_pending)
        self.assertTrue(all(tx in expected_linked_tx for tx in linked_txs), msg=msg)
        msg = "The payment shouldn't be reconciled yet."
        self.assertFalse(partial_tx_done.payment_id.is_reconciled, msg=msg)

        partial_tx_done._post_process()

        msg = "The payment should now be reconciled."
        self.assertTrue(partial_tx_done.payment_id.is_reconciled, msg=msg)

        self.sale_order.order_line[0].product_uom_qty += 2
        self.sale_order._create_invoices()

        second_invoice = self.sale_order.invoice_ids - first_invoice
        msg = "The newly created invoice should only be linked to the pending transaction."
        self.assertEqual(len(second_invoice.transaction_ids), 1, msg=msg)
        self.assertEqual(second_invoice.transaction_ids.state, 'pending', msg=msg)

    def test_downpayment_confirm_sale_order_sufficient_amount(self):
        """Paying down payments can confirm an order if amount is enough."""
        self.sale_order.require_payment = True
        self.sale_order.prepayment_percent = 0.1
        order_amount = self.sale_order.amount_total

        tx = self._create_transaction(
            flow='direct',
            amount=order_amount * self.sale_order.prepayment_percent,
            sale_order_ids=[self.sale_order.id],
            state='done',
        )
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertTrue(self.sale_order.state == 'sale')

    def test_downpayment_confirm_sale_order_insufficient_amount(self):
        """Confirmation cannot occur if amount is not enough."""

        self.sale_order.require_payment = True
        self.sale_order.prepayment_percent = 0.2
        order_amount = self.sale_order.amount_total

        tx = self._create_transaction(
            flow='direct',
            amount=order_amount * 0.10,
            sale_order_ids=[self.sale_order.id],
            state='done',
        )
        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        self.assertTrue(self.sale_order.state == 'draft')

    def test_downpayment_confirm_sale_order_several_payments(self):
        """
        Several payments also trigger the confirmation of the sale order if
        down payment confirmation is allowed.
        """
        self.sale_order.require_payment = True
        self.sale_order.prepayment_percent = 0.2
        order_amount = self.sale_order.amount_total

        # Make a first payment, order should not be confirmed.
        tx = self._create_transaction(
            flow='direct',
            reference="Test down payment 1",
            amount=order_amount * 0.1,
            sale_order_ids=[self.sale_order.id],
            state='done',
        )
        tx._post_process()
        self.assertTrue(self.sale_order.state == 'draft')

        # Order should be confirmed after this payment.
        tx = self._create_transaction(
            flow='direct',
            reference="Test down payment 2",
            amount=order_amount * 0.15,
            sale_order_ids=[self.sale_order.id],
            state='done',
        )
        tx._post_process()
        self.assertTrue(self.sale_order.state == 'sale')

    def test_downpayment_automatic_invoice(self):
        """
        Down payment invoices should be created when a down payment confirms
        the order and automatic invoice is checked.
        """
        self.sale_order.require_payment = True
        self.sale_order.prepayment_percent = 0.2
        self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')

        tx = self._create_transaction(
            flow='direct',
            amount=self.sale_order.amount_total * self.sale_order.prepayment_percent,
            sale_order_ids=[self.sale_order.id],
            state='done')

        with mute_logger('odoo.addons.sale.models.payment_transaction'):
            tx._post_process()

        invoice = self.sale_order.invoice_ids
        self.assertTrue(len(invoice) == 1)
        self.assertTrue(invoice.line_ids[0].is_downpayment)

    def test_check_portal_access_token_before_rerouting_flow(self):
        """ Test that access to the provided sales order is checked against the portal access token
        before rerouting the payment flow. """
        payment_portal_controller = PaymentPortal()

        with patch.object(CustomerPortal, '_document_check_access') as mock:
            payment_portal_controller._get_extra_payment_form_values()
            self.assertEqual(
                mock.call_count, 0, msg="No check should be made when sale_order_id is not provided."
            )

            mock.reset_mock()

            payment_portal_controller._get_extra_payment_form_values(
                sale_order_id=self.sale_order.id, access_token='whatever'
            )
            self.assertEqual(
                mock.call_count, 1, msg="The check should be made as sale_order_id is provided."
            )

    def test_check_payment_access_token_before_rerouting_flow(self):
        """ Test that access to the provided sales order is checked against the payment access token
        before rerouting the payment flow. """
        payment_portal_controller = PaymentPortal()

        def _document_check_access_mock(*_args, **_kwargs):
            raise AccessError('')

        with patch.object(
            CustomerPortal, '_document_check_access', _document_check_access_mock
        ), patch('odoo.addons.payment.utils.check_access_token') as check_payment_access_token_mock:
            try:
                payment_portal_controller._get_extra_payment_form_values(
                    sale_order_id=self.sale_order.id, access_token='whatever'
                )
            except Exception:
                pass  # We don't care if it runs or not; we only count the calls.
            self.assertEqual(
                check_payment_access_token_mock.call_count,
                1,
                msg="The access token should be checked again as a payment access token if the"
                    " check as a portal access token failed.",
            )

    @mute_logger('odoo.http')
    def test_transaction_route_rejects_unexpected_kwarg(self):
        url = self._build_url(f'/my/orders/{self.sale_order.id}/transaction')
        route_kwargs = {
            'access_token': self.sale_order._portal_ensure_token(),
            'partner_id': self.partner.id,  # This should be rejected.
        }
        with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
            self.make_jsonrpc_request(url, route_kwargs)

    def test_partial_payment_confirm_order(self):
        """
        Test that a sale order can be confirmed through partial payments and that
        correct mails are sent each time.
        """

        self.amount = self.sale_order.amount_total / 2

        with patch(
            'odoo.addons.sale.models.sale_order.SaleOrder._send_order_notification_mail',
        ) as notification_mail_mock:
            tx_pending = self._create_transaction(
                flow='direct',
                sale_order_ids=[self.sale_order.id],
                state='pending',
                reference='Test Transaction Draft 1',
            )

            self.assertEqual(self.sale_order.state, 'draft')

            tx_pending._set_done()
            tx_pending._post_process()

            self.assertEqual(notification_mail_mock.call_count, 1)
            notification_mail_mock.assert_called_once_with(
                self.env.ref('sale.mail_template_sale_payment_executed'))
            self.assertEqual(self.sale_order.state, 'draft')
            self.assertEqual(self.sale_order.amount_paid, self.amount)

            tx_done = self._create_transaction(
                flow='direct',
                sale_order_ids=[self.sale_order.id],
                state='done',
                reference='Test Transaction Draft 2',
            )
            tx_done._post_process()

            self.assertEqual(notification_mail_mock.call_count, 2)
            notification_mail_mock.assert_called_with(
                self.env.ref('sale.mail_template_sale_confirmation'))
            self.assertEqual(self.sale_order.state, 'sale')
