# Copyright 2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

import os
import tempfile
from unittest import mock

import odoo
from odoo.modules import get_module_path
from odoo.tests import TransactionCase

from ..addon_hash import addon_hash
from ..models.module import DEFAULT_EXCLUDE_PATTERNS, IncompleteUpgradeError

MODULE_NAME = "module_auto_update"


class TestModule(TransactionCase):
    def setUp(self):
        super().setUp()
        self.own_module = self.env["ir.module.module"].search(
            [("name", "=", MODULE_NAME)]
        )
        self.own_dir_path = get_module_path(MODULE_NAME)
        keep_langs = self.env["res.lang"].search([]).mapped("code")
        self.own_checksum = addon_hash(
            self.own_dir_path,
            exclude_patterns=DEFAULT_EXCLUDE_PATTERNS.split(","),
            keep_langs=keep_langs,
        )
        self.own_writeable = os.access(self.own_dir_path, os.W_OK)

    def test_compute_checksum_dir(self):
        """It should compute the directory's SHA-1 hash"""
        self.assertEqual(
            self.own_module._get_checksum_dir(),
            self.own_checksum,
            "Module directory checksum not computed properly",
        )

    def test_compute_checksum_dir_ignore_excluded(self):
        """It should exclude .pyc/.pyo extensions from checksum
        calculations"""
        if not self.own_writeable:
            self.skipTest("Own directory not writeable")
        with tempfile.NamedTemporaryFile(suffix=".pyc", dir=self.own_dir_path):
            self.assertEqual(
                self.own_module._get_checksum_dir(),
                self.own_checksum,
                "SHA1 checksum does not ignore excluded extensions",
            )

    def test_compute_checksum_dir_recomputes_when_file_added(self):
        """It should return a different value when a non-.pyc/.pyo file is
        added to the module directory"""
        if not self.own_writeable:
            self.skipTest("Own directory not writeable")
        with tempfile.NamedTemporaryFile(suffix=".py", dir=self.own_dir_path):
            self.assertNotEqual(
                self.own_module._get_checksum_dir(),
                self.own_checksum,
                "SHA1 checksum not recomputed",
            )

    def test_saved_checksums(self):
        Imm = self.env["ir.module.module"]
        base_module = Imm.search([("name", "=", "base")])
        self.assertEqual(base_module.state, "installed")
        self.assertFalse(Imm._get_saved_checksums())
        Imm._save_installed_checksums()
        saved_checksums = Imm._get_saved_checksums()
        self.assertTrue(saved_checksums)
        self.assertTrue(saved_checksums["base"])

    def test_get_modules_with_changed_checksum(self):
        Imm = self.env["ir.module.module"]
        self.assertTrue(Imm._get_modules_with_changed_checksum())
        Imm._save_installed_checksums()
        self.assertFalse(Imm._get_modules_with_changed_checksum())


@odoo.tests.tagged("post_install", "-at_install")
class TestModuleAfterInstall(TransactionCase):
    def setUp(self):
        super().setUp()
        Imm = self.env["ir.module.module"]
        self.own_module = Imm.search([("name", "=", MODULE_NAME)])
        self.base_module = Imm.search([("name", "=", "base")])

    def test_get_modules_partially_installed(self):
        Imm = self.env["ir.module.module"]
        self.assertTrue(self.own_module not in Imm._get_modules_partially_installed())
        self.own_module.button_upgrade()
        self.assertTrue(self.own_module in Imm._get_modules_partially_installed())
        self.own_module.button_upgrade_cancel()
        self.assertTrue(self.own_module not in Imm._get_modules_partially_installed())

    def test_upgrade_changed_checksum(self):
        Imm = self.env["ir.module.module"]
        Bmu = type(self.env["base.module.upgrade"])

        # check modules are in installed state
        installed_modules = Imm.search([("state", "=", "installed")])
        self.assertTrue(self.own_module in installed_modules)
        self.assertTrue(self.base_module in installed_modules)
        self.assertTrue(len(installed_modules) > 2)
        # change the checksum of 'base'
        Imm._save_installed_checksums()
        saved_checksums = Imm._get_saved_checksums()
        saved_checksums["base"] = False
        Imm._save_checksums(saved_checksums)
        changed_modules = Imm._get_modules_with_changed_checksum()
        self.assertEqual(len(changed_modules), 1)
        self.assertTrue(self.base_module in changed_modules)

        def upgrade_module_mock(self_model):
            upgrade_module_mock.call_count += 1
            # since we are upgrading base, all installed module
            # must have been marked to upgrade at this stage
            self.assertEqual(self.base_module.state, "to upgrade")
            self.assertEqual(self.own_module.state, "to upgrade")
            installed_modules.write({"state": "installed"})

        upgrade_module_mock.call_count = 0

        # upgrade_changed_checksum commits, so mock that
        with mock.patch.object(self.env.cr, "commit"):
            # we simulate an install by setting module states
            origin = Bmu.upgrade_module
            Bmu.upgrade_module = upgrade_module_mock
            try:
                Imm.upgrade_changed_checksum()
                self.assertEqual(upgrade_module_mock.call_count, 1)
                self.assertEqual(self.base_module.state, "installed")
                self.assertEqual(self.own_module.state, "installed")
                saved_checksums = Imm._get_saved_checksums()
                self.assertTrue(saved_checksums["base"])
                self.assertTrue(saved_checksums[MODULE_NAME])
            finally:
                Bmu.upgrade_module = origin

    def test_incomplete_upgrade(self):
        Imm = self.env["ir.module.module"]
        Bmu = type(self.env["base.module.upgrade"])

        installed_modules = Imm.search([("state", "=", "installed")])
        # change the checksum of 'base'
        Imm._save_installed_checksums()
        saved_checksums = Imm._get_saved_checksums()
        saved_checksums["base"] = False
        Imm._save_checksums(saved_checksums)

        def upgrade_module_mock(self_model):
            upgrade_module_mock.call_count += 1
            # since we are upgrading base, all installed module
            # must have been marked to upgrade at this stage
            self.assertEqual(self.base_module.state, "to upgrade")
            self.assertEqual(self.own_module.state, "to upgrade")
            installed_modules.write({"state": "installed"})
            # simulate partial upgrade
            self.own_module.write({"state": "to upgrade"})

        upgrade_module_mock.call_count = 0

        # upgrade_changed_checksum commits, so mock that
        with mock.patch.object(self.env.cr, "commit"):
            # we simulate an install by setting module states
            origin = Bmu.upgrade_module
            Bmu.upgrade_module = upgrade_module_mock
            try:
                with self.assertRaises(IncompleteUpgradeError):
                    Imm.upgrade_changed_checksum()
                self.assertEqual(upgrade_module_mock.call_count, 1)
            finally:
                Bmu.upgrade_module = origin

    def test_incomplete_upgrade_no_checkusm(self):
        Imm = self.env["ir.module.module"]
        Bmu = type(self.env["base.module.upgrade"])

        installed_modules = Imm.search([("state", "=", "installed")])
        # change the checksum of 'base'
        Imm._save_installed_checksums()
        saved_checksums = Imm._get_saved_checksums()

        Imm._save_checksums(saved_checksums)
        self.base_module.write({"state": "to upgrade"})

        def upgrade_module_mock(self_model):
            upgrade_module_mock.call_count += 1
            # since we are upgrading base, all installed module
            # must have been marked to upgrade at this stage
            self.assertEqual(self.base_module.state, "to upgrade")
            self.assertEqual(self.own_module.state, "installed")
            installed_modules.write({"state": "installed"})

        upgrade_module_mock.call_count = 0

        # upgrade_changed_checksum commits, so mock that
        with mock.patch.object(self.env.cr, "commit"):
            # we simulate an install by setting module states
            origin = Bmu.upgrade_module
            Bmu.upgrade_module = upgrade_module_mock
            # got just other modules to_upgrade and no checksum ones
            try:
                Imm.upgrade_changed_checksum()
                self.assertEqual(upgrade_module_mock.call_count, 1)
            finally:
                Bmu.upgrade_module = origin

    def test_nothing_to_upgrade(self):
        Imm = self.env["ir.module.module"]
        Bmu = type(self.env["base.module.upgrade"])

        Imm._save_installed_checksums()

        def upgrade_module_mock(self_model):
            upgrade_module_mock.call_count += 1

        upgrade_module_mock.call_count = 0

        # upgrade_changed_checksum commits, so mock that
        with mock.patch.object(self.env.cr, "commit"):
            # we simulate an install by setting module states
            origin = Bmu.upgrade_module
            Bmu.upgrade_module = upgrade_module_mock
            try:
                Imm.upgrade_changed_checksum()
                self.assertEqual(upgrade_module_mock.call_count, 0)
            finally:
                Bmu.upgrade_module = origin
