# -*- coding: utf-8 -*-
import base64

from freezegun import freeze_time
from collections import Counter

from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
from odoo import fields
from odoo.modules.module import get_resource_path
from odoo.tests import tagged
from lxml import etree


class TestUBLCommon(AccountEdiTestCommon):

    @classmethod
    def setUpClass(cls, chart_template_ref=None, edi_format_ref=None):
        super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)

        # Required for `product_uom_id` to be visible in the form views
        cls.env.user.groups_id += cls.env.ref('uom.group_uom')

        # Ensure the testing currency is using a valid ISO code.
        real_usd = cls.env.ref('base.USD')
        real_usd.name = 'FUSD'
        real_usd.flush_model(['name'])
        cls.currency_data['currency'].name = 'USD'

        # remove this tax, otherwise, at import, this tax with children taxes can be selected and the total is wrong
        cls.tax_armageddon.children_tax_ids.unlink()
        cls.tax_armageddon.unlink()

        # Fixed Taxes
        cls.recupel = cls.env['account.tax'].create({
            'name': "RECUPEL",
            'amount_type': 'fixed',
            'amount': 1,
            'include_base_amount': True,
            'sequence': 1,
        })
        cls.auvibel = cls.env['account.tax'].create({
            'name': "AUVIBEL",
            'amount_type': 'fixed',
            'amount': 1,
            'include_base_amount': True,
            'sequence': 2,
        })

    @classmethod
    def setup_company_data(cls, company_name, chart_template=None, **kwargs):
        # OVERRIDE to force the company with EUR currency.
        eur = cls.env.ref('base.EUR')
        if not eur.active:
            eur.active = True

        res = super().setup_company_data(company_name, chart_template=chart_template, **kwargs)
        res['company'].currency_id = eur
        return res

    def assert_same_invoice(self, invoice1, invoice2, **invoice_kwargs):
        self.assertEqual(len(invoice1.invoice_line_ids), len(invoice2.invoice_line_ids))
        self.assertRecordValues(invoice2, [{
            'partner_id': invoice1.partner_id.id,
            'invoice_date': fields.Date.from_string(invoice1.date),
            'currency_id': invoice1.currency_id.id,
            'amount_untaxed': invoice1.amount_untaxed,
            'amount_tax': invoice1.amount_tax,
            'amount_total': invoice1.amount_total,
            **invoice_kwargs,
        }])

        default_invoice_line_kwargs_list = [{}] * len(invoice1.invoice_line_ids)
        invoice_line_kwargs_list = invoice_kwargs.get('invoice_line_ids', default_invoice_line_kwargs_list)
        self.assertRecordValues(invoice2.invoice_line_ids, [{
            'quantity': line.quantity,
            'price_unit': line.price_unit,
            'discount': line.discount,
            'product_id': line.product_id.id,
            'product_uom_id': line.product_uom_id.id,
            **invoice_line_kwargs,
        } for line, invoice_line_kwargs in zip(invoice1.invoice_line_ids, invoice_line_kwargs_list)])

    # -------------------------------------------------------------------------
    # IMPORT HELPERS
    # -------------------------------------------------------------------------

    @freeze_time('2017-01-01')
    def _assert_imported_invoice_from_etree(self, invoice, attachment):
        """
        Create an account.move directly from an attachment file, asserts the invoice obtained is the same as the expected
        invoice.
        """
        # /!\ use the same journal as the invoice's one to import the attachment !
        new_invoice = self.edi_format\
            .with_context(default_move_type=invoice.move_type)\
            ._create_document_from_attachment(attachment)

        self.assertTrue(new_invoice)
        self.assert_same_invoice(invoice, new_invoice)

    def _assert_imported_invoice_from_file(self, subfolder, filename, amount_total, amount_tax, list_line_subtotals,
                                           list_line_price_unit=None, list_line_discount=None, list_line_taxes=None,
                                           move_type='in_invoice', currency_id=None):
        """
        Create an empty account.move, update the file to fill its fields, asserts the currency, total and tax amounts
        are as expected.
        """
        if not currency_id:
            currency_id = self.env.ref('base.EUR').id

        # Create empty account.move, then update a file
        if move_type == 'in_invoice':
            invoice = self._create_empty_vendor_bill()
        elif move_type == 'out_invoice':
            invoice = self.env['account.move'].create({
                'move_type': move_type,
                'journal_id': self.company_data['default_journal_sale'].id,
            })
        else:
            invoice = self.env['account.move'].create({
                'move_type': move_type,
                'journal_id': self.company_data['default_journal_purchase'].id,
            })
        invoice_count = len(self.env['account.move'].search([]))

        # Import the file to fill the empty invoice
        self.update_invoice_from_file('l10n_account_edi_ubl_cii_tests', subfolder, filename, invoice)

        # Checks
        self.assertEqual(len(self.env['account.move'].search([])), invoice_count)
        self.assertRecordValues(invoice, [{
            'amount_total': amount_total,
            'amount_tax': amount_tax,
            'currency_id': currency_id,
        }])
        self.assertEqual(
            Counter(invoice.invoice_line_ids.mapped('price_subtotal')),
            Counter(list_line_subtotals),
        )
        if list_line_price_unit:
            self.assertEqual(invoice.invoice_line_ids.mapped('price_unit'), list_line_price_unit)
        if list_line_discount:
            # See test_import_tax_included: sometimes, it's impossible to retrieve the exact discount at import because
            # of rounding during export. The obtained discount might be 10.001 while the expected is 10.
            dp = self.env.ref('product.decimal_discount').precision_get("Discount")
            self.assertEqual([round(d, dp) for d in invoice.invoice_line_ids.mapped('discount')], list_line_discount)
        if list_line_taxes:
            for line, taxes in zip(invoice.invoice_line_ids, list_line_taxes):
                self.assertEqual(line.tax_ids, taxes)

    # -------------------------------------------------------------------------
    # EXPORT HELPERS
    # -------------------------------------------------------------------------

    @freeze_time('2017-01-01')
    def _generate_move(self, seller, buyer, **invoice_kwargs):
        """
        Create and post an account.move.
        """
        # Setup the seller.
        self.env.company.write({
            'partner_id': seller.id,
            'name': seller.name,
            'street': seller.street,
            'zip': seller.zip,
            'city': seller.city,
            'vat': seller.vat,
            'country_id': seller.country_id.id,
        })

        move_type = invoice_kwargs['move_type']
        account_move = self.env['account.move'].create({
            'partner_id': buyer.id,
            'partner_bank_id': (seller if move_type == 'out_invoice' else buyer).bank_ids[:1].id,
            'invoice_payment_term_id': self.pay_terms_b.id,
            'invoice_date': '2017-01-01',
            'date': '2017-01-01',
            'currency_id': self.currency_data['currency'].id,
            'narration': 'test narration',
            'ref': 'ref_move',
            **invoice_kwargs,
            'invoice_line_ids': [
                (0, 0, {
                    'sequence': i,
                    **invoice_line_kwargs,
                })
                for i, invoice_line_kwargs in enumerate(invoice_kwargs.get('invoice_line_ids', []))
            ],
        })
        # this is needed for formats not enabled by default on the journal
        account_move.journal_id.edi_format_ids += self.edi_format
        account_move.action_post()
        return account_move

    def _assert_invoice_attachment(self, invoice, xpaths, expected_file):
        """
        Get attachment from a posted account.move, and asserts it's the same as the expected xml file.
        """
        attachment = invoice._get_edi_attachment(self.edi_format)
        self.assertTrue(attachment)
        xml_filename = attachment.name
        xml_content = base64.b64decode(attachment.with_context(bin_size=False).datas)
        xml_etree = self.get_xml_tree_from_string(xml_content)

        expected_file_path = get_resource_path('l10n_account_edi_ubl_cii_tests', 'tests/test_files', expected_file)
        expected_etree = etree.parse(expected_file_path).getroot()

        modified_etree = self.with_applied_xpath(
            expected_etree,
            xpaths
        )

        self.assertXmlTreeEqual(
            xml_etree,
            modified_etree,
        )

        return attachment

    def _import_invoice_attachment(self, invoice, edi_code, journal):
        """ Extract the attachment from the invoice and import it on the given journal.
        """
        # Get the attachment from the invoice
        edi_attachment = invoice.edi_document_ids.filtered(
            lambda doc: doc.edi_format_id.code == edi_code).attachment_id
        edi_etree = self.get_xml_tree_from_string(edi_attachment.raw)

        # import the attachment and return the resulting invoice
        return self.edi_format._create_invoice_from_xml_tree(
            filename='test_filename',
            tree=edi_etree,
            journal=journal,
        )

    def _test_encoding_in_attachment(self, edi_code, filename):
        """
        Generate an invoice, assert that the tag '<?xml version='1.0' encoding='UTF-8'?>' is present in the attachment
        """
        invoice = self._generate_move(
            seller=self.partner_1,
            buyer=self.partner_2,
            move_type='out_invoice',
            invoice_line_ids=[{'product_id': self.product_a.id}],
        )
        edi_attachment = invoice.edi_document_ids.filtered(
            lambda doc: doc.edi_format_id.code == edi_code).attachment_id
        self.assertEqual(edi_attachment.name, filename)
        self.assertIn(b"<?xml version='1.0' encoding='UTF-8'?>", edi_attachment.raw)
