from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.models import Model
from odoo.tests import tagged
from odoo.tests.common import Form
from odoo import fields
from odoo.exceptions import UserError
from odoo.tools import format_date


@tagged('post_install', '-at_install')
class TestAccountMoveInalterableHash(AccountTestInvoicingCommon):
    @classmethod
    def setUpClass(cls, chart_template_ref=None):
        super().setUpClass(chart_template_ref=chart_template_ref)

    def test_account_move_inalterable_hash(self):
        """Test that we cannot alter a field used for the computation of the inalterable hash"""
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        move = self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True)

        with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
            move.inalterable_hash = 'fake_hash'
        with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
            move.secure_sequence_number = 666
        with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
            move.name = "fake name"
        with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
            move.date = fields.Date.from_string('2023-01-02')
        with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
            move.company_id = 666
        with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
            move.write({
                'company_id': 666,
                'date': fields.Date.from_string('2023-01-03')
            })

        with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Account.*"):
            move.line_ids[0].account_id = move.line_ids[1]['account_id']
        with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Partner.*"):
            move.line_ids[0].partner_id = 666

        # The following fields are not part of the hash so they can be modified
        move.ref = "bla"
        move.line_ids[0].date_maturity = fields.Date.from_string('2023-01-02')

    def test_account_move_hash_integrity_report(self):
        """Test the hash integrity report"""
        moves = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
        )
        moves.action_post()

        # No records to be hashed because the restrict mode is not activated yet
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]  # First journal
        self.assertEqual(integrity_check['msg_cover'], 'This journal is not in strict mode.')

        # No records to be hashed even if the restrict mode is activated because the hashing is not retroactive
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], 'There isn\'t any journal entry flagged for data inalterability yet for this journal.')

        # Everything should be correctly hashed and verified
        new_moves = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-03", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_a, "2023-01-05", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-06", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_a, "2023-01-07", amounts=[1000, 2000])
        )
        new_moves.action_post()
        moves |= new_moves
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*')
        self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[2].date)))
        self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))

        # Let's change one of the fields used by the hash. It should be detected by the integrity report.
        # We need to bypass the write method of account.move to do so.
        Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')

        # Revert the previous change
        Model.write(moves[4], {'date': fields.Date.from_string("2023-01-05")})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*')

        # Let's try with the one of the subfields
        Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_b.id})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')

        # Let's try with the inalterable_hash field itself
        Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_a.id})  # Revert the previous change
        Model.write(moves[-1], {'inalterable_hash': 'fake_hash'})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')

    def test_account_move_hash_versioning_1(self):
        """We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
        This test focuses on the case where the user has only moves with the old hash algorithm."""
        self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True)  # Not hashed
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        moves = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-02", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
        )
        moves.with_context(hash_version=1).action_post()
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
        self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
        self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))

        # Let's change one of the fields used by the hash. It should be detected by the integrity report
        # independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
        # We need to bypass the write method of account.move to do so.
        Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')

    def test_account_move_hash_versioning_2(self):
        """We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
        This test focuses on the case where the user has only moves with the new hash algorithm."""
        self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True)  # Not hashed
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        moves = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
        )
        moves.action_post()
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
        self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
        self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))

        # Let's change one of the fields used by the hash. It should be detected by the integrity report
        # independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
        # We need to bypass the write method of account.move to do so.
        Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')

    def test_account_move_hash_versioning_v1_to_v2(self):
        """We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
        This test focuses on the case where the user has moves with both hash algorithms."""
        self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True)  # Not hashed
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        moves_v1 = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
        )
        moves_v1.with_context(hash_version=1).action_post()
        fields_v1 = moves_v1.with_context(hash_version=1)._get_integrity_hash_fields()
        moves_v2 = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
        )
        moves_v2.with_context(hash_version=2).action_post()
        fields_v2 = moves_v2._get_integrity_hash_fields()
        self.assertNotEqual(fields_v1, fields_v2)  # Make sure two different hash algorithms were used

        moves = moves_v1 | moves_v2
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
        self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
        self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))

        # Let's change one of the fields used by the hash. It should be detected by the integrity report
        # independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
        # We need to bypass the write method of account.move to do so.
        Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')

        # Let's revert the change and make sure that we cannot use the v1 after the v2.
        # This means we don't simply check whether the move is correctly hashed with either algorithms,
        # but that we can only use v2 after v1 and not go back to v1 afterwards.
        Model.write(moves[4], {'date': fields.Date.from_string("2023-01-02")})  # Revert the previous change
        moves_v1_bis = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-10", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-11", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-12", amounts=[1000, 2000])
        )
        moves_v1_bis.with_context(hash_version=1).action_post()
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves_v1_bis[0].id}.')

    def test_account_move_hash_versioning_3(self):
        """
        Version 2 does not take into account floating point representation issues.
        Test that version 3 covers correctly this case
        """
        self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
                          post=True)  # Not hashed
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        moves_v3 = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[30*0.17, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
        )
        moves_v3.action_post()

        # invalidate cache
        moves_v3[0].line_ids[0].invalidate_recordset()

        integrity_check_v3 = moves_v3.company_id._check_hash_integrity()['results'][0]
        self.assertRegex(integrity_check_v3['msg_cover'], f'Entries are hashed from {moves_v3[0].name}.*')

    def test_account_move_hash_versioning_v2_to_v3(self):
        """
        We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
        This test focuses on the case with version 2 and version 3.
        """
        self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
                          post=True)  # Not hashed
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        moves_v2 = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
        )
        moves_v2.with_context(hash_version=2).action_post()

        moves_v3 = (
            self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
            | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
        )
        moves_v3.with_context(hash_version=3).action_post()

        moves = moves_v2 | moves_v3
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
        self.assertEqual(integrity_check['first_move_date'],
                         format_date(self.env, fields.Date.to_string(moves[0].date)))
        self.assertEqual(integrity_check['last_move_date'],
                         format_date(self.env, fields.Date.to_string(moves[-1].date)))

        Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
        integrity_check = moves.company_id._check_hash_integrity()['results'][0]
        self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')

    def test_account_move_hash_with_cash_rounding(self):
        # Enable inalterable hash
        self.company_data['default_journal_sale'].restrict_mode_hash_table = True
        # Required for `invoice_cash_rounding_id` to be visible in the view
        self.env.user.groups_id += self.env.ref('account.group_cash_rounding')
        # Test 'add_invoice_line' rounding
        invoice = self.init_invoice('out_invoice', products=self.product_a+self.product_b)
        move_form = Form(invoice)
        # Add a cash rounding having 'add_invoice_line'.
        move_form.invoice_cash_rounding_id = self.cash_rounding_a
        with move_form.invoice_line_ids.edit(0) as line_form:
            line_form.price_unit = 999.99
        move_form.save()

        # Should not raise
        invoice.action_post()

        self.assertEqual(invoice.amount_total, 1410.0)
        self.assertEqual(invoice.amount_untaxed, 1200.0)
        self.assertEqual(invoice.amount_tax, 210)
        self.assertEqual(len(invoice.invoice_line_ids), 2)
        self.assertEqual(len(invoice.line_ids), 6)
