# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.common import AccountTestInvoicingCommon

from contextlib import contextmanager
from functools import wraps
from unittest.mock import patch

import base64


def _generate_mocked_needs_web_services(needs_web_services):
    return lambda edi_format: needs_web_services


def _mocked_get_move_applicability(edi_format, move):
    return {
        'post': edi_format._post_invoice_edi,
        'cancel': edi_format._cancel_invoice_edi,
    }

def _mocked_check_move_configuration_success(edi_format, move):
    return []


def _mocked_check_move_configuration_fail(edi_format, move):
    return ['Fake error (mocked)']


def _mocked_cancel_success(edi_format, invoices):
    return {invoice: {'success': True} for invoice in invoices}


class AccountEdiTestCommon(AccountTestInvoicingCommon):
    # To override by the helper method setup_edi_format to set up an edi format
    edi_format_ref = False

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

        # ==== EDI ====
        if cls.edi_format_ref:
            cls.edi_format = cls.env.ref(cls.edi_format_ref)
        else:
            with cls.mock_edi(cls, _needs_web_services_method=_generate_mocked_needs_web_services(True)):
                cls.edi_format = cls.env['account.edi.format'].sudo().create({
                    'name': 'Test EDI format',
                    'code': 'test_edi',
                })
        cls.journal = cls.company_data['default_journal_sale']
        cls.journal.edi_format_ids = [(6, 0, cls.edi_format.ids)]

    @staticmethod
    def setup_edi_format(edi_format_ref):
        def _decorator(function):
            @wraps(function)
            def wrapper(self):
                self.edi_format_ref = edi_format_ref
                function(self)
            return wrapper

        return _decorator

    ####################################################
    # EDI helpers
    ####################################################

    def _create_fake_edi_attachment(self):
        return self.env['ir.attachment'].create({
            'name': '_create_fake_edi_attachment.xml',
            'datas': base64.encodebytes(b"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
            'mimetype': 'application/xml'
        })

    @contextmanager
    def with_custom_method(self, method_name, method_content):
        path = f'odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat.{method_name}'
        with patch(path, new=method_content, create=not hasattr(self.env['account.edi.format'], method_name)):
            yield

    @contextmanager
    def mock_edi(self,
                 _get_move_applicability_method=_mocked_get_move_applicability,
                 _needs_web_services_method=_generate_mocked_needs_web_services(False),
                 _check_move_configuration_method=_mocked_check_move_configuration_success,
                 ):

        try:
            with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._needs_web_services',
                       new=_needs_web_services_method), \
                 patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._check_move_configuration',
                       new=_check_move_configuration_method), \
                 patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._get_move_applicability',
                       new=_get_move_applicability_method):

                yield
        finally:
            pass

    def edi_cron(self):
        self.env['account.edi.document'].sudo().search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services(with_commit=False)

    def assert_generated_file_equal(self, invoice, expected_values, applied_xpath=None):
        invoice.action_post()
        invoice.edi_document_ids._process_documents_web_services(with_commit=False)  # synchronous are called in post, but there's no CRON in tests for asynchronous
        attachment = invoice._get_edi_attachment(self.edi_format)
        if not attachment:
            raise ValueError('No attachment was generated after posting EDI')
        xml_content = base64.b64decode(attachment.with_context(bin_size=False).datas)
        current_etree = self.get_xml_tree_from_string(xml_content)
        expected_etree = self.get_xml_tree_from_string(expected_values)
        if applied_xpath:
            expected_etree = self.with_applied_xpath(expected_etree, applied_xpath)
        self.assertXmlTreeEqual(current_etree, expected_etree)

    def create_edi_document(self, edi_format, state, move=None, move_type=None):
        """ Creates a document based on an existing invoice or creates one, too.

        :param edi_format:  The edi_format of the document.
        :param state:       The state of the document.
        :param move:        The move of the document or None to create a new one.
        :param move_type:   If move is None, the type of the invoice to create, defaults to 'out_invoice'.
        """
        move = move or self.init_invoice(move_type or 'out_invoice', products=self.product_a)
        return self.env['account.edi.document'].create({
            'edi_format_id': edi_format.id,
            'move_id': move.id,
            'state': state
        })

    def _process_documents_web_services(self, moves, formats_to_return=None):
        """ Generates and returns EDI files for the specified moves.
        formats_to_return is an optional parameter used to pass a set of codes from
        the formats we want to return the files for (in case we want to test specific formats).
        Other formats will still generate documents, they simply won't be returned.
        """
        moves.edi_document_ids._process_documents_web_services(with_commit=False)

        documents_to_return = moves.edi_document_ids
        if formats_to_return != None:
            documents_to_return = documents_to_return.filtered(lambda x: x.edi_format_id.code in formats_to_return)

        attachments = documents_to_return.attachment_id
        data_str_list = []
        for attachment in attachments.with_context(bin_size=False):
            data_str_list.append(base64.decodebytes(attachment.datas))
        return data_str_list
