from .common import TestAccountReportsCommon

from odoo import fields, Command
from odoo.tests import tagged
from odoo.tools import frozendict

from unittest.mock import patch


@tagged('post_install', '-at_install')
class TestReportEngines(TestAccountReportsCommon):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.company_data['company'].totals_below_sections = False

        cls.garbage_account = cls.env['account.account'].create({
            'code': "turlututu",
            'name': "turlututu",
            'account_type': "asset_current",
        })

        cls.fake_country = cls.env['res.country'].create({
            'name': "L'Île de la Mouche",
            'code': 'YY',
        })

    # -------------------------------------------------------------------------
    # HELPERS
    # -------------------------------------------------------------------------

    def _prepare_test_account_move_line(self, balance, account_code=None, tax_tags=None, date='2020-01-01', **kwargs):
        if tax_tags:
            tags = self.env['account.account.tag'].search([
                ('applicability', '=', 'taxes'),
                ('country_id', '=', self.fake_country.id),
                ('name', 'in', tax_tags),
            ])
        else:
            tags = self.env['account.account.tag']

        return {
            'account_move_line_values': {
                'name': "turlututu",
                'account_id': self.garbage_account.id,
                **kwargs,
                'debit': balance if balance > 0.0 else 0.0,
                'credit': -balance if balance < 0.0 else 0.0,
                'tax_tag_ids': [Command.set(tags.ids)],
            },
            'account_move_values': {'date': date},
            'account_code': account_code,
        }

    def _create_test_account_moves(self, test_account_move_line_values_list):
        # Create the missing account on-the-fly.
        accounts_to_create_by_code = set()
        for test_account_move_line_values in test_account_move_line_values_list:
            if test_account_move_line_values.get('account_code'):
                accounts_to_create_by_code.add(test_account_move_line_values['account_code'])

        if accounts_to_create_by_code:
            accounts = self.env['account.account'].create([
                {
                    'code': account_code,
                    'name': account_code,
                    'account_type': "asset_current",
                }
                for account_code in accounts_to_create_by_code
            ])
            account_by_code = {x.code: x for x in accounts}

            for test_account_move_line_values in test_account_move_line_values_list:
                account = account_by_code.get(test_account_move_line_values.get('account_code'))
                if account:
                    test_account_move_line_values['account_move_line_values']['account_id'] = account.id

        # Create the journal entries.
        to_create = {}
        for test_account_move_line_values in test_account_move_line_values_list:
            account_move_key = frozendict(test_account_move_line_values['account_move_values'])
            account_move_line_values = test_account_move_line_values['account_move_line_values']
            account_move_to_create = to_create.setdefault(account_move_key, {
                'account_move_values': {'line_ids': []},
                'balance': 0.0,
            })
            account_move_to_create['account_move_values']['line_ids'].append(Command.create(account_move_line_values))
            account_move_to_create['balance'] += account_move_line_values['debit'] - account_move_line_values['credit']

        account_move_create_list = []
        for account_move_dict, account_move_to_create in to_create.items():
            open_balance = account_move_to_create['balance']
            account_move_values = account_move_to_create['account_move_values']
            if not self.env.company.currency_id.is_zero(open_balance):
                account_move_values['line_ids'].append(Command.create({
                    'name': 'open balance',
                    'account_id': self.garbage_account.id,
                    'debit': -open_balance if open_balance < 0.0 else 0.0,
                    'credit': open_balance if open_balance > 0.0 else 0.0,
                }))
            account_move_create_list.append({
                **account_move_dict,
                **account_move_values,
            })

        moves = self.env['account.move'].create(account_move_create_list)
        moves.action_post()
        return moves

    def _prepare_test_external_values(self, value, date, figure_type=False):
        field_name = 'text_value' if figure_type == 'string' else 'value'
        return {
            'name': date,
            field_name: value,
            'date': date,
        }

    def _prepare_test_expression(self, formula, label='balance', **kwargs):
        return {
            'expression_values': {
                'label': label,
                'formula': formula,
                **kwargs,
            },
        }

    def _prepare_test_expression_tax_tags(self, formula, **kwargs):
        return self._prepare_test_expression(engine='tax_tags', formula=formula, **kwargs)

    def _prepare_test_expression_domain(self, formula, subformula, **kwargs):
        return self._prepare_test_expression(engine='domain', formula=formula, subformula=subformula, **kwargs)

    def _prepare_test_expression_account_codes(self, formula, **kwargs):
        return self._prepare_test_expression(engine='account_codes', formula=formula, **kwargs)

    def _prepare_test_expression_external(self, formula, external_value_generators, **kwargs):
        return {
            **self._prepare_test_expression(engine='external', formula=formula, **kwargs),
            'external_value_generators': external_value_generators,
        }

    def _prepare_test_expression_custom(self, formula, **kwargs):
        return self._prepare_test_expression(engine='custom', formula=formula, **kwargs)

    def _prepare_test_expression_aggregation(self, formula, subformula=None, column='balance', date_scope=None):
        expression_values = {
            'label': column,
            'engine': 'aggregation',
            'formula': formula,
            'subformula': subformula,
        }

        if date_scope:
            expression_values['date_scope'] = date_scope

        return {
            'expression_values': expression_values,
        }

    def _prepare_test_report_line(self, *expression_generators, **kwargs):
        return {
            'report_line_values': {
                **kwargs,
                'expression_ids': [
                    Command.create({
                        'date_scope': 'strict_range',
                        **expression_values['expression_values'],
                    })
                    for expression_values in expression_generators
                ],
            },
            'expression_generators': expression_generators,
        }

    def _create_report(self, test_report_line_values_list, columns=None, **kwargs):
        if not columns:
            columns = ['balance']

        # Create a new report
        report = self.env['account.report'].create({
            'name': "_run_report",
            'filter_date_range': True,
            'filter_unfold_all': True,
            **kwargs,
            'column_ids': [
                Command.create({
                    'name': column,
                    'expression_label': column,
                    'sequence': i,
                })
                for i, column in enumerate(columns)
            ],
            'line_ids': [
                Command.create({
                    'name': f"test_line_{i}",
                    **test_report_line_values['report_line_values'],
                    'sequence': i,
                })
                for i, test_report_line_values in enumerate(test_report_line_values_list, start=1)
            ],
        })

        # Create the external values
        external_values_create_list = []
        for report_line, test_report_line_values in zip(report.line_ids, test_report_line_values_list):
            for expression, expression_values in zip(report_line.expression_ids, test_report_line_values['expression_generators']):
                for external_values in expression_values.get('external_value_generators', []):
                    external_values_create_list.append({
                        **external_values,
                        'target_report_expression_id': expression.id,
                    })
        self.env['account.report.external.value'].create(external_values_create_list)

        return report

    # -------------------------------------------------------------------------
    # TESTS
    # -------------------------------------------------------------------------

    def test_engine_tax_tags(self):
        self.env.company.account_fiscal_country_id = self.fake_country

        # Create the report.
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_tax_tags('11'),
            groupby='account_id,move_type',
        )
        test_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_tax_tags('222T'),
            groupby='parent_state,account_id',
        )
        test_line_3 = self._prepare_test_report_line(
            self._prepare_test_expression_tax_tags('3333'),
            groupby='account_id',
        )
        report = self._create_report(
            [test_line_1, test_line_2, test_line_3],
            country_id=self.fake_country.id,
        )

        # Create the journal entries.
        move = self._create_test_account_moves([
            self._prepare_test_account_move_line(2000.0, account_code='101001', tax_tags=['+11', '-222T']),
            self._prepare_test_account_move_line(1000.0, account_code='101001', tax_tags=['+11', '-222T']),
            self._prepare_test_account_move_line(3600.0, account_code='101001', tax_tags=['+222T']),
            self._prepare_test_account_move_line(-600.0, account_code='101001', tax_tags=['+222T', '-3333']),
            self._prepare_test_account_move_line(-900.0, account_code='101002', tax_tags=['-11']),
            self._prepare_test_account_move_line(1500.0, account_code='101002', tax_tags=['+11']),
        ])

        options = self._generate_options(report, '2020-01-01', '2020-01-01', default_options={'unfold_all': True})
        report_lines = report._get_lines(options)
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [   0,                          1],
            [
                ('test_line_1',        5400.0),
                ('101001 101001',      3000.0),
                ('Journal Entry',      3000.0),
                ('101002 101002',      2400.0),
                ('Journal Entry',      2400.0),
                ('test_line_2',           0.0),
                ('Posted',                0.0),
                ('101001 101001',         0.0),
                ('test_line_3',         600.0),
                ('101001 101001',       600.0),
            ],
            options,
        )

        # Check redirection.
        expected_redirection_values_list = [
            move.line_ids[:2] + move.line_ids[4:6],
            move.line_ids[:4],
            move.line_ids[3],
        ]
        for report_line, expected_amls in zip(report.line_ids, expected_redirection_values_list):
            report_line_dict = [x for x in report_lines if x['name'] == report_line.name][0]
            with self.subTest(report_line=report_line.name):
                action_dict = report.action_audit_cell(options, self._get_audit_params_from_report_line(options, report_line, report_line_dict))
                self.assertEqual(move.line_ids.filtered_domain(action_dict['domain']), expected_amls)

    def test_engine_domain(self):
        domain = [('account_id.code', '=like', '1%'), ('balance', '<', 0.0)]

        # Create the report.
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(domain, 'sum'),
            groupby='account_id,move_type',
        )
        test_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(domain, '-sum'),
            groupby='parent_state,account_id',
        )
        test_line_3 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(domain, 'sum_if_neg'),
            groupby='account_id',
        )
        test_line_4 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(domain, '-sum_if_neg'),
            groupby='account_id',
        )
        test_line_5 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(domain, 'sum_if_pos'),
            groupby='account_id',
        )
        test_line_6 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(domain, '-sum_if_pos'),
            groupby='account_id',
        )
        test_line_7 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(domain, 'count_rows'),
            groupby='account_id',
        )
        report = self._create_report([test_line_1, test_line_2, test_line_3, test_line_4, test_line_5, test_line_6, test_line_7])

        # Create the journal entries.
        move = self._create_test_account_moves([
            self._prepare_test_account_move_line(2000.0, account_code='101001'),
            self._prepare_test_account_move_line(-300.0, account_code='101002'),
            self._prepare_test_account_move_line(-600.0, account_code='101003'),
            self._prepare_test_account_move_line(-900.0, account_code='101004'),
        ])

        # Check the values.
        options = self._generate_options(report, '2020-01-01', '2020-01-01', default_options={'unfold_all': True})
        report_lines = report._get_lines(options)
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [   0,                          1],
            [
                ('test_line_1',       -1800.0),
                ('101002 101002',      -300.0),
                ('Journal Entry',      -300.0),
                ('101003 101003',      -600.0),
                ('Journal Entry',      -600.0),
                ('101004 101004',      -900.0),
                ('Journal Entry',      -900.0),
                ('test_line_2',        1800.0),
                ('Posted',             1800.0),
                ('101002 101002',       300.0),
                ('101003 101003',       600.0),
                ('101004 101004',       900.0),
                ('test_line_3',       -1800.0),
                ('101002 101002',      -300.0),
                ('101003 101003',      -600.0),
                ('101004 101004',      -900.0),
                ('test_line_4',        1800.0),
                ('101002 101002',       300.0),
                ('101003 101003',       600.0),
                ('101004 101004',       900.0),
                ('test_line_5',           0.0),
                ('test_line_6',           0.0),
                ('test_line_7',             3),
                ('101002 101002',           1),
                ('101003 101003',           1),
                ('101004 101004',           1),
            ],
            options,
        )

        # Check redirection.
        expected_amls = move.line_ids.search(domain)
        for report_line in report.line_ids:
            report_line_dict = [x for x in report_lines if x['name'] == report_line.name][0]
            with self.subTest(report_line=report_line.name):
                action_dict = report.action_audit_cell(options, self._get_audit_params_from_report_line(options, report_line, report_line_dict))
                self.assertEqual(move.line_ids.filtered_domain(action_dict['domain']), expected_amls)

    def test_engine_account_codes(self):
        # Create test account tags
        account_tags = self.env['account.account.tag']._load_records([
            {
                'xml_id': 'odex30_account_reports.account_codes_engine_test_tag1',
                'noupdate': True,
                'values': {
                    'name': "account_codes test tag 1",
                    'applicability': 'accounts',
                },
            },

            {
                'xml_id': 'odex30_account_reports.account_codes_engine_test_tag2',
                'noupdate': True,
                'values': {
                    'name': "account_codes test tag 2",
                    'applicability': 'accounts',
                },
            },
        ])

        # Create the report.
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('1'),
            groupby='account_id',
        )
        test_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('1C'),
            groupby='parent_state,account_id',
        )
        test_line_3 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('1D'),
            groupby='account_id',
        )
        test_line_4 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'-101\(101003)'),
            groupby='account_id',
        )
        test_line_5 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'-101\(101003)C'),
            groupby='account_id',
        )
        test_line_6 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'-101\(101002,101003)'),
            groupby='account_id,move_type',
        )
        test_line_7 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('10.'),
            groupby='account_id',
        )
        test_line_8 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('10.20'),
            groupby='account_id',
        )
        test_line_9 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('10.20 - 101 + 101002'),
            groupby='account_id',
        )
        test_line_10 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'10.20 - 101\(101002)'),
            groupby='account_id',
        )
        test_line_11 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'345D\()D'),
            groupby='account_id',
        )
        test_line_12 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'345D\()C'),
            groupby='account_id',
        )
        test_line_13 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(rf'tag(odex30_account_reports.account_codes_engine_test_tag1) + tag({account_tags[1].id})'),
            groupby='account_id',
        )
        test_line_14 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'tag(odex30_account_reports.account_codes_engine_test_tag1)D'),
            groupby='account_id',
        )
        test_line_15 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(r'tag(odex30_account_reports.account_codes_engine_test_tag1)C'),
            groupby='account_id',
        )
        test_line_16 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes(rf'tag(odex30_account_reports.account_codes_engine_test_tag1)\(101)D + 101003 + tag({account_tags[1].id})\(101)C'),
            groupby='account_id',
        )

        report = self._create_report([
            test_line_1, test_line_2, test_line_3, test_line_4, test_line_5, test_line_6, test_line_7, test_line_8,
            test_line_9, test_line_10, test_line_11, test_line_12, test_line_13, test_line_14, test_line_15, test_line_16
        ])

        # Create the journal entries.
        move = self._create_test_account_moves([
            self._prepare_test_account_move_line(1000.0, account_code='100001'),
            self._prepare_test_account_move_line(2000.0, account_code='101001'),
            self._prepare_test_account_move_line(-300.0, account_code='101002'),
            self._prepare_test_account_move_line(-600.0, account_code='101003'),
            self._prepare_test_account_move_line(10000.0, account_code='10.20.0'),
            self._prepare_test_account_move_line(10.0, account_code='345D'),
        ])

        # Setup tags on accounts
        self.env['account.account'].search([('code', 'in', ('100001', '101001'))]).tag_ids = account_tags[0]
        self.env['account.account'].search([('code', 'in', ('10.20.0', '101002'))]).tag_ids = account_tags[1]

        # Check the values.
        options = self._generate_options(report, '2020-01-01', '2020-01-01', default_options={'unfold_all': True})
        report_lines = report._get_lines(options)

        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [   0,                          1],
            [
                ('test_line_1',       12100.0),
                ('10.20.0 10.20.0',   10000.0),
                ('100001 100001',      1000.0),
                ('101001 101001',      2000.0),
                ('101002 101002',      -300.0),
                ('101003 101003',      -600.0),
                ('test_line_2',        -900.0),
                ('Posted',             -900.0),
                ('101002 101002',      -300.0),
                ('101003 101003',      -600.0),
                ('test_line_3',       13000.0),
                ('10.20.0 10.20.0',   10000.0),
                ('100001 100001',      1000.0),
                ('101001 101001',      2000.0),
                ('test_line_4',       -1700.0),
                ('101001 101001',     -2000.0),
                ('101002 101002',       300.0),
                ('test_line_5',         300.0),
                ('101002 101002',       300.0),
                ('test_line_6',       -2000.0),
                ('101001 101001',     -2000.0),
                ('Journal Entry',     -2000.0),
                ('test_line_7',       10000.0),
                ('10.20.0 10.20.0',   10000.0),
                ('test_line_8',       10000.0),
                ('10.20.0 10.20.0',   10000.0),
                ('test_line_9',        8600.0),
                ('10.20.0 10.20.0',   10000.0),
                ('101001 101001',     -2000.0),
                ('101002 101002',         0.0),
                ('101003 101003',       600.0),
                ('test_line_10',       8600.0),
                ('10.20.0 10.20.0',   10000.0),
                ('101001 101001',     -2000.0),
                ('101003 101003',       600.0),
                ('test_line_11',         10.0),
                ('345D 345D',            10.0),
                ('test_line_12',          0.0),
                ('test_line_13',      12700.0),
                ('10.20.0 10.20.0',   10000.0),
                ('100001 100001',      1000.0),
                ('101001 101001',      2000.0),
                ('101002 101002',      -300.0),
                ('test_line_14',       3000.0),
                ('100001 100001',      1000.0),
                ('101001 101001',      2000.0),
                ('test_line_15',          0.0),
                ('test_line_16',        400.0),
                ('100001 100001',      1000.0),
                ('101003 101003',      -600.0),
            ],
            options,
        )

        # Check redirection.
        expected_redirection_values_list = [
            move.line_ids[:5],
            move.line_ids[:5],
            move.line_ids[:5],
            move.line_ids[1:3],
            move.line_ids[1:3],
            move.line_ids[1],
            move.line_ids[4],
            move.line_ids[4],
            move.line_ids[1:5],
            move.line_ids[1] + move.line_ids[3:5],
            move.line_ids[5],
            move.line_ids[5],
        ]
        for report_line, expected_amls in zip(report.line_ids, expected_redirection_values_list):
            report_line_dict = [x for x in report_lines if x['name'] == report_line.name][0]
            with self.subTest(report_line=report_line.name):
                action_dict = report.action_audit_cell(options, self._get_audit_params_from_report_line(options, report_line, report_line_dict))
                self.assertEqual(move.line_ids.filtered_domain(action_dict['domain']), expected_amls)

    def test_engine_external(self):
        # Create the report.
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_external('sum', [
                self._prepare_test_external_values(100.0, '2020-01-02'),
                self._prepare_test_external_values(200.0, '2020-01-03'),
                self._prepare_test_external_values(300.0, '2020-01-03'),
                self._prepare_test_external_values(299.999999999, '2020-01-05'),
            ])
        )
        test_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_external('most_recent', [
                self._prepare_test_external_values(100.0, '2020-01-02'),
                self._prepare_test_external_values(200.0, '2020-01-03'),
                self._prepare_test_external_values(300.0, '2020-01-03'),
                self._prepare_test_external_values(299.999999999, '2020-01-05'),
            ])
        )
        report = self._create_report([test_line_1, test_line_2])

        # Check the values at multiple dates.
        options = self._generate_options(report, '2020-01-01', '2020-01-01')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',           0.0),
                ('test_line_2',           0.0),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-02', '2020-01-02')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',         100.0),
                ('test_line_2',         100.0),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-03', '2020-01-03')
        report_lines = report._get_lines(options)
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [   0,                          1],
            [
                ('test_line_1',         500.0),
                ('test_line_2',         500.0),
            ],
            options,
        )

        # Check redirection.
        for report_line, report_line_dict in zip(report.line_ids, report_lines):
            with self.subTest(report_line=report_line.name):
                action_dict = report.action_audit_cell(options, self._get_audit_params_from_report_line(options, report_line, report_line_dict))
                self.assertRecordValues(
                    self.env['account.report.external.value'].search(action_dict['domain']),
                    [
                        {'date': fields.Date.from_string('2020-01-03')},
                        {'date': fields.Date.from_string('2020-01-03')},
                    ],
                )

        options = self._generate_options(report, '2020-01-04', '2020-01-04')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',           0.0),
                ('test_line_2',           0.0),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-02', '2020-01-04')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',          600.0),
                ('test_line_2',          500.0),
            ],
            options,
        )

        # Check redirection.
        expected_redirection_values_list = [
            [
                {'date': fields.Date.from_string('2020-01-02')},
                {'date': fields.Date.from_string('2020-01-03')},
                {'date': fields.Date.from_string('2020-01-03')},
            ],
            [
                {'date': fields.Date.from_string('2020-01-03')},
                {'date': fields.Date.from_string('2020-01-03')},
            ],
        ]
        for report_line, report_line_dict, expected_values in zip(report.line_ids, report_lines, expected_redirection_values_list):
            with self.subTest(report_line=report_line.name):
                action_dict = report.action_audit_cell(options, self._get_audit_params_from_report_line(options, report_line, report_line_dict))
                self.assertRecordValues(
                    self.env['account.report.external.value'].search(action_dict['domain']),
                    expected_values,
                )

        options = self._generate_options(report, '2020-01-03', '2020-01-05')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',          800.0),
                ('test_line_2',          300.0),
            ],
            options,
        )

    def test_engine_external_editable_percentage(self):
        # Create the report.
        test_rounding_4 = self._prepare_test_report_line(
            self._prepare_test_expression_external(
                'most_recent', [
                    self._prepare_test_external_values(10.1254, '2020-01-01'),
                    self._prepare_test_external_values(5, '2020-01-02'),
                ], figure_type='percentage', subformula='editable;rounding=4',
            ),
            code='TEST_PERCENTAGE'
        )
        test_rounding_2 = self._prepare_test_report_line(
            self._prepare_test_expression_external(
                'most_recent', [
                    self._prepare_test_external_values(10.12, '2020-01-01'),
                    self._prepare_test_external_values(5, '2020-01-02'),
                ], figure_type='percentage', subformula='editable;rounding=2',
            )
        )
        test_percentage_aggregate = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('10000 * TEST_PERCENTAGE.balance'),
        )
        test_float = self._prepare_test_report_line(
            self._prepare_test_expression_external(
                'most_recent', [
                    self._prepare_test_external_values(10.12, '2020-01-01'),
                    self._prepare_test_external_values(5, '2020-01-02'),
                ], figure_type='float', subformula='editable;rounding=2',
            )
        )

        report = self._create_report([test_rounding_4, test_rounding_2, test_percentage_aggregate, test_float])
        # Check the values at multiple dates.
        options = self._generate_options(report, '2020-01-01', '2020-01-01')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [0,                          1],
            [
                ('test_line_1', '10.1254%'),
                ('test_line_2',   '10.12%'),
                ('test_line_3',     101254),
                ('test_line_4',    '10.12'),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-02', '2020-01-02')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [0,                         1],
            [
                ('test_line_1', '5.0000%'),
                ('test_line_2',   '5.00%'),
                ('test_line_3',     50000),
                ('test_line_4',    '5.00'),
            ],
            options,
        )

    def test_engine_custom(self):
        # Create the report.
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_custom('_custom_engine_test', subformula='sum'),
            groupby='account_id',
        )
        report = self._create_report([test_line_1])

        # Create the journal entries.
        self._create_test_account_moves([
            self._prepare_test_account_move_line(2000.0, account_code='101001'),
            self._prepare_test_account_move_line(-300.0, account_code='101002'),
        ])

        # Check the values.

        def _custom_engine_test(expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
            domain = [('account_id.code', '=', '101002')]
            domain_key = str(domain)
            formulas_dict = {domain_key: expressions}
            domain_result = report._compute_formula_batch_with_engine_domain(
                options, date_scope, formulas_dict, current_groupby, next_groupby,
                offset=offset, limit=limit,
            )
            return list(domain_result.values())[0]

        orig_get_custom_report_function = report._get_custom_report_function

        def get_custom_report_function(_report, function_name, prefix):
            if function_name == '_custom_engine_test':
                return _custom_engine_test
            return orig_get_custom_report_function(function_name, prefix)

        with patch.object(type(report), '_get_custom_report_function', get_custom_report_function):
            options = self._generate_options(report, '2020-01-01', '2020-01-01')
            self.assertLinesValues(
                # pylint: disable=bad-whitespace
                report._get_lines(options),
                [   0,                          1],
                [
                    ('test_line_1',        -300.0),
                    ('101002 101002',      -300.0),
                ],
                options,
            )

    def test_engine_aggregation(self):
        self.env.company.account_fiscal_country_id = self.fake_country

        # Test division by zero.
        test1 = self._prepare_test_report_line(
            self._prepare_test_expression_tax_tags('11', label='tax_tags'),
            self._prepare_test_expression_domain([('account_id.code', '=', '101002')], 'sum', label='domain'),
            self._prepare_test_expression_external('sum', [self._prepare_test_external_values(100.0, '2020-01-01')], label='external'),
            self._prepare_test_expression_aggregation('test1.tax_tags + test1.domain', column='aggregation'),
            self._prepare_test_expression_aggregation('test1.tax_tags / 0', subformula='ignore_zero_division'),
            self._prepare_test_expression_external('sum', [self._prepare_test_external_values(100.47, '2020-01-01')], label='external_decimal'),
            name='test1', code='test1',
        )

        # Test if_(above|below|between) operators.
        test2_1 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.tax_tags', subformula='if_above(USD(0))'),
            name='test2_1', code='test2_1',
        )
        test2_2 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.tax_tags', subformula='if_above(USD(1999.9999999))'),
            name='test2_2', code='test2_2',
        )
        test2_3 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.tax_tags', subformula='if_above(USD(2500.0))'),
            name='test2_3', code='test2_3',
        )
        test2_4 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.tax_tags', subformula='if_above(CAD(3600.0))'),
            name='test2_4', code='test2_4',
        )
        test3_1 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.domain', subformula='if_below(USD(0))'),
            name='test3_1', code='test3_1',
        )
        test3_2 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.domain', subformula='if_below(USD(-300.00001))'),
            name='test3_2', code='test3_2',
        )
        test3_3 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.domain', subformula='if_below(USD(- 350))'),
            name='test3_3', code='test3_3',
        )
        test4_1 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.tax_tags + test1.domain', subformula='if_between(USD(0), USD(2000))'),
            name='test4_1', code='test4_1',
        )
        test4_2 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.tax_tags + test1.domain', subformula='if_between(CAD(0), CAD(3000))'),
            name='test4_2', code='test4_2',
        )

        # Test line code recognition.
        test5 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('101003', label='account_codes'),
            self._prepare_test_expression_aggregation('test1.tax_tags + 9999.account_codes'),
            name='9999', code='9999',
        )

        # Test mathematical operators.
        test6 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation(
                '(test1.tax_tags + (2 * test1.domain) + 100.0) / (9999.account_codes)'
            ),
            name='test6', code='test6',
        )

        # Test other date scope
        test7 = self._prepare_test_report_line(
            self._prepare_test_expression_domain(
                [('account_id.code', '=', '101002')],
                'sum',
                label='domain',
                date_scope='to_beginning_of_period',
            ),
            self._prepare_test_expression_aggregation('test7.domain'),
            name='test7', code='test7',
        )

        # Test exponential notation
        test9 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation(
                '(test1.tax_tags + (2 * test1.domain) + 100.0 + 1.752e-17) / (9999.account_codes)'
            ),
            name='test9', code='test9',
        )

        # Test 'round' subformula
        test10_1 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external_decimal', subformula='round(0)'),
            name='test10_1', code='test10_1',
        )
        test10_2 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external_decimal', subformula='round(1)'),
            name='test10_2', code='test10_2',
        )
        test10_3 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external_decimal', subformula='round(3)'),
            name='test10_3', code='test10_3',
        )

        # Test if_other_expr_above / if_other_expr_below
        test11_1 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external', subformula='if_other_expr_above(test1.tax_tags, USD(3000.0))'),
            name='test11_1', code='test11_1',
        )
        test11_2 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external', subformula='if_other_expr_below(test1.tax_tags, USD(3000.0))'),
            name='test11_2', code='test11_2',
        )
        test11_3 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external', subformula='if_other_expr_above(test1.tax_tags, USD(1000.0))'),
            name='test11_3', code='test11_3',
        )
        test11_4 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external', subformula='if_other_expr_below(test1.tax_tags, USD(1000.0))'),
            name='test11_4', code='test11_4',
        )
        # Test with an aggregation in the condition
        test11_5 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external', subformula='if_other_expr_above(test1.aggregation, USD(1000.0))'),
            name='test11_5', code='test11_5',
        )
        test11_6 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.external', subformula='if_other_expr_below(test1.aggregation, USD(1000.0))'),
            name='test11_6', code='test11_6',
        )

        # Test sum_children formula (parent_id relationship is populated below)
        test_12_1 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('sum_children'),
            name='test12_1', code='test12_1',
        )
        test_12_2 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.tax_tags'),
            name='test12_2', code='test12_2',
        )
        test_12_3 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test1.domain'),
            name='test12_3', code='test12_3',
        )
        test_12_4 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('sum_children'),
            name='test12_4', code='test12_4',
        )
        test_12_5 = self._prepare_test_report_line(
            self._prepare_test_expression_domain([('account_id.code', '=', '101003')], 'sum'),
            name='test12_5', # No code on purpose to check a different case of sum_children
        )

        report = self._create_report(
            [
                test1, test2_1, test2_2, test2_3, test2_4, test3_1, test3_2, test3_3, test4_1, test4_2,
                test5, test6, test7, test9, test10_1, test10_2, test10_3, test11_1, test11_2, test11_3, test11_4,
                test11_5, test11_6, test_12_1, test_12_2, test_12_3, test_12_4, test_12_5,
            ],
            country_id=self.fake_country.id,
        )

        # Set parent link properly for sum_children test, now that all lines are created:
        line_12_1 = self.env['account.report.line'].search([('code', '=', 'test12_1')])
        self.env['account.report.line'].search([('code', 'in', ('test12_2', 'test12_3', 'test12_4'))]).parent_id = line_12_1
        line_12_4 = self.env['account.report.line'].search([('code', '=', 'test12_4')])
        self.env['account.report.line'].search([('name', '=', 'test12_5')]).parent_id = line_12_4

        # Create the journal entries.
        moves = self._create_test_account_moves([
            self._prepare_test_account_move_line(100000.0, account_code='101002', date='2019-01-01'),
            self._prepare_test_account_move_line(2000.0, account_code='101001', tax_tags=['+11']),
            self._prepare_test_account_move_line(-300.0, account_code='101002'),
            self._prepare_test_account_move_line(1500.0, account_code='101003'),
        ])

        # Check the values.
        options = self._generate_options(report, '2020-01-01', '2020-01-01')
        report_lines = report._get_lines(options)
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_lines,
            [   0,                          1],
            [
                ('test1',                 0.0),
                ('test2_1',            2000.0),
                ('test2_2',               0.0),
                ('test2_3',               0.0),
                ('test2_4',            2000.0),
                ('test3_1',            -300.0),
                ('test3_2',               0.0),
                ('test3_3',               0.0),
                ('test4_1',            1700.0),
                ('test4_2',               0.0),
                ('9999',               3500.0),
                ('test6',                 1.0),
                ('test7',            100000.0),
                ('test9',                 1.0),
                ('test10_1',            100.0),
                ('test10_2',            100.5),
                ('test10_3',            100.47),
                ('test11_1',              0.0),
                ('test11_2',            100.0),
                ('test11_3',            100.0),
                ('test11_4',              0.0),
                ('test11_5',            100.0),
                ('test11_6',              0.0),
                ('test12_1',           3200.0),
                ('test12_2',           2000.0),
                ('test12_3',           -300.0),
                ('test12_4',           1500.0),
                ('test12_5',           1500.0),
            ],
            options,
        )

        # Check redirection.
        expected_amls_to_test = [
            ('9999', moves[1].line_ids[0] + moves[1].line_ids[2]),
            ('test7', moves[0].line_ids[0]),
            ('test12_1', moves[1].line_ids[:3]),
        ]
        for report_line_name, expected_amls in expected_amls_to_test:
            report_line = report.line_ids.filtered(lambda x: x.name == report_line_name)
            report_line_dict = [x for x in report_lines if x['name'] == report_line.name][0]
            with self.subTest(report_line=report_line.name):
                action_dict = report.action_audit_cell(options, self._get_audit_params_from_report_line(options, report_line, report_line_dict))
                self.assertEqual(moves.line_ids.filtered_domain(action_dict['domain']), expected_amls)

    def test_engine_aggregation_cross_report(self):
        moves = self._create_test_account_moves([
            self._prepare_test_account_move_line(1.0, account_code='100000', date='2020-01-01'),
            self._prepare_test_account_move_line(2.0, account_code='100000', date='2021-01-01'),
            self._prepare_test_account_move_line(3.0, account_code='200000', date='2020-01-01'),
            self._prepare_test_account_move_line(4.0, account_code='200000', date='2021-01-01'),
            self._prepare_test_account_move_line(5.0, account_code='300000', date='2021-01-01'),
        ])

        # Other report
        other_report_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('1'),
            name='other_report_line_1', code='other_report_line_1',
        )

        other_report_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('2'),
            name='other_report_line_2', code='other_report_line_2',
        )

        other_report_line_3 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('other_report_line_1.balance + other_report_line_2.balance'),
            name='other_report_line_3', code='other_report_line_3',
        )

        other_report_line_4 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('3'),
            name='other_report_line_4', code='other_report_line_4',
        )

        other_report = self._create_report([other_report_line_1, other_report_line_2, other_report_line_3, other_report_line_4])
        other_report_options = self._generate_options(other_report, '2021-01-01', '2021-01-01')

        # Main report
        main_report_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('other_report_line_2.balance', subformula='cross_report', date_scope='strict_range'),
            name='main_report_line_1', code='main_report_line_1',
        )

        main_report_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('other_report_line_2.balance', subformula='cross_report', date_scope='from_beginning'),
            name='main_report_line_2', code='main_report_line_2',
        )

        main_report_line_3 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('other_report_line_3.balance', subformula='cross_report', date_scope='strict_range'),
            name='main_report_line_3', code='main_report_line_3',
        )

        main_report_line_4 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('other_report_line_3.balance', subformula='cross_report', date_scope='from_beginning'),
            name='main_report_line_4', code='main_report_line_4',
        )

        main_report_line_5 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation(
                'main_report_line_1.balance + main_report_line_2.balance + main_report_line_3.balance + main_report_line_4.balance',
            ),
            name='main_report_line_5', code='main_report_line_5',
        )

        main_report_line_6 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation(
                'main_report_line_1.balance + main_report_line_2.balance + main_report_line_3.balance + main_report_line_4.balance',
            ),
            name='main_report_line_6', code='main_report_line_6',
        )

        main_report = self._create_report([main_report_line_1, main_report_line_2, main_report_line_3, main_report_line_4, main_report_line_5, main_report_line_6])
        main_report_options = self._generate_options(main_report, '2021-01-01', '2021-01-01')

        # First check other_report
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            other_report._get_lines(other_report_options),
            [   0,                                      1],
            [
                ('other_report_line_1',               2.0),
                ('other_report_line_2',               4.0),
                ('other_report_line_3',               6.0),
                ('other_report_line_4',               5.0),
            ],
            other_report_options,
        )

        # Check main_report
        main_report_options = self._generate_options(main_report, '2021-01-01', '2021-01-01')
        main_report_lines = main_report._get_lines(main_report_options)
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            main_report_lines,
            [   0,                                      1],
            [
                ('main_report_line_1',                4.0),
                ('main_report_line_2',                7.0),
                ('main_report_line_3',                6.0),
                ('main_report_line_4',               10.0),
                ('main_report_line_5',               27.0),
                ('main_report_line_6',               27.0),
            ],
            main_report_options,
        )

        # Check redirection.
        expected_amls_to_test = [
            ('main_report_line_1', moves[1].line_ids[1]),
            ('main_report_line_2', moves[1].line_ids[1] + moves[0].line_ids[1]),
        ]

        for report_line_name, expected_amls in expected_amls_to_test:
            report_line = main_report.line_ids.filtered(lambda x: x.name == report_line_name)
            report_line_dict = [x for x in main_report_lines if x['name'] == report_line.name][0]
            with self.subTest(report_line=report_line.name):
                action_dict = main_report.action_audit_cell(main_report_options, self._get_audit_params_from_report_line(main_report_options, report_line, report_line_dict))
                self.assertEqual(moves.line_ids.filtered_domain(action_dict['domain']), expected_amls)

    def test_engine_aggregation_expansion(self):
        report = self._create_report([
            self._prepare_test_report_line(
                self._prepare_test_expression_tax_tags('42'),
                code='TAG_1',
            ),
            self._prepare_test_report_line(
                self._prepare_test_expression_tax_tags('221292'),
                code='TAG_2',
            ),
            self._prepare_test_report_line(
                self._prepare_test_expression_tax_tags('777'),
                code='TAG_3',
            ),
            self._prepare_test_report_line(
                self._prepare_test_expression_aggregation('TAG_1.balance + TAG_2.balance'),
                code='SIMPLE_AGG',
            ),
            self._prepare_test_report_line(
                self._prepare_test_expression_aggregation('SIMPLE_AGG.balance + TAG_3.balance'),
                code='COMPLEX_AGG',
            ),
            self._prepare_test_report_line(
                self._prepare_test_expression_aggregation('TAG_1.balance + TAG_2.balance', subformula='if_other_expr_above(TAG_3.balance, EUR(13))'),
                code='BOUNDED_AGG',
            ),
            self._prepare_test_report_line(
                self._prepare_test_expression_tax_tags('3333'),
            ),
        ])

        other_report = self._create_report([
            self._prepare_test_report_line(
                self._prepare_test_expression_aggregation('SIMPLE_AGG.balance + BOUNDED_AGG.balance', subformula='cross_report'),
                code='CROSS_REPORT_AGG',
            ),
        ])

        expr_map = {expression.report_line_id.code: expression for expression in (report + other_report).line_ids.expression_ids}

        self.assertEqual(
            expr_map['SIMPLE_AGG']._expand_aggregations(),
            expr_map['SIMPLE_AGG'] + expr_map['TAG_1'] + expr_map['TAG_2'],
        )

        self.assertEqual(
            expr_map['COMPLEX_AGG']._expand_aggregations(),
            expr_map['COMPLEX_AGG'] + expr_map['SIMPLE_AGG'] + expr_map['TAG_1'] + expr_map['TAG_2'] + expr_map['TAG_3'],
        )

        self.assertEqual(
            expr_map['BOUNDED_AGG']._expand_aggregations(),
            expr_map['BOUNDED_AGG'] + expr_map['TAG_1'] + expr_map['TAG_2'] + expr_map['TAG_3'],
        )

        self.assertEqual(
            expr_map['CROSS_REPORT_AGG']._expand_aggregations(),
            expr_map['CROSS_REPORT_AGG'] + expr_map['SIMPLE_AGG'] + expr_map['BOUNDED_AGG'] + expr_map['TAG_1'] + expr_map['TAG_2'] + expr_map['TAG_3'],
        )

    def test_load_more(self):
        partner_a, partner_b, partner_c = self.env['res.partner'].create([
            {'name': 'Partner A'},
            {'name': 'Partner B'},
            {'name': 'Partner C'},
        ])

        self._create_test_account_moves([
            self._prepare_test_account_move_line(1000.0, partner_id=partner_a.id, date='2020-01-01'),
            self._prepare_test_account_move_line(2000.0, partner_id=partner_b.id, date='2020-01-01'),
            self._prepare_test_account_move_line(3000.0, partner_id=partner_c.id, date='2020-01-01'),
        ])

        report = self._create_report(
            test_report_line_values_list=[self._prepare_test_report_line(
                self._prepare_test_expression_domain([('partner_id', '!=', False)], 'sum'),
                groupby='partner_id',
            )],
            load_more_limit=2,
        )

        options = self._generate_options(report, '2020-01-01', '2020-01-31')
        lines = report._get_lines(options)

        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            lines,
            [   0,                                1],
            [
                ('test_line_1',          '6,000.00'),
                ('Partner A',            '1,000.00'),
                ('Partner B',            '2,000.00'),
                ('Load more...',                 ''),
            ],
            options,
        )

        load_more_line = lines[-1]
        load_more_res = report.get_expanded_lines(
            options,
            load_more_line['id'],
            load_more_line['groupby'],
            load_more_line['expand_function'],
            load_more_line['progress'],
            load_more_line['offset'],
            None,
        )

        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            load_more_res,
            [   0,                                1],
            [
                ('Partner C',            '3,000.00'),
            ],
            options,
        )

    def test_engine_external_boolean(self):
        # Create the report.
        test_line = self._prepare_test_report_line(
            self._prepare_test_expression_external('most_recent', [
                self._prepare_test_external_values('1', '2020-01-02'),
                self._prepare_test_external_values('0', '2020-01-03'),
                self._prepare_test_external_values('1', '2020-01-03'),
                self._prepare_test_external_values('0', '2020-01-05'),
            ], figure_type='boolean')
        )

        report = self._create_report([test_line])
        # Check the values at multiple dates.
        options = self._generate_options(report, '2020-01-01', '2020-01-01')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',          'No'),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-02', '2020-01-02')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',         'Yes'),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-03', '2020-01-03')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',         'Yes'),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-05', '2020-01-05')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',          'No'),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-02', '2020-01-05')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1],
            [
                ('test_line_1',          'No'),
            ],
            options,
        )

    def test_engine_external_string(self):
        # Create the report.
        test_line = self._prepare_test_report_line(
            self._prepare_test_expression_external('most_recent', [
                self._prepare_test_external_values('TARDIS', '2020-01-02', figure_type='string'),
                self._prepare_test_external_values('Kris Kelvin', '2020-01-03', figure_type='string'),
                self._prepare_test_external_values('Trisolaris', '2020-01-03', figure_type='string'),
                self._prepare_test_external_values("5-ounce bird carrying a 1-pound coconut", '2020-01-05', figure_type='string'),
            ], figure_type='string')
        )

        report = self._create_report([test_line])
        # Check the values at multiple dates.
        options = self._generate_options(report, '2020-01-01', '2020-01-01')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                                    1],
            [
                ('test_line_1',                      ''),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-02', '2020-01-02')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                                    1],
            [
                ('test_line_1',                'TARDIS'),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-03', '2020-01-03')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                                    1],
            [
                ('test_line_1',            'Trisolaris'),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-05', '2020-01-05')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                                    1],
            [
                ('test_line_1', "5-ounce bird carrying a 1-pound coconut"),
            ],
            options,
        )

        options = self._generate_options(report, '2020-01-02', '2020-01-05')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                                    1],
            [
                ('test_line_1', "5-ounce bird carrying a 1-pound coconut"),
            ],
            options,
        )

    def test_engine_external_default_value_tax_closing_fiscalyear_lock_date(self):
        def lock_via_fiscalyear_lock_date(non_tax_report, tax_report, report_options_map):
            lock_date_wizard = self.env['account.change.lock.date'].create({
                'fiscalyear_lock_date': fields.Date.from_string('2020-01-02'),
            })
            lock_date_wizard.change_lock_date()

        self._run_external_engine_default_test_case(False, True, lock_via_fiscalyear_lock_date)

    def test_engine_external_default_value_tax_closing_tax_lock_date(self):
        def lock_via_tax_lock_date(non_tax_report, tax_report, report_options_map):
            lock_date_wizard = self.env['account.change.lock.date'].create({
                'tax_lock_date': fields.Date.from_string('2020-01-02'),
            })
            lock_date_wizard.change_lock_date()

        self._run_external_engine_default_test_case(True, False, lock_via_tax_lock_date)

    def test_engine_external_default_value_tax_closing(self):
        def lock_via_tax_closing(non_tax_report, tax_report, report_options_map):
            tax_closing_action = self.env['account.tax.report.handler'].with_context({'override_tax_closing_warning': True}).action_periodic_vat_entries(report_options_map[tax_report])
            closing_move_id = tax_closing_action['res_id']
            with self.enter_test_mode():
                self.env['account.move'].browse(closing_move_id).action_post()

        self._run_external_engine_default_test_case(True, False, lock_via_tax_closing)

    def _run_external_engine_default_test_case(self, impact_tax_report, impact_non_tax_report, lock_operation_function):
        """ Common helper to run the tests of _default expressions
        """
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('10'),
            groupby='account_id',
        )
        test_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_external('sum', {}),
            self._prepare_test_expression_account_codes('10', label='_default_balance'),
        )

        non_tax_report = self._create_report([test_line_1, test_line_2], name="non_tax_report")
        tax_report = self._create_report([test_line_1, test_line_2], root_report_id=self.env.ref('account.generic_tax_report').id, name="tax_report")

        # Create the journal entries.
        self._create_test_account_moves([
            self._prepare_test_account_move_line(1000.0, account_code='100001'),
            self._prepare_test_account_move_line(-300.0, account_code='101002'),
            self._prepare_test_account_move_line(-600.0, account_code='314159'),
        ])

        report_options_map = {
            report: self._generate_options(
                report,
                '2020-01-01', '2020-01-31',
                default_options={
                    'unfold_all': True,
                }
            )
            for report in [non_tax_report, tax_report]
        }

        # Check the values before locking
        for report in [non_tax_report, tax_report]:
            with self.subTest(report=report.name):
                options = report_options_map[report]
                self.assertLinesValues(
                    # pylint: disable=bad-whitespace
                    report._get_lines(options),
                    [0,                             1],
                    [
                        ('test_line_1',         700.0),
                        ('100001 100001',      1000.0),
                        ('101002 101002',      -300.0),
                        ('test_line_2',           0.0),
                    ],
                    options,
                )

        # Run the lock operation
        lock_operation_function(non_tax_report, tax_report, report_options_map)

        # Check the values after locking
        for report, impacted in [(non_tax_report, impact_non_tax_report), (tax_report, impact_tax_report)]:
            with self.subTest(report=report.name):
                options = report_options_map[report]
                if impacted:
                    self.assertLinesValues(
                        # pylint: disable=bad-whitespace
                        report._get_lines(options),
                        [0,                             1],
                        [
                            ('test_line_1',         700.0),
                            ('100001 100001',      1000.0),
                            ('101002 101002',      -300.0),
                            ('test_line_2',         700.0),
                        ],
                        options,
                    )
                else:
                    self.assertLinesValues(
                        # pylint: disable=bad-whitespace
                        report._get_lines(options),
                        [0,                             1],
                        [
                            ('test_line_1',         700.0),
                            ('100001 100001',      1000.0),
                            ('101002 101002',      -300.0),
                            ('test_line_2',           0.0),
                        ],
                        options,
                    )

    def test_engine_aggregation_cross_bound(self):
        report_1 = self._create_report([
            self._prepare_test_report_line(
                self._prepare_test_expression_aggregation('line_2_1.balance', subformula='cross_report'),
                name='Line 1-1',
                code='line_1_1',
            ),
        ])

        self._create_report([
            self._prepare_test_report_line(
                self._prepare_test_expression_aggregation('14.0', subformula='if_other_expr_above(line_2_1.dudu, EUR(0))'),
                self._prepare_test_expression_account_codes('101', label='dudu'),
                name='Line 2-1',
                code='line_2_1',
            ),
        ])

        options = self._generate_options(report_1, '2020-01-01', '2020-01-01')

        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_1._get_lines(options),
            [   0,                          1],
            [
                ('Line 1-1',              0.0),
            ],
            options
        )

        self._create_test_account_moves([
            self._prepare_test_account_move_line(10, account_code='101001'),
            self._prepare_test_account_move_line(-10, account_code='100001'),
        ])

        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report_1._get_lines(options),
            [   0,                          1],
            [
                ('Line 1-1',             14.0),
            ],
            options
        )

    def test_change_expression_engine_to_tax_tags(self):
        """
        Ensure that tax tags are created when switching the expression engine to tax tags if formula is unchanged.
        """
        formula = 'dudu'
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_external(formula, [self._prepare_test_external_values(100.0, '2020-01-01')], label='external'),
        )
        report = self._create_report([test_line_1], country_id=self.fake_country.id)
        tags = self.env['account.account.tag']._get_tax_tags(formula, self.fake_country.id)
        self.assertEqual(len(tags), 0)
        report.line_ids[0].expression_ids[0].engine = 'tax_tags'
        tags = self.env['account.account.tag']._get_tax_tags(formula, self.fake_country.id)
        self.assertEqual(tags.mapped('name'), ['-' + formula, '+' + formula])

    def test_integer_rounding(self):
        line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_domain([('account_id.code', '=', '101001')], 'sum'),
            code='test_1',
        )
        line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_tax_tags('42'),
            code='test_2',
        )
        line_3 = self._prepare_test_report_line(
            self._prepare_test_expression_account_codes('1'),
            code='test_3',
        )
        line_4 = self._prepare_test_report_line(
            self._prepare_test_expression_external('sum', [
                self._prepare_test_external_values(3.5, '2023-01-01'),
            ]),
            code='test_4',
        )
        line_5 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test_1.balance + test_2.balance + test_3.balance + test_4.balance'),
            code='test_5',
        )
        line_6 = self._prepare_test_report_line(
            self._prepare_test_expression_aggregation('test_5.balance / 10'),
        )

        report = self._create_report([line_1, line_2, line_3, line_4, line_5, line_6], country_id=self.fake_country.id,)

        self._create_test_account_moves([
            self._prepare_test_account_move_line(5.4, account_code='101001', tax_tags=['+42'], date='2023-01-01'),
        ])

        # Test with a first rounding
        report.integer_rounding = 'HALF-UP'
        half_up_options = self._generate_options(report, '2023-01-01', '2023-01-01')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(half_up_options),
            [   0,                              1],
            [
                ('test_line_1',               5.0),
                ('test_line_2',               5.0),
                ('test_line_3',               5.0),
                ('test_line_4',               4.0),
                ('test_line_5',              19.0),
                ('test_line_6',               2.0),
            ],
            half_up_options,
        )

        # Test with another rounding method
        report.integer_rounding = 'UP'
        up_options = self._generate_options(report, '2023-01-01', '2023-01-01')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(up_options),
            [   0,                              1],
            [
                ('test_line_1',               6.0),
                ('test_line_2',               6.0),
                ('test_line_3',               6.0),
                ('test_line_4',               4.0),
                ('test_line_5',              22.0),
                ('test_line_6',               3.0),
            ],
            up_options
        )

        # In file export mode, the rounding should always be applied, even if it was previously disabled
        print_mode_options = self._generate_options(report, '2023-01-01', '2023-01-01', default_options={'integer_rounding_enabled': False, 'export_mode': 'file'})
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(print_mode_options),
            [   0,                              1],
            [
                ('test_line_1',               6.0),
                ('test_line_2',               6.0),
                ('test_line_3',               6.0),
                ('test_line_4',               4.0),
                ('test_line_5',              22.0),
                ('test_line_6',               3.0),
            ],
            print_mode_options,
        )

        # Rounding available, but disabled
        no_rounding_options = self._generate_options(report, '2023-01-01', '2023-01-01', default_options={'integer_rounding_enabled': False})
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(no_rounding_options),
            [   0,                               1],
            [
                ('test_line_1',               5.40),
                ('test_line_2',               5.40),
                ('test_line_3',               5.40),
                ('test_line_4',               3.50),
                ('test_line_5',              19.70),
                ('test_line_6',               1.97),
            ],
            no_rounding_options,
        )

    def test_print_hide_0_lines(self):
        # Create the report.
        test_line_1 = self._prepare_test_report_line(
            self._prepare_test_expression_tax_tags('11'),
            groupby='account_id',
        )
        test_line_2 = self._prepare_test_report_line(
            self._prepare_test_expression_tax_tags('222T'),
            groupby='account_id',
        )
        report = self._create_report([test_line_1, test_line_2], country_id=self.fake_country.id)

        # Create the journal entries.
        self._create_test_account_moves([
            self._prepare_test_account_move_line(3000.0, account_code='101001', tax_tags=['+11', '-222T']),
            self._prepare_test_account_move_line(3600.0, account_code='101001', tax_tags=['+222T']),
            self._prepare_test_account_move_line(-600.0, account_code='101001', tax_tags=['+222T', '-11']),
        ])

        # To ensure that the lines are shown when hide_0_lines isn't toggled and vice versa, we test both scenarios.
        options_not_hide = self._generate_options(
            report,
            '2020-01-01', '2020-01-01',
            default_options={'unfold_all': True, 'export_mode': 'print'}
        )
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options_not_hide),
            [   0,                        1],
            [
                ('test_line_1',      3600.0),
                ('101001 101001',    3600.0),
                ('test_line_2',         0.0),
                ('101001 101001',       0.0),
            ],
            options_not_hide,
        )

        options_hide = self._generate_options(
            report,
            '2020-01-01', '2020-01-01',
            default_options={'unfold_all': True, 'export_mode': 'print', 'hide_0_lines': True}
        )
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options_hide),
            [   0,                        1],
            [
                ('test_line_1',      3600.0),
                ('101001 101001',    3600.0),
            ],
            options_hide,
        )

    def test_column_blank_if_zero(self):
        """account.report.column's `blank_if_zero` option should only impacts number figure types"""
        test_line = self._prepare_test_report_line(
            self._prepare_test_expression_external('most_recent', [], label='monetary', figure_type='monetary'),

            self._prepare_test_expression_external('most_recent', [], label='percentage', figure_type='percentage'),

            self._prepare_test_expression_external('most_recent', [], label='integer', figure_type='integer'),

            self._prepare_test_expression_external('most_recent', [], label='float', figure_type='float'),

            self._prepare_test_expression_external('most_recent', [
                self._prepare_test_external_values(False, '2024-01-01', figure_type='boolean'),
            ], label='boolean', figure_type='boolean'),

            self._prepare_test_expression_external('most_recent', [
                self._prepare_test_external_values('dudu', '2024-01-01', figure_type='string'),
            ], label='string', figure_type='string'),
        )

        report = self._create_report([test_line], columns=['monetary', 'percentage', 'integer', 'float', 'boolean', 'string'])
        report.column_ids.blank_if_zero = True

        options = self._generate_options(report, '2024-01-01', '2024-01-01')
        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                          1,  2,   3,  4,      5,       6],
            [
                ('test_line_1',            '', '',  '', '',   'No',  'dudu'),
            ],
            options,
        )

    def test_column_groups_audit(self):
        account = self.company_data['default_account_assets']
        move = self.env['account.move'].create({
            'move_type': 'entry',
            'date': '2024-01-01',
            'line_ids': [
                (0, 0, {'debit': 10.0,      'credit': 0.0,      'account_id': account.id,   'partner_id': self.partner_a.id}),
                (0, 0, {'debit': 20.0,      'credit': 0.0,      'account_id': account.id,   'partner_id': self.partner_b.id}),
                (0, 0, {'debit':  0.0,     'credit': 30.0,      'account_id': account.id}),
            ],
        })
        move.action_post()

        report = self._create_report([
            self._prepare_test_report_line(
                self._prepare_test_expression_domain([('account_id', '=', account.id)], 'sum'),
                code='report_line',
            ),
        ], country_id=self.fake_country.id)

        horizontal_group = self.env['account.report.horizontal.group'].create({
            'name': 'Horizontal Group',
            'report_ids': report.ids,
            'rule_ids': [
                Command.create({
                    'field_name': 'partner_id',
                    'domain': f"[('id', 'in', {(self.partner_a + self.partner_b).ids})]",
                }),
            ],
        })

        main_options = self._generate_options(report, '2024-01-01', '2024-01-01', default_options={'selected_horizontal_group_id': horizontal_group.id})
        lines = report._get_lines(main_options)
        for (col_group_key, col_group_options), expected_partner in zip(report._split_options_per_column_group(main_options).items(), [self.partner_a, self.partner_b]):
            audit_params = self._get_audit_params_from_report_line(col_group_options, report.line_ids, lines[0], column_group_key=col_group_key)
            action_dict = report.action_audit_cell(col_group_options, audit_params)

            expected_amls = move.line_ids.filtered(lambda x: x.partner_id == expected_partner)
            audit_result_amls = move.line_ids.filtered_domain(action_dict['domain'])
            self.assertEqual(audit_result_amls, expected_amls, f"Wrong audit result for partner {expected_partner.name}: {audit_result_amls}")

    def test_account_codes_load_more_limit_groupby(self):
        """ The account_codes engine performs an additional groupby in its SQL query. This tests makes sure this does not break the behavior
        of the load_more_limit when grouping.
        """
        partner_a, partner_b, partner_c = self.env['res.partner'].create([
            {'name': 'Partner A'},
            {'name': 'Partner B'},
            {'name': 'Partner C'},
        ])

        report = self._create_report(
            [
                self._prepare_test_report_line(
                    self._prepare_test_expression_account_codes('1'),
                    groupby='partner_id',
                ),
            ],
            load_more_limit=2,
        )

        move = self._create_test_account_moves([
            self._prepare_test_account_move_line(10, account_code='11', partner_id=partner_a.id),
            self._prepare_test_account_move_line(20, account_code='11', partner_id=partner_a.id),
            self._prepare_test_account_move_line(25, account_code='12', partner_id=partner_a.id),
            self._prepare_test_account_move_line(30, account_code='11'),
            self._prepare_test_account_move_line(40, account_code='11'),
            self._prepare_test_account_move_line(50, account_code='12', partner_id=partner_b.id),
            self._prepare_test_account_move_line(60, account_code='13', partner_id=partner_c.id),
            self._prepare_test_account_move_line(-210, account_code='2'),
        ])

        options = self._generate_options(report, '2020-01-01', '2020-01-01', default_options={'unfold_all': True})

        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            report._get_lines(options),
            [   0,                               1],
            [
                ('test_line_1',              235.0),
                ('Partner A',                 55.0),
                ('Partner B',                 50.0),
                ('Load more...',                ''),
            ],
            options,
        )

    def test_account_name_added_in_account_code_grouping(self):
        company_a = self.env['res.company'].sudo().create({
            'name': "Company A",
        })
        company_b = self.env['res.company'].sudo().create({
            'name': "Company B",
        })
        context = {
            **self.env.context,
            'allowed_company_ids': [company_a.id, company_b.id],
        }
        journal_vals = {
            'name': "Misc",
            'type': 'general',
            'code': "MSC",
        }
        AccountJournal = self.env['account.journal'].with_context(context)
        AccountJournal.create(journal_vals | {'company_id': company_a.id})
        AccountJournal.with_company(company_b).create(journal_vals | {'company_id': company_b.id})

        AccountAccount = self.env['account.account'].with_context(context)
        account_a = AccountAccount.create({
            'name': "Account A",
            'code': "100000",
            'company_ids': company_a.ids,
        })
        counterpart_account_a = AccountAccount.create({
            'name': "Some account",
            'code': "200000",
            'company_ids': company_a.ids,
        })
        account_b1 = AccountAccount.create({
            'name': "Mapping to Account A code",
            'code': "300000",
            'company_ids': company_b.ids,
        })
        account_b2 = AccountAccount.create({
            'name': "Mapping to a code that doesn't exist in company A",
            'code': "400000",
            'company_ids': company_b.ids,
        })
        account_b3 = AccountAccount.create({
            'name': "No mapping in company A",
            'code': "500000",
            'company_ids': company_b.ids,
        })

        account_b1.code = account_a.code
        account_b2.code = "600000"

        date = '2025-01-01'

        AccountMove = self.env['account.move'].with_context(context)
        AccountMove.create({
            'move_type': 'entry',
            'date': date,
            'line_ids': [
                Command.create({
                    'account_id': account_a.id,
                    'balance': 20,
                }),
                Command.create({
                    'account_id': counterpart_account_a.id,
                    'balance': -20,
                }),
            ],
            'company_id': company_a.id,
        }).action_post()
        AccountMove.create({
            'move_type': 'entry',
            'date': date,
            'line_ids': [
                Command.create({
                    'account_id': account_b1.id,
                    'balance': 30,
                }),
                Command.create({
                    'account_id': account_b2.id,
                    'balance': 70,
                }),
                Command.create({
                    'account_id': account_b3.id,
                    'balance': -100,
                }),
            ],
            'company_id': company_b.id,
        }).action_post()

        report = self.env['account.report'].with_context(context).create({
            'name': "Simple Report",
            'filter_multi_company': 'selector',
            'column_ids': [Command.create({
                'name': "Balance",
                'expression_label': 'balance',
            })],
            'line_ids': [Command.create({
                'name': "The line",
                'groupby': 'account_code',
                'expression_ids': [Command.create({
                    'label': 'balance',
                    'engine': 'domain',
                    'formula': [],
                    'subformula': 'sum',
                })],
            })],
        })
        options = self._generate_options(report, date, date)
        lines = report._get_lines(options)

        self.assertLinesValues(
            # pylint: disable=bad-whitespace
            lines,
            [   0,                                                                       1],
            [
                (report.line_ids.name,                                                   0),
                (f'{account_a.code} {account_a.name}',                                  50),
                (f'{counterpart_account_a.code} {counterpart_account_a.name}',         -20),
                (account_b2.code,                                                       70),
                ('Undefined',                                                         -100),
            ],
            options,
        )
