from lxml import etree

from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.tools import misc


@tagged('post_install_l10n', 'post_install', '-at_install')
class TestMyDATAInvoice(AccountTestInvoicingCommon):

    @classmethod
    @AccountTestInvoicingCommon.setup_country('gr')
    def setUpClass(cls):
        super().setUpClass()

        cls.env.company.write({
            'name': 'My Greece Company',
            'vat': '047747270',
            'l10n_gr_edi_test_env': True,
            'l10n_gr_edi_aade_id': 'odoodev',
            'l10n_gr_edi_aade_key': '20ea658627fd8c7d90594fe4601d3327',
        })
        cls.partner_a.write({
            'country_id': cls.env.ref('base.gr').id,
            'vat': '047747210',
        })
        cls.env['res.company'].create({
            'name': 'Greece Partner A',
            'partner_id': cls.partner_a.id,
            'l10n_gr_edi_test_env': True,
        })
        cls.tax_24 = cls.env.ref("account.%s_l10n_gr_tax_s24_G" % cls.env.company.id)
        cls.tax_13 = cls.env.ref("account.%s_l10n_gr_tax_s13_G" % cls.env.company.id)
        cls.tax_0 = cls.env.ref("account.%s_l10n_gr_tax_s0_exempt" % cls.env.company.id)

    def _create_mydata_invoice(
            self,
            inv_type='1.1',
            tax_ids=False,
            cls_category='category1_1',
            cls_type='E3_561_001',
            post=True,
            paid=True,
            **kwargs,
    ):
        invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_a.id,
            'invoice_date': '2024-01-01',
            'date': '2024-01-01',
            'l10n_gr_edi_inv_type': inv_type,
            'invoice_line_ids': [Command.create({
                'product_id': self.product_a.id,
                'tax_ids': tax_ids or [Command.set(self.tax_24.ids)],
                'l10n_gr_edi_cls_category': cls_category,
                'l10n_gr_edi_cls_type': cls_type,
            })],
            **kwargs,
        })
        if post:
            invoice.action_post()
        if paid:
            self.env['account.payment.register'] \
                .with_context(active_ids=invoice.ids, active_model='account.move') \
                .create({'payment_date': invoice.date}) \
                ._create_payments()

        return invoice

    def _create_mydata_bill(
            self,
            mydata_mark='400001924190891',
            inv_type='13.1',
            invoice_line_ids=False,
            **kwargs,
    ):
        if not invoice_line_ids:
            invoice_line_ids = [
                Command.create({
                    'product_id': self.product_a.id,
                    'tax_ids': [Command.set(self.tax_24.ids)],
                    'l10n_gr_edi_cls_category': 'category2_1',
                    'l10n_gr_edi_cls_type': 'E3_102_001',
                }),
                Command.create({
                    'product_id': self.product_b.id,
                    'tax_ids': [Command.set(self.tax_13.ids)],
                    'l10n_gr_edi_cls_category': 'category2_10',
                    'l10n_gr_edi_cls_type': 'E3_313_004',
                }),
            ]
        bill = self._create_mydata_invoice(
            move_type='in_invoice',
            inv_type=inv_type,
            invoice_line_ids=invoice_line_ids,
            **kwargs,
        )
        bill.l10n_gr_edi_document_ids = self.env['l10n_gr_edi.document'].create([{
            'move_id': bill.id,
            'mydata_mark': mydata_mark,
            'state': 'bill_fetched',
        }])
        return bill

    @staticmethod
    def _add_address(partner):
        partner.write({'zip': '10431', 'city': 'Athens'})

    def assert_mydata_xml_tree(self, invoice, expected_file_path, send_classification=False):
        if send_classification:
            xml_template = 'l10n_gr_edi.mydata_expense_classification'
            xml_vals = invoice._l10n_gr_edi_get_expense_classification_xml_vals()
        else:
            xml_template = 'l10n_gr_edi.mydata_invoice'
            xml_vals = invoice._l10n_gr_edi_get_invoices_xml_vals()

        xml_content = self.env['account.move']._l10n_gr_edi_generate_xml_content(xml_template, xml_vals)
        xml_etree = self.get_xml_tree_from_string(xml_content)

        expected_file_full_path = misc.file_path(f'{self.test_module}/tests/test_files/{expected_file_path}')
        expected_etree = etree.parse(expected_file_full_path).getroot()

        self.assertXmlTreeEqual(xml_etree, expected_etree)

    def assert_mydata_error(self, invoice, expected_error_message):
        """
        :param account.move invoice:
        :param str expected_error_message:
        """
        document = invoice.l10n_gr_edi_document_ids.sorted()[0]
        self.assertRecordValues(document, [{
            'state': 'invoice_error' if invoice.is_sale_document(include_receipts=True) else 'bill_error',
            'message': expected_error_message,
        }])

    ####################################################################################################
    # Test: assert available classification value for dynamic selection fields
    ####################################################################################################

    def test_mydata_available_inv_type_values(self):
        invoice = self._create_mydata_invoice(inv_type='1.1', cls_category='', cls_type='')
        self.assertRecordValues(invoice, [{
            'l10n_gr_edi_available_inv_type': '1.1,1.2,1.3,1.4,1.5,1.6,2.1,2.2,2.3,2.4,3.1,3.2,5.1,5.2,'
                                              '6.1,6.2,7.1,8.1,8.2,11.1,11.2,11.3,11.4,11.5,17.3,17.4',
        }])
        self.assertRecordValues(invoice.invoice_line_ids, [{
            'l10n_gr_edi_available_cls_category': 'category1_1,category1_2,category1_3,category1_4,category1_5,'
                                                  'category1_7,category1_8,category1_9,category1_95',
            'l10n_gr_edi_available_cls_type': False,
            'l10n_gr_edi_available_cls_vat': False,
        }])

        invoice.invoice_line_ids.l10n_gr_edi_cls_category = 'category1_1'
        self.assertRecordValues(invoice.invoice_line_ids, [{
            'l10n_gr_edi_available_cls_type': 'E3_561_001,E3_561_002,E3_561_007',
        }])
        invoice.invoice_line_ids.l10n_gr_edi_cls_category = 'category1_8'
        # In some cases, the order of available types are jumbled
        self.assertEqual(sorted(invoice.invoice_line_ids.l10n_gr_edi_available_cls_type.split(',')), [
            'E3_561_001', 'E3_561_002', 'E3_561_007', 'E3_562', 'E3_563', 'E3_564', 'E3_565', 'E3_566', 'E3_567',
            'E3_568', 'E3_570', 'E3_596', 'E3_597', 'E3_880_001', 'E3_881_001', 'E3_881_003', 'E3_881_004'])

    ####################################################################################################
    # Test: assert XML tree to file
    ####################################################################################################

    def test_mydata_send_invoice(self):
        invoice = self._create_mydata_invoice(invoice_line_ids=[
            Command.create({
                'product_id': self.product_a.id,
                'tax_ids': [Command.set(self.tax_24.ids)],
                'l10n_gr_edi_cls_category': 'category1_1',
                'l10n_gr_edi_cls_type': 'E3_561_001',
            }),
            Command.create({
                'product_id': self.product_b.id,
                'tax_ids': [Command.set(self.tax_13.ids)],
                'l10n_gr_edi_cls_category': 'category1_3',
                'l10n_gr_edi_cls_type': 'E3_561_002',
            }),
        ])
        self.assert_mydata_xml_tree(invoice, expected_file_path='from_odoo/mydata_invoice.xml')

    def test_mydata_send_multi_invoices(self):
        invoice_1 = self._create_mydata_invoice(inv_type='2.1', cls_category='category1_3', cls_type='E3_561_002')
        invoice_2 = self._create_mydata_invoice(inv_type='11.1', cls_category='category1_95', cls_type='')
        self.assert_mydata_xml_tree(invoice_1 + invoice_2, expected_file_path='from_odoo/mydata_multi_invoices.xml')

    def test_mydata_send_bill_cls_expense(self):
        bill = self._create_mydata_bill()
        self.assert_mydata_xml_tree(bill, expected_file_path='from_odoo/mydata_cls_expense.xml', send_classification=True)

    ####################################################################################################
    # Test: assert built-in constraints
    ####################################################################################################

    def test_l10n_gr_edi_try_send_invoices_no_credentials_and_vat(self):
        self.company_data['company'].write({
            'l10n_gr_edi_aade_id': False,
            'l10n_gr_edi_aade_key': False,
        })
        invoice = self._create_mydata_invoice()
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, "You need to set AADE ID and Key in the company settings.")

    def test_l10n_gr_edi_try_send_invoices_no_classification(self):
        # No invoice type
        invoice = self._create_mydata_invoice()
        invoice.l10n_gr_edi_inv_type = False
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, 'Missing MyDATA Invoice Type.')

        # No classification category
        invoice = self._create_mydata_invoice(cls_category='')
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, 'Missing MyDATA classification category on line 1.')

        # No classification type, and inv_type + cls_category combination doesn't allow empty cls_type
        invoice = self._create_mydata_invoice(cls_type='')
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, 'Missing MyDATA classification type on line 1.')

    def test_l10n_gr_edi_try_send_invoices_allowed_no_cls_type(self):
        """Allow no cls_type on some combinations with available cls_type"""
        allowed_inv_type_category = (('1.1', 'category1_95'), ('3.2', 'category1_95'), ('5.1', 'category1_95'))
        for inv_type, category in allowed_inv_type_category:
            invoice = self._create_mydata_invoice(inv_type=inv_type, cls_category=category, cls_type='')
            self.assertFalse(invoice._l10n_gr_edi_get_pre_error_string())

    def test_l10n_gr_edi_try_send_invoices_invalid_tax_amount(self):
        """ Invalid tax amount should raises error. """
        invalid_tax = self.env['account.tax'].create([{
            'name': 'Bad 12%',
            'type_tax_use': 'sale',
            'amount': 12.0,
            'company_id': self.env.company.id,
        }])
        invoice = self._create_mydata_invoice(tax_ids=[Command.set(invalid_tax.ids)])
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, 'Invalid tax amount for line 1. The valid values are 24, 13, 6, 17, 9, 4, 0.')

    def test_l10n_gr_edi_try_send_invoices_invalid_tax_multi(self):
        """ Multiple tax should raises error. """
        invoice = self._create_mydata_invoice(tax_ids=[Command.set((self.tax_24 + self.tax_0).ids)])
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, 'MyDATA does not support multiple taxes on line 1.')

    def test_l10n_gr_edi_try_send_invoices_invalid_tax_nonexistent(self):
        """ No tax should raises error. """
        invoice = self._create_mydata_invoice(post=False)
        invoice.invoice_line_ids.tax_ids = False
        invoice.action_post()
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, 'Missing tax on line 1.')

    def test_l10n_gr_edi_try_send_invoices_invalid_tax_exempt_no_category(self):
        """ Tax 0% and no tax exemption category should raises error. """
        invoice = self._create_mydata_invoice(tax_ids=[Command.set(self.tax_0.ids)])
        invoice.with_context(skip_readonly_check=True).invoice_line_ids.l10n_gr_edi_tax_exemption_category = False
        invoice.l10n_gr_edi_try_send_invoices()
        self.assert_mydata_error(invoice, 'Missing MyDATA Tax Exemption Category for line 1.')
