# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import logging
from base64 import b64decode
from unittest import skipIf

import odoo
import odoo.tests

try:
    from pdfminer.converter import PDFPageAggregator
    from pdfminer.layout import LAParams, LTFigure, LTTextBox
    from pdfminer.pdfdocument import PDFDocument
    from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
    from pdfminer.pdfpage import PDFPage
    from pdfminer.pdfparser import PDFParser
    pdfminer = True
except ImportError:
    pdfminer = False

_logger = logging.getLogger(__name__)


@odoo.tests.tagged('post_install', '-at_install', 'post_install_l10n')
class TestReports(odoo.tests.TransactionCase):
    def test_reports(self):
        invoice_domain = [('move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt', 'in_invoice', 'in_refund', 'in_receipt'))]
        specific_model_domains = {
            'account.report_original_vendor_bill': [('move_type', 'in', ('in_invoice', 'in_receipt'))],
            'account.report_invoice_with_payments': invoice_domain,
            'account.report_invoice': invoice_domain,
            'l10n_th.report_commercial_invoice': invoice_domain,
        }
        Report = self.env['ir.actions.report']
        for report in Report.search([('report_type', 'like', 'qweb')]):
            report_model = 'report.%s' % report.report_name
            try:
                self.env[report_model]
            except KeyError:
                # Only test the generic reports here
                _logger.info("testing report %s", report.report_name)
                report_model_domain = specific_model_domains.get(report.report_name, [])
                report_records = self.env[report.model].search(report_model_domain, limit=10)
                if not report_records:
                    _logger.info("no record found skipping report %s", report.report_name)

                # Test report generation
                if not report.multi:
                    for record in report_records:
                        Report._render_qweb_html(report.id, record.ids)
                else:
                    Report._render_qweb_html(report.id, report_records.ids)
            else:
                continue

    def test_report_reload_from_attachment(self):
        def get_attachments(res_id):
            return self.env["ir.attachment"].search([('res_model', "=", "res.partner"), ("res_id", "=", res_id)])

        Report = self.env['ir.actions.report'].with_context(force_report_rendering=True)

        report = Report.create({
            'name': 'test report',
            'report_name': 'base.test_report',
            'model': 'res.partner',
        })

        self.env['ir.ui.view'].create({
            'type': 'qweb',
            'name': 'base.test_report',
            'key': 'base.test_report',
            'arch': '''
                <main>
                    <div class="article" data-oe-model="res.partner" t-att-data-oe-id="docs.id">
                        <span t-field="docs.display_name" />
                    </div>
                </main>
            '''
        })

        pdf_text = "0"
        def _run_wkhtmltopdf(*args, **kwargs):
            return bytes(pdf_text, "utf-8")

        self.patch(type(Report), "_run_wkhtmltopdf", _run_wkhtmltopdf)

        # sanity check: the report is not set to save attachment
        # assert that there are no pre-existing attachment
        partner_id = self.env.user.partner_id.id
        self.assertFalse(get_attachments(partner_id))
        pdf = report._render_qweb_pdf(report.id, [partner_id])
        self.assertFalse(get_attachments(partner_id))
        self.assertEqual(pdf[0], b"0")

        # set the report to reload from attachment and make one
        pdf_text = "1"
        report.attachment = "'test_attach'"
        report.attachment_use = True
        report._render_qweb_pdf(report.id, [partner_id])
        attach_1 = get_attachments(partner_id)
        self.assertTrue(attach_1.exists())

        # use the context key to not reload from attachment
        # and not create another one
        pdf_text = "2"
        report = report.with_context(report_pdf_no_attachment=True)
        pdf = report._render_qweb_pdf(report.id, [partner_id])
        attach_2 = get_attachments(partner_id)
        self.assertEqual(attach_2.id, attach_1.id)

        self.assertEqual(b64decode(attach_1.datas), b"1")
        self.assertEqual(pdf[0], b"2")


# Some paper format examples
PAPER_SIZES = {
    (842, 1190): 'A3',
    (595, 842): 'A4',
    (420, 595): 'A5',
    (297, 420): 'A6',
    (612, 792): 'Letter',
    (612, 1008): 'Legal',
    (792, 1224): 'Ledger',
}

class Box:
    """
    Utility class to help assertions
    """
    def __init__(self, obj, page_height, page_width):
        self.x1 = round(obj.x0, 1)
        self.y1 = round(page_height-obj.y1, 1)
        self.x2 = round(obj.x1, 1)
        self.y2 = round(page_height-obj.y0, 1)
        self.page_height = page_height
        self.page_width = page_width

    @property
    def height(self):
        return self.y2 - self.y1

    @property
    def width(self):
        return self.x2 - self.x1

    @property
    def top(self):
        return self.y1

    @property
    def left(self):
        return self.x1

    @property
    def end_top(self):
        return self.y2

    @property
    def end_left(self):
        return self.x2

    @property
    def right(self):
        return self.page_width - self.x2

    @property
    def bottom(self):
        return self.page_height - self.y2

    def __lt__(self, other):
        return (self.y1, self.x1, self.y2, self.x2) < (other.y1, other.x1, other.y2, other.x2)


@skipIf(pdfminer is False, "pdfminer not installed")
class TestReportsRenderingCommon(odoo.tests.HttpCase):

    def setUp(self):
        super().setUp()
        self.report = self.env['ir.actions.report'].create({
            'name': 'Test Report Partner',
            'model': 'res.partner',
            'report_name': 'test_report.test_report_partner',
            'paperformat_id': self.env.ref('base.paperformat_euro').id,
        })

        self.partners = self.env['res.partner'].create([{
            'name': f'Report record {i}',
        } for i in range(2)])

        self.report_view = self.env['ir.ui.view'].create({
            'type': 'qweb',
            'name': 'test_report_partner',
            'key': 'test_report.test_report_partner',
            'arch': "<t></t>",
        })
        self.last_pdf_content = None
        self.last_pdf_content_saved = False

    def _addError(self, result, test, exc_info):
        if self.last_pdf_content and not self.last_pdf_content_saved:
            self.last_pdf_content_saved = True
            self.save_pdf()
        super()._addError(result, test, exc_info)

    def get_paper_format(self, mediabox):
        """
            :param: mediabox: a page mediabox. (Example: (0, 0, 595, 842))
            :return: a (format, orientation). Example ('A4', 'portait')
        """
        x, y, width, height = mediabox
        self.assertEqual((x, y), (0, 0), "Expecting top corner to be 0, 0 ")
        orientation = 'portait'
        paper_size = (width, height)
        if width > height:
            orientation = 'landscape'
            paper_size = (height, width)
        return PAPER_SIZES.get(paper_size, f'custom{paper_size}'), orientation

    def create_pdf(self, partners=None, header_content=None, page_content=None, footer_content=None):
        if header_content is None:
            header_content = '''
                <img t-if="company.logo" t-att-src="image_data_uri(company.logo)" style="max-height: 45px;" alt="Logo"/>
                <span>Some header Text</span>
            '''

        if footer_content is None:
            footer_content = '''
                <div style="text-align:center">Footer for <t t-esc="o.name"/> Page: <span class="page"/> / <span class="topage"/></div>
            '''

        if page_content is None:
            page_content = '''
                <div class="page">
                    <div style="background-color:red">
                        Name: <t t-esc="o.name"/>
                    </div>
                </div>
            '''

        self.report_view.arch = f'''
                <t t-name="test_report.test_report_partner">
                    <t t-set="company" t-value="res_company"/>
                    <t t-call="web.html_container">
                        <t t-foreach="docs" t-as="o">
                            <div class="header" style="font-family:Sans">
                                {header_content}
                            </div>
                            <div class="article" style="font-family:Sans">

                                {page_content}
                            </div>
                            <div class="footer" style="font-family:Sans">
                                {footer_content}
                            </div>
                        </t>
                    </t>
                </t>
            '''
        # this templates doesn't use the "web.external_layout" in order to simplify the final result and make the edition of footer and header easier
        # this test does not aims to test company base.document.layout, but the rendering only.
        if partners is None:
            partners = self.partners
        self.last_pdf_content = self.env['ir.actions.report'].with_context(force_report_rendering=True)._render_qweb_pdf(self.report, partners.ids)[0]
        return self.last_pdf_content

    def save_pdf(self):
        assert self.last_pdf_content
        odoo.tests.save_test_file(self._testMethodName, self.last_pdf_content, 'pdf_', 'pdf', document_type='Report PDF', logger=_logger)

    def _get_pdf_pages(self, pdf_content):
        ioBytes = io.BytesIO(pdf_content)
        parser = PDFParser(ioBytes)
        doc = PDFDocument(parser)
        return list(PDFPage.create_pages(doc))

    def _parse_pdf(self, pdf_content, expected_format=('A4', 'portait')):
        """
            :param: pdf_content: the bdf binary content
            :param: expected_format: a get_paper_format like format.
            :return: list[list[(box, Element)]] a list of element per page
            Note: box is a 4 float tuple based on the top left corner to ease ordering of elements.
            The result is also rounded to one digit
        """
        pages = self._get_pdf_pages(pdf_content)
        ressource_manager = PDFResourceManager()
        device = PDFPageAggregator(ressource_manager, laparams=LAParams())
        interpreter = PDFPageInterpreter(ressource_manager, device)

        parsed_pages = []
        for page in pages:
            self.assertEqual(
                self.get_paper_format(page.mediabox),
                expected_format,
                "Expecting pdf to be in A4 portait format",
            ) # this is the default expected format and other layout assertions are based on this one.
            interpreter.process_page(page)
            layout = device.get_result()
            elements = []
            parsed_pages.append(elements)
            for obj in layout:
                box = Box(
                    obj,
                    page_height=pages[0].mediabox[3],
                    page_width=pages[0].mediabox[2],
                )
                if isinstance(obj, LTTextBox):
                    #inverse x to start from top left corner
                    elements.append((box, obj.get_text().strip()))
                elif isinstance(obj, LTFigure):
                    elements.append((box, 'LTFigure'))
            elements.sort()

        return parsed_pages

    def assertPageFormat(self, paper_format, orientation):
        pdf_content = self.create_pdf()
        pages = self._get_pdf_pages(pdf_content)
        self.assertEqual(len(pages), 2)
        for page in pages:
            self.assertEqual(
                self.get_paper_format(page.mediabox),
                (paper_format, orientation),
                f"Expecting pdf to be in {paper_format} {orientation} format",
            )


@odoo.tests.tagged('post_install', '-at_install', 'pdf_rendering')
class TestReportsRendering(TestReportsRenderingCommon):
    """
        This test aims to test as much as possible the current pdf rendering,
        especially multipage headers and footers
        (the main reason why we are currently using wkhtmltopdf with patched qt)
        A custom template without web.external_layout is used on purpose in order to
        easily test headers and footer regarding rendering only,
        without using any comany document.layout logic
    """

    def test_format_A4(self):
        self.report.paperformat_id = self.env.ref('base.paperformat_euro')
        self.assertPageFormat('A4', 'portait')

    def test_format_letter(self):
        self.report.paperformat_id = self.env.ref('base.paperformat_us')
        self.assertPageFormat('Letter', 'portait')

    def test_format_landscape(self):
        paper_format = self.env.ref('base.paperformat_euro')
        paper_format.orientation = 'Landscape'
        self.report.paperformat_id = paper_format
        self.assertPageFormat('A4', 'landscape')

    def test_layout(self):
        pdf_content = self.create_pdf()
        pages = self._parse_pdf(pdf_content)
        self.assertEqual(len(pages), 2)

        page_contents = [[elem[1] for elem in page] for page in pages]

        expected_pages_content = [[
            'LTFigure',
            'Some header Text',
            f'Name: {partner.name}',
            f'Footer for {partner.name} Page: 1 / 1',
        ] for partner in self.partners]

        self.assertEqual(
            page_contents,
            expected_pages_content,
        )

        page_positions = [[elem[0] for elem in page] for page in pages]
        logo, header, content, footer = page_positions[0]

        # leaving this as reference but this is to fragile to make a strict assertion
        # 14.3, 29.6, 43.1, 137.2     # logo
        # 19.1, 137.2, 32.5, 214.2   # header
        # 111.3, 29.6, 124.8, 123.7   # content
        # 751.6, 220.1, 765.1, 375.0  # footer

        #
        #   \ \ / // _ \ | | | || _ \  | |
        #    \ V /| (_) || |_| ||   /  | |__ / _ \/ _` |/ _ \     Some header Text
        #     |_|  \___/  \___/ |_|_\  |____|\___/\__, |\___/
        #
        #
        #   Name: Report record 0
        #
        #
        #
        #
        #
        #
        #             Footer for Report record 0 Page: 1 / 1
        #
        #

        self.assertEqual(logo.left, content.left, 'Logo and content should have the same left margin')
        self.assertEqual(header.left, logo.end_left, 'Header starts after logo')
        self.assertGreaterEqual(header.top, logo.top, 'header is vertically centered on logo')
        self.assertGreaterEqual(logo.end_top, header.end_top, 'header is vertically centered on logo')
        self.assertGreaterEqual(content.top, logo.end_top, 'Content is bellow logo')
        self.assertGreaterEqual(footer.top, content.end_top, 'Footer is bellow content')
        self.assertGreaterEqual(100, footer.bottom, 'Footer is on the bottom of the page')
        self.assertAlmostEqual(footer.left, footer.right, -1, 'Footer is centered on the page')

    def test_report_pdf_page_break(self):

        partners = self.partners[:2]
        page_content = '''
                <div class="page">
                    <div style="background-color:red">
                        Name: <t t-esc="o.name"/>
                    </div>
                    <div style="page-break-before:always;background-color:blue">
                        Last page for <t t-esc="o.name"/>
                    </div>
                </div>
            '''

        pdf_content = self.create_pdf(partners=partners, page_content=page_content)

        pages = self._parse_pdf(pdf_content)

        self.assertEqual(len(pages), 4, "Expecting 2 pages * 2 partners")

        expected_pages_contents = []
        for partner in self.partners:
            expected_pages_contents.append([
                'LTFigure', #logo
                'Some header Text',
                f'Name: {partner.name}',
                f'Footer for {partner.name} Page: 1 / 2',
            ])
            expected_pages_contents.append([
                'LTFigure', #logo
                'Some header Text',
                f'Last page for {partner.name}',
                f'Footer for {partner.name} Page: 2 / 2',
            ])
        pages_contents = [[elem[1] for elem in page] for page in pages]
        self.assertEqual(pages_contents, expected_pages_contents)

    def test_pdf_render_page_overflow(self):
        nb_lines = 80

        page_content = f'''
            <div class="page">
                <div style="background-color:red">
                    Name: <t t-esc="o.name"/>
                    <div t-foreach="range({nb_lines})" t-as="pos" t-esc="pos"/>
                </div>
            </div>
        '''
        pdf_content = self.create_pdf(page_content=page_content)
        pages = self._parse_pdf(pdf_content)

        self.assertEqual(len(pages), 4, '4 pages are expected, 2 per record (you may ensure `nb_lines` has a correct value to generate an oveflow)')
        page_break_at = int(pages[1][2][1].split('\n')[0])  # This element should be the first line, 61 when this test was written

        expected_pages_contents = []
        for partner in self.partners:
            expected_pages_contents.append([
                'LTFigure', #logo
                'Some header Text',
                f'Name: {partner.name}\n' + '\n'.join([str(i) for i in range(page_break_at)]),
                f'Footer for {partner.name} Page: 1 / 2',
            ])
            expected_pages_contents.append([
                'LTFigure', #logo
                'Some header Text',
                '\n'.join([str(i) for i in range(page_break_at, nb_lines)]),
                f'Footer for {partner.name} Page: 2 / 2',
            ])
        pages_contents = [[elem[1] for elem in page] for page in pages]
        self.assertEqual(pages_contents, expected_pages_contents)

    def test_thead_tbody_repeat(self):
        """
            Check that thead and t-foot are repeated after page break inside a tbody
        """
        nb_lines = 50
        page_content = f'''
            <div class="page">
                <table class="table">
                    <thead><tr><th> T1 </th><th> T2 </th><th> T3 </th></tr></thead>
                    <tbody>
                    <t t-foreach="range({nb_lines})" t-as="pos">
                        <tr><td><t t-esc="pos"/></td><td><t t-esc="pos"/></td><td><t t-esc="pos"/></td></tr>
                    </t>
                    </tbody>
                    <tfoot><tr><th> T1 </th><th> T2 </th><th> T3 </th></tr></tfoot>
                </table>
            </div>
        '''

        pdf_content = self.create_pdf(page_content=page_content)
        pages = self._parse_pdf(pdf_content)

        self.assertEqual(len(pages), 4, '4 pages are expected, 2 per record (you may ensure `nb_lines` has a correct value to generate an oveflow)')
        page_break_at = int(pages[1][5][1])  # This element should be the first line of the table, 28 when this test was written

        def expected_table(start, end):
            table = ['T1', 'T2', 'T3'] # thead
            for i in range(start, end):
                table += [str(i), str(i), str(i)]
            table += ['T1', 'T2', 'T3'] # tfoot
            return table

        expected_pages_contents = []
        for partner in self.partners:
            expected_pages_contents.append([
                'LTFigure', #logo
                'Some header Text',
                * expected_table(0, page_break_at),
                f'Footer for {partner.name} Page: 1 / 2',
            ])
            expected_pages_contents.append([
                'LTFigure', #logo
                'Some header Text',
                * expected_table(page_break_at, nb_lines),
                f'Footer for {partner.name} Page: 2 / 2',
            ])

        pages_contents = [[elem[1] for elem in page] for page in pages]
        self.assertEqual(pages_contents, expected_pages_contents)


@odoo.tests.tagged('post_install', '-at_install', '-standard', 'pdf_rendering')
class TestReportsRenderingLimitations(TestReportsRenderingCommon):
    def test_no_clip(self):
        """
            Current version will add a fixed margin on top of document
            This test demonstrates this limitation
        """
        header_content = '''
            <div style="background-color:blue">
                <div t-foreach="range(15)" t-as="pos" t-esc="'Header %s' % pos"/>
            </div>
        '''
        page_content = '''
            <div class="page">
                <div style="background-color:red; margin-left:100px">
                    <div t-foreach="range(10)" t-as="pos" t-esc="'Content %s' % pos"/>
                </div>
            </div>
        '''
        # adding a margin on page to avoid bot block to me considered as the same
        pdf_content = self.create_pdf(page_content=page_content, header_content=header_content)
        pages = self._parse_pdf(pdf_content)
        self.assertEqual(len(pages), 2, "2 partners")
        page = pages[0]
        self.assertEqual(len(page), 3, "Expecting 3 box per page, Header, body, footer")
        header = page[0][0]
        content = page[1][0]
        self.assertGreaterEqual(content.top, header.end_top, "EXISTING LIMITATION: large header shouldn't overflow on body, but they do")
