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

import contextlib
import datetime
import requests

from unittest import mock

from odoo import Command, tools
from odoo.addons.account.tests.common import AccountTestInvoicingCommon


class L10nHuEdiTestCommon(AccountTestInvoicingCommon):
    @classmethod
    @AccountTestInvoicingCommon.setup_country('hu')
    def setUpClass(cls):
        super().setUpClass()

        cls.today = datetime.date.today()
        cls.yesterday = cls.today - datetime.timedelta(days=1)

        # Company
        company = cls.company_data['company']
        company.write({
            'city': 'Budapest',
            'zip': '1073',
            'street': 'Akácfa utca 74.',
            'country_id': cls.env.ref('base.hu').id,
            'vat': '27725414-2-13',
        })

        cls.write_edi_credentials()

        # Products
        cls.product_service = cls.env['product.product'].create({
            'name': 'Consultancy Service',
            'type': 'service',
            'uom_id': cls.env.ref('uom.product_uom_hour').id,
            'property_account_income_id': cls.company_data['default_account_revenue'].id,
            'property_account_expense_id': cls.company_data['default_account_expense'].id,
        })
        cls.product_no_uom = cls.env['product.product'].create({
            'name': 'Item without UoM',
            'type': 'service',
            'property_account_income_id': cls.company_data['default_account_revenue'].id,
            'property_account_expense_id': cls.company_data['default_account_expense'].id,
        })

        # Partners
        cls.partner_company = cls.env['res.partner'].create({
            'name': 'Magyar Vevő Kft.',
            'street': 'Alkotmány utca 11.',
            'city': 'Debrecen',
            'zip': '4000',
            'country_id': cls.env.ref('base.hu').id,
            'vat': '14933477-2-13',
            'invoice_edi_format': False,
        })
        cls.partner_group_company_1 = cls.env['res.partner'].create({
            'name': 'MOL Nyrt.',
            'street': 'Dombóvári út 28.',
            'city': 'Budapest',
            'zip': '1117',
            'country_id': cls.env.ref('base.hu').id,
            'vat': '10625790-4-44',
            'l10n_hu_group_vat': '17781774-5-44',
        })
        cls.partner_group_company_2 = cls.env['res.partner'].create({
            'name': 'MOL Petrolkémia Zrt.',
            'street': 'TVK-Ipartelep, TVK Központi Irodaház 2119/3hrsz. 136. ép.',
            'city': 'Tiszaújváros',
            'zip': '3581',
            'country_id': cls.env.ref('base.hu').id,
            'vat': '10725759-4-05',
            'l10n_hu_group_vat': '17781774-5-44',
        })

        # Currency rates
        currency_eur = cls.env.ref('base.EUR')
        currency_eur.active = True
        cls.env['res.currency.rate'].create(
            {
                'name': cls.today - datetime.timedelta(days=1),
                'currency_id': currency_eur.id,
                'company_id': company.id,
                'inverse_company_rate': '380.77',
            }
        )
        cls.env['res.currency.rate'].create(
            {
                'name': cls.today - datetime.timedelta(days=2),
                'currency_id': currency_eur.id,
                'company_id': company.id,
                'inverse_company_rate': '377.66',
            }
        )

        # Taxes
        cls.tax_vat = cls.env['account.chart.template'].ref('F27')
        cls.tax_vat_18 = cls.env['account.chart.template'].ref('F18')
        cls.tax_aam = cls.env['account.chart.template'].ref('VA')
        cls.tax_price_include = cls.env['account.tax'].create({
            'name': 'Excise tax',
            'amount_type': 'percent',
            'amount': 30.0,
            'country_id': company.account_fiscal_country_id.id,
            'tax_exigibility': 'on_invoice',
            'price_include_override': 'tax_included',
            'include_base_amount': True,
            'l10n_hu_tax_type': 'VAT',
            'invoice_repartition_line_ids': [
                Command.create({'repartition_type': 'base'}),
                Command.create({
                    'repartition_type': 'tax',
                    'account_id': cls.company_data['default_account_tax_sale'].id,
                }),
            ],
            'refund_repartition_line_ids': [
                Command.create({'repartition_type': 'base'}),
                Command.create({
                    'repartition_type': 'tax',
                    'account_id': cls.company_data['default_account_tax_sale'].id,
                }),
            ],
        })

    @classmethod
    def write_edi_credentials(cls):
        # Set up test EDI user
        return cls.company_data['company'].write({
            'l10n_hu_edi_server_mode': 'test',
            'l10n_hu_edi_username': 'this',
            'l10n_hu_edi_password': 'that',
            'l10n_hu_edi_signature_key': 'some_key',
            'l10n_hu_edi_replacement_key': 'abcdefghijklmnop',
        })
    
    def _create_simple_move(self, move_type='out_invoice', currency=None, amount=None):
        journal = self.company_data['default_journal_sale'] if move_type in self.env['account.move'].get_sale_types() else self.company_data['default_journal_purchase']

        return self.env['account.move'].create({
            'move_type': move_type,
            'journal_id': journal.id,
            'currency_id': (currency or self.env.ref('base.HUF')).id,
            'partner_id': self.partner_company.id,
            'invoice_date': self.today,
            'delivery_date': self.today,
            'invoice_line_ids': [
                Command.create({
                    'product_id': self.product_a.id,
                    'price_unit': amount or 10000.0,
                    'quantity': 1,
                    'tax_ids': [Command.set(self.tax_vat.ids)],
                })
            ]
        })

    def create_invoice_simple(self, currency=None, amount=None):
        """ Create a really basic invoice - just one line. """
        return self._create_simple_move(move_type='out_invoice', currency=currency, amount=amount)
    
    def create_bill_simple(self, currency=None):
        """ Create a really basic bill - just one line. """
        return self._create_simple_move(move_type='in_invoice', currency=currency)
    
    def create_credit_note_simple(self, currency=None):
        """ Create a really basic credit note - just one line. """
        return self._create_simple_move(move_type='out_refund', currency=currency)
    
    def create_refund_simple(self, currency=None):
        """ Create a really basic bill refund - just one line. """
        return self._create_simple_move(move_type='in_refund', currency=currency)

    def create_invoice_simple_discount(self):
        """ Create a really basic invoice with a discount - just one line. """
        return self.env['account.move'].create({
            'move_type': 'out_invoice',
            'journal_id': self.company_data['default_journal_sale'].id,
            'currency_id': self.env.ref('base.HUF').id,
            'partner_id': self.partner_company.id,
            'invoice_date': self.today,
            'delivery_date': self.today,
            'invoice_line_ids': [
                Command.create({
                    'product_id': self.product_a.id,
                    'price_unit': 10000.0,
                    'quantity': 1,
                    'discount': 20,
                    'tax_ids': [Command.set(self.tax_vat.ids)],
                })
            ]
        })

    def create_invoice_tax_price_include(self):
        """ Create an invoice with :
            * one line using a tax with price included
            * one line using a tax with price included and a discount
        """
        return self.env['account.move'].create({
            'move_type': 'out_invoice',
            'journal_id': self.company_data['default_journal_sale'].id,
            'currency_id': self.env.ref('base.HUF').id,
            'partner_id': self.partner_company.id,
            'invoice_date': self.today,
            'delivery_date': self.today,
            'invoice_line_ids': [
                Command.create({
                    'product_id': self.product_a.id,
                    'price_unit': 1000.00,
                    'quantity': 1,
                    'tax_ids': [Command.set(self.tax_price_include.ids)],
                }),
                Command.create({
                    'product_id': self.product_a.id,
                    'price_unit': 1000.00,
                    'quantity': 1,
                    'discount': 20,
                    'tax_ids': [Command.set(self.tax_price_include.ids)],
                }),
            ]
        })

    def create_advance_invoice(self):
        """ Create a sale order and an advance invoice. """
        self.product_a.invoice_policy = 'order'
        pricelist_huf = self.env['product.pricelist'].create({
            'name': 'HUF pricelist',
            'currency_id': self.company_data['company'].currency_id.id,
            'company_id': False,
        })
        sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
            'partner_id': self.partner_company.id,
            'pricelist_id': pricelist_huf.id,
            'order_line': [
                Command.create({
                    'product_id': self.product_a.id,
                    'product_uom_qty': 1,
                    'price_unit': 10000.0,
                    'tax_ids': [Command.set(self.tax_vat.ids)],
                })
            ]
        })
        sale_order.action_confirm()

        downpayment = self.env['sale.advance.payment.inv'].with_context({
            'active_model': 'sale.order',
            'active_ids': [sale_order.id],
            'active_id': sale_order.id,
            'default_journal_id': self.company_data['default_journal_sale'].id,
        }).create({
            'advance_payment_method': 'fixed',
            'fixed_amount': 6350.0,
        })
        downpayment.create_invoices()
        return sale_order, sale_order.invoice_ids

    def create_final_invoice(self, sale_order):
        """ Create a final invoice for a sale order """
        advance_invoice = sale_order.invoice_ids
        final_payment = self.env['sale.advance.payment.inv'].with_context({
            'active_model': 'sale.order',
            'active_ids': [sale_order.id],
            'active_id': sale_order.id,
            'default_journal_id': self.company_data['default_journal_sale'].id,
        }).create({
            'advance_payment_method': 'delivered',
        })
        final_payment.create_invoices()
        final_invoice = sale_order.invoice_ids - advance_invoice

        return final_invoice

    def create_invoice_complex_huf(self):
        """ Create a complex invoice in HUF, with cash rounding. """
        return self.env['account.move'].create({
            'move_type': 'out_invoice',
            'journal_id': self.company_data['default_journal_sale'].id,
            'currency_id': self.env.ref('base.HUF').id,
            'partner_id': self.partner_company.id,
            'invoice_date': self.today,
            'delivery_date': self.today,
            'l10n_hu_payment_mode': 'TRANSFER',
            'invoice_cash_rounding_id': self.env['account.chart.template'].with_company(self.company).ref('cash_rounding_1_huf').id,
            'invoice_line_ids': [
                Command.create({
                    'product_id': self.product_a.id,
                    'price_unit': 10.00,
                    'quantity': 3,
                    'discount': 20,
                    'tax_ids': [Command.set(self.tax_vat.ids)],
                }),
                Command.create({
                    'product_id': self.product_b.id,
                    'price_unit': 19.99,
                    'quantity': 1,
                    'tax_ids': [Command.set(self.tax_vat.ids)],
                }),
                Command.create({
                    'product_id': self.product_service.id,
                    'price_unit': 50.00,
                    'quantity': 2,
                    'tax_ids': [Command.set(self.tax_vat_18.ids)],
                }),
                Command.create({
                    'product_id': self.product_no_uom.id,
                    'price_unit': 200.00,
                    'quantity': 1,
                    'tax_ids': [Command.set(self.tax_aam.ids)],
                })
            ]
        })

    def create_invoice_complex_eur(self):
        """ Create a complex invoice in EUR with several lines, different taxes, and discounts. """
        return self.env['account.move'].create({
            'move_type': 'out_invoice',
            'journal_id': self.company_data['default_journal_sale'].id,
            'currency_id': self.env.ref('base.EUR').id,
            'partner_id': self.partner_company.id,
            'invoice_date': self.yesterday,
            'delivery_date': self.today,
            'l10n_hu_payment_mode': 'TRANSFER',
            'invoice_line_ids': [
                Command.create({
                    'product_id': self.product_a.id,
                    'price_unit': 10.00,
                    'quantity': 3,
                    'discount': 20,
                    'tax_ids': [Command.set(self.tax_vat.ids)],
                }),
                Command.create({
                    'product_id': self.product_b.id,
                    'price_unit': 19.99,
                    'quantity': 1,
                    'tax_ids': [Command.set(self.tax_vat.ids)],
                }),
                Command.create({
                    'product_id': self.product_service.id,
                    'price_unit': 50.00,
                    'quantity': 2,
                    'tax_ids': [Command.set(self.tax_vat_18.ids)],
                }),
                Command.create({
                    'product_id': self.product_no_uom.id,
                    'price_unit': 200.00,
                    'quantity': 1,
                    'tax_ids': [Command.set(self.tax_aam.ids)],
                })
            ]
        })

    def create_reversal(self, invoice, is_modify=False, amount=None):
        """ Create a credit note that reverses an invoice. """
        wizard_vals = {'journal_id': invoice.journal_id.id}
        wizard_reverse = self.env['account.move.reversal'].with_context(active_ids=invoice.ids, active_model='account.move').create(wizard_vals)
        wizard_reverse.reverse_moves(is_modify=is_modify)
        reversal_moves = wizard_reverse.new_move_ids
        if amount:
            reversal_moves.invoice_line_ids.write({
                'price_unit': amount
            })
        return reversal_moves

    def create_debit_note(self, invoice, amount=None):
        """ Create Debit Note """
        wizard_vals = {
            'journal_id': invoice.journal_id.id,
            'copy_lines': True,
        }
        move_debit_note_wiz = self.env['account.debit.note'].with_context(active_model="account.move", active_ids=invoice.ids).create(wizard_vals)
        move_debit_note_wiz.create_debit()
        debit_note = self.env['account.move'].search([('debit_origin_id', '=', invoice.id)])
        if amount:
            debit_note.invoice_line_ids.write({
                'price_unit': amount
            })
        return debit_note

    def register_payment(self, invoice, amount):
        payment_register = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
            'payment_date': self.today,
            'journal_id': self.company_data['default_journal_bank'].id,
            'amount': amount,
        })
        payment_register.action_create_payments()

    def create_cancel_wizard(self):
        """ Create an invoice, send it, and create a cancellation wizard for it. """
        invoice = self.create_invoice_simple()
        invoice.action_post()
        send_and_print = self.create_send_and_print(invoice, sending_methods=[])
        self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked'))
        self.assertFalse(invoice._l10n_hu_edi_check_invoices())
        send_and_print.action_send_and_print()
        cancel_wizard = self.env['l10n_hu_edi.cancellation'].with_context({"default_invoice_id": invoice.id}).create({
            'code': 'ERRATIC_DATA',
            'reason': 'Some reason...',
        })
        return invoice, cancel_wizard

    def _get_mocked_requests(self):
        return ['manageInvoice', 'queryTaxpayer', 'tokenExchange', 'queryTransactionStatus', 'queryTransactionList', 'manageAnnulment']

    def _get_request_file_name(self, service, data):
        return f'{service}_request'

    def _get_response_file_name(self, service, data):
        return f'{service}_response'

    @contextlib.contextmanager
    def patch_post(self, responses=None):
        """ Patch requests.Session in l10n_hu_edi.connection.

        :param responses: If specified, a dict {service: response} that gives, for any service,
                          bytes that should be served as response data, or an Exception that should be raised.
                          Otherwise, will use the default responses stored under
                          mocked_requests/{service}_response.xml
        """
        test_case = self

        class MockedSession:
            def post(self, url, data, headers, timeout=None):
                prod_url = 'https://api.onlineszamla.nav.gov.hu/invoiceService/v3'
                demo_url = 'https://api-test.onlineszamla.nav.gov.hu/invoiceService/v3'
                folder_path = f'{test_case.test_module}/tests/mocked_requests'

                base_url, __, service = url.rpartition('/')
                if base_url not in (prod_url, demo_url) or service not in test_case._get_mocked_requests():
                    test_case.fail(f'Invalid POST url: {url}')

                request_file_name = test_case._get_request_file_name(service, data)
                with tools.file_open(f'{folder_path}/{request_file_name}.xml', 'rb') as expected_request_file:
                    test_case.assertXmlTreeEqual(
                        test_case.get_xml_tree_from_string(data),
                        test_case.get_xml_tree_from_string(expected_request_file.read()),
                    )

                mock_response = mock.Mock(spec=requests.Response)
                mock_response.status_code = 200
                mock_response.headers = ''

                if responses and service in responses:
                    if isinstance(responses[service], Exception):
                        raise responses[service]
                    mock_response.text = responses[service]
                else:
                    response_file_name = test_case._get_response_file_name(service, data)
                    with tools.file_open(f'{folder_path}/{response_file_name}.xml', 'r') as response_file:
                        mock_response.text = response_file.read()
                return mock_response

            def close(self):
                pass

        with mock.patch('odoo.addons.l10n_hu_edi.models.l10n_hu_edi_connection.requests.Session', side_effect=MockedSession, autospec=True):
            yield
