import re
from base64 import b64encode
from dateutil.relativedelta import relativedelta
from functools import wraps
from io import BytesIO
from unittest.mock import MagicMock, call, patch

from odoo import fields
from odoo.exceptions import UserError
from odoo.tests import freeze_time, tagged
from odoo.tools import file_open

from odoo.addons.l10n_tr_nilvera_einvoice.tests.test_xml_ubl_tr_common import TestUBLTRCommon

COMPANY_VAT = '3297552117'
ERRORENOUS_ALIAS = 'erroneous_alias'
EINVOICE_PARTNER_VAT = '1729171602'
EARCHIVE_PARTNER_VAT = '17291716060'
SERVER_ERROR_ALIAS = 'server_error_alias'
UNAUTHORIZED_ALIAS = 'unauthorized_alias'
UUID_INVALID_STATUS = 'uuid_with_invalid_status_code'
UUID_INVALID_INVOICE = 'uuid_invalid_invoice'
UUID_VALID_INVOICE = 'uuid_valid_invoice'


def mock_requests_request(method, url, *args, **kwargs):
    response = MagicMock()
    response.status_code = 200

    if method == 'GET' and 'Check/TaxNumber' in url:
        if EINVOICE_PARTNER_VAT in url:
            response.json.return_value = [
                {
                    'DocumentType': 'Invoice',
                    'Name': 'urn:mail:salt@bae.com',
                    'TaxNumber': EINVOICE_PARTNER_VAT,
                    'Title': 'Salt Bae LLC',
                    'Type': 'OZEL',
                },
            ]
        elif COMPANY_VAT in url:
            response.json.return_value = [
                {
                    'TaxNumber': 'text',
                    'Title': 'text',
                    'FirstCreatedTime': '2025-06-23',
                    'CreationTime': '2025-06-23',
                    'DocumentType': 'text',
                    'Name': 'text',
                    'Type': 'text',
                },
            ]
        elif EARCHIVE_PARTNER_VAT in url:
            response.json.return_value = []

    elif method == 'GET' and (match := re.fullmatch(r'/einvoice/sale/([\w-]+)/Status', url)):
        if match.group(1) == UUID_INVALID_STATUS:
            data = {
                "InvoiceStatus": {
                    "Code": "boop",
                    "Description": "text",
                    "DetailDescription": "text",
                },
            }
            response.get.side_effect = data.get
        else:
            data = {
                "InvoiceStatus": {
                    "Code": "succeed",
                    "Description": "text",
                    "DetailDescription": "text",
                },
            }
            response.get.side_effect = data.get

    elif method == 'POST' and 'Send/Xml' in url:
        if UNAUTHORIZED_ALIAS in url:
            response.status_code = 401
            response.text = 'Unauthorized'
        elif SERVER_ERROR_ALIAS in url:
            response.status_code = 500
            response.text = 'Internal Server Error'
        elif ERRORENOUS_ALIAS in url:
            response.status_code = 422
            response.json.return_value = {
                "Message": "HATALI ISTEK",
                "Errors": [{
                    "Code": 2000,
                    "Description": "Yeterli Kontörünüz Bulunmamaktadır.",
                    "Detail": "Yeterli Kontörünüz Bulunmamaktadır. Lütfen Kontör Alımı Yapınız.",
                }],
            }
        else:
            response.json.return_value = {
                "UUID": "00aac88a-576b-4a62-98b5-ed34fe4d187d",
                "InvoiceNumber": "",
            }

    elif method == 'GET' and re.fullmatch(r'/(einvoice|earchive)/(sale|invoices)/[\w-]+/pdf', url):
        # Outbound PDF retrieval: e-invoice sales use "sale", e-archive uses "invoices".
        with file_open('l10n_tr_nilvera_einvoice/tests/test_files/fetching/invoice.pdf', 'rb') as pdf:
            response = b64encode(pdf.read()).decode()

    elif method == 'GET' and '/einvoice/Purchase' in url:
        if '/xml' in url:
            with file_open('l10n_tr_nilvera_einvoice/tests/test_files/fetching/invoice.xml', 'rb') as xml:
                response = xml.read()
        elif '/pdf' in url:
            with file_open('l10n_tr_nilvera_einvoice/tests/test_files/fetching/invoice.pdf', 'rb') as pdf:
                # Nilvera's /pdf endpoint returns the PDF base64-encoded inside a JSON
                # string body. NilveraClient.request() calls response.json() by default,
                # so the caller receives a base64 str, not raw bytes.
                response = b64encode(pdf.read()).decode()
        else:
            data = {
                    'TotalPages': 1,
                    'Content': [
                        {
                            'UUID': 'invoice_uuid',
                            'CreatedDate': '2025-03-05',
                        },
                    ],
            }
            response.get.side_effect = data.get
    return response


def patch_nilvera_request(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        with patch(
            'odoo.addons.l10n_tr_nilvera.lib.nilvera_client.NilveraClient.request',
            side_effect=mock_requests_request,
        ) as mocked_request:
            # If the test expects the mock as an argument, pass it
            if 'mocked_request' in function.__code__.co_varnames:
                return function(*args, mocked_request, **kwargs)
            return function(*args, **kwargs)
    return wrapper


@tagged('post_install_l10n', 'post_install', '-at_install')
class TestTRNilveraMockedRequests(TestUBLTRCommon):

    @classmethod
    @patch_nilvera_request
    def setUpClass(cls):
        super().setUpClass()
        with patch.object(cls.env.cr, 'commit', autospec=True):
            cls.einvoice_partner._check_nilvera_customer()
            cls.earchive_partner._check_nilvera_customer()
        cls.env['account.journal'].create({
            'name': 'TR Journal',
            'code': 'TRJ',
            'type': 'purchase',
            'company_id': cls.company.id,
        })

    def test_amount_in_words_rounds_subunit(self):
        note = self.env['account.edi.xml.ubl.tr']._l10n_tr_get_amount_integer_partn_text_note(
            3989.33, self.env.ref('base.TRY'),
        )
        self.assertEqual(note, 'YALNIZ : ÜÇBINDOKUZYÜZSEKSENDOKUZ TRY OTUZÜÇ KURUS')

    @patch_nilvera_request
    def test_which_service_to_call(self):
        _, invoice = self._generate_invoice_xml(self.einvoice_partner, include_invoice=True)

        invoices_data = {
            invoice: {
                **self.env['account.move.send']._get_default_sending_settings(invoice),
            }
        }

        with patch('odoo.addons.l10n_tr_nilvera_einvoice.models.account_move.AccountMove._l10n_tr_nilvera_submit_einvoice') as mock_submit_einvoice, \
             patch('odoo.addons.l10n_tr_nilvera_einvoice.models.account_move.AccountMove._l10n_tr_nilvera_submit_earchive') as mock_submit_earchive:
            self.env['account.move.send']._call_web_service_before_invoice_pdf_render(invoices_data)
            mock_submit_einvoice.assert_called_once()
            mock_submit_earchive.assert_not_called()

            # Reset the alias to empty for the next test
            invoice.partner_id.l10n_tr_nilvera_customer_alias_id = self.env['l10n_tr.nilvera.alias']
            invoice.partner_id.vat = False
            mock_submit_einvoice.reset_mock()
            mock_submit_earchive.reset_mock()

            self.env['account.move.send']._call_web_service_before_invoice_pdf_render(invoices_data)
            mock_submit_earchive.assert_called_once()
            mock_submit_einvoice.assert_not_called()

    @patch_nilvera_request
    def test_submit_einvoice(self, mocked_request):
        xml, invoice = self._generate_invoice_xml(self.einvoice_partner, include_invoice=True)
        wrapped_xml = BytesIO(xml)
        wrapped_xml.name = 'invoice.xml'
        invoice._l10n_tr_nilvera_submit_einvoice(wrapped_xml, self.einvoice_partner.l10n_tr_nilvera_customer_alias_id.name)

        mocked_request.assert_called_once_with(
            'POST',
            '/einvoice/Send/Xml?Alias=urn%3Amail%3Asalt%40bae.com',
            files={'file': ('invoice.xml', wrapped_xml, 'application/xml')},
            handle_response=False,
        )
        self.assertTrue(invoice.is_move_sent)
        self.assertEqual(invoice.l10n_tr_nilvera_send_status, 'sent')

    @patch_nilvera_request
    def test_submit_earchive(self, mocked_request):
        xml, invoice = self._generate_invoice_xml(self.earchive_partner, include_invoice=True)
        wrapped_xml = BytesIO(xml)
        wrapped_xml.name = 'invoice.xml'
        invoice._l10n_tr_nilvera_submit_earchive(wrapped_xml)

        mocked_request.assert_called_once_with(
            'POST',
            '/earchive/Send/Xml',
            files={'file': ('invoice.xml', wrapped_xml, 'application/xml')},
            handle_response=False,
        )

        self.assertTrue(invoice.is_move_sent)
        self.assertEqual(invoice.l10n_tr_nilvera_send_status, 'sent')

    @patch_nilvera_request
    def test_submit_einvoice_errors(self):
        xml, invoice = self._generate_invoice_xml(self.einvoice_partner, include_invoice=True)
        wrapped_xml = BytesIO(xml)
        wrapped_xml.name = 'invoice.xml'
        error_cases = [
            (UNAUTHORIZED_ALIAS, "Oops, seems like you're unauthorised to do this. Try another API key with more rights or contact Nilvera."),
            (SERVER_ERROR_ALIAS, "Server error from Nilvera, please try again later."),
            (ERRORENOUS_ALIAS, "The invoice couldn't be sent due to the following errors:\n\n2000 - You do not have sufficient credits:\nYeterli Kontörünüz Bulunmamaktadır. Lütfen Kontör Alımı Yapınız.\n"),
        ]

        for alias, expected_error in error_cases:
            with self.assertRaises(UserError) as context:
                invoice._l10n_tr_nilvera_submit_einvoice(wrapped_xml, alias)
            self.assertEqual(str(context.exception), expected_error)

    @patch_nilvera_request
    def test_fetch_status(self):
        _, invoice = self._generate_invoice_xml(self.einvoice_partner, include_invoice=True)
        invoice._l10n_tr_nilvera_get_submitted_document_status()

        self.assertEqual(invoice.l10n_tr_nilvera_send_status, 'succeed')

    @patch_nilvera_request
    def test_get_pdf_earchive(self, mocked_request):
        # E-archive PDF retrieval must use the "invoices" resource, not the e-invoice "sale" one.
        _, invoice = self._generate_invoice_xml(self.earchive_partner, include_invoice=True)
        invoice.l10n_tr_nilvera_send_status = 'succeed'

        invoice.l10n_tr_nilvera_get_pdf()

        mocked_request.assert_any_call(
            'GET',
            f'/earchive/invoices/{invoice.l10n_tr_nilvera_uuid}/pdf',
        )
        self.assertTrue(invoice.message_main_attachment_id.raw.startswith(b'%PDF-'))

    @patch_nilvera_request
    def test_fetch_invalid_status(self):
        _, invoice = self._generate_invoice_xml(self.einvoice_partner, include_invoice=True)
        invoice.l10n_tr_nilvera_uuid = UUID_INVALID_STATUS

        invoice._l10n_tr_nilvera_get_submitted_document_status()

        self.assertIn(
            invoice.message_ids[0].preview,
            "The invoice status couldn't be retrieved from Nilvera.",
        )

    @freeze_time('2025-03-05')
    @patch_nilvera_request
    def test_fetching_einvoices(self, mocked_request):
        # EndDate is adjusted to match Europe/Istanbul timezone(UTC+3)
        with patch.object(self.env.cr, 'commit', autospec=True):
            self.env
            self.env['account.move']._l10n_tr_nilvera_get_documents()
            self.env['account.move']._l10n_tr_nilvera_get_documents()

            self.assertListEqual(mocked_request.call_args_list, [
                call(
                    'GET',
                    '/einvoice/Purchase',
                    params={
                        'StatusCode': ['succeed'],
                        'StartDate': fields.Datetime.now() - relativedelta(months=1),
                        'EndDate': '2025-03-05T03:00:00',
                        'DateFilterType': 'CreatedDate',
                        'SortColumn': 'CreationDateTime',
                        'SortType': 'ASC',
                        'Page': 1,
                    },
                ),
                call('GET', '/einvoice/Purchase/invoice_uuid/xml', params={'StatusCode': ['succeed']}),
                call('GET', '/einvoice/Purchase/invoice_uuid/pdf'),
                call(
                    'GET',
                    '/einvoice/Purchase',
                    params={
                        'StatusCode': ['succeed'],
                        'StartDate': fields.Datetime.now(),
                        'EndDate': '2025-03-05T03:00:00',
                        'DateFilterType': 'CreatedDate',
                        'SortColumn': 'CreationDateTime',
                        'SortType': 'ASC',
                        'Page': 1,
                    },
                ),
            ])

            invoice = self.env['account.move'].search([('l10n_tr_nilvera_uuid', '=', 'invoice_uuid')])
            self.assertEqual(len(invoice), 1)
            self.assertFalse(invoice.attachment_ids)
            self.assertTrue(invoice.ubl_cii_xml_id)  # XML file used at import
            self.assertTrue(invoice.l10n_tr_nilvera_pdf_id)
            self.assertTrue(
                invoice.l10n_tr_nilvera_pdf_id.raw.startswith(b'%PDF-'),
                "PDF attachment must contain decoded PDF bytes, not base64 text",
            )
