import contextlib
import mock
import os.path

import pytest

from uaclient import apt
from uaclient.entitlements.esm import ESMEntitlement
from uaclient import exceptions
from uaclient import util

M_PATH = "uaclient.entitlements.esm.ESMEntitlement."
M_REPOPATH = "uaclient.entitlements.repo."
M_GETPLATFORM = M_REPOPATH + "util.get_platform_info"


@pytest.fixture
def entitlement(entitlement_factory):
    return entitlement_factory(ESMEntitlement, suites=["trusty"])


class TestESMEntitlementEnable:
    def test_enable_configures_apt_sources_and_auth_files(self, entitlement):
        """When entitled, configure apt repo auth token, pinning and url."""
        patched_packages = ["a", "b"]
        original_exists = os.path.exists

        def fake_exists(path):
            if path == "/etc/apt/preferences.d/ubuntu-esm-infra-trusty":
                return True
            if path in (apt.APT_METHOD_HTTPS_FILE, apt.CA_CERTIFICATES_FILE):
                return True
            return original_exists(path)

        with contextlib.ExitStack() as stack:
            m_add_apt = stack.enter_context(
                mock.patch("uaclient.apt.add_auth_apt_repo")
            )
            m_add_pinning = stack.enter_context(
                mock.patch("uaclient.apt.add_ppa_pinning")
            )
            m_subp = stack.enter_context(
                mock.patch("uaclient.util.subp", return_value=("", ""))
            )
            m_can_enable = stack.enter_context(
                mock.patch.object(entitlement, "can_enable")
            )
            stack.enter_context(
                mock.patch(M_GETPLATFORM, return_value={"series": "trusty"})
            )
            stack.enter_context(
                mock.patch(
                    M_REPOPATH + "os.path.exists", side_effect=fake_exists
                )
            )
            m_unlink = stack.enter_context(
                mock.patch("uaclient.apt.os.unlink")
            )
            # Note that this patch uses a PropertyMock and happens on the
            # entitlement's type because packages is a property
            m_packages = mock.PropertyMock(return_value=patched_packages)
            stack.enter_context(
                mock.patch.object(type(entitlement), "packages", m_packages)
            )

            m_can_enable.return_value = True

            assert True is entitlement.enable()

        add_apt_calls = [
            mock.call(
                "/etc/apt/sources.list.d/ubuntu-{}-trusty.list".format(
                    entitlement.name
                ),
                "http://ESM-INFRA",
                "TOKEN",
                ["trusty"],
                entitlement.repo_key_file,
            )
        ]
        install_cmd = mock.call(
            ["apt-get", "install", "--assume-yes"] + patched_packages,
            capture=True,
            retry_sleeps=apt.APT_RETRIES,
        )

        subp_calls = [
            mock.call(
                ["apt-get", "update"],
                capture=True,
                retry_sleeps=apt.APT_RETRIES,
            ),
            install_cmd,
        ]

        assert [mock.call(silent=mock.ANY)] == m_can_enable.call_args_list
        assert add_apt_calls == m_add_apt.call_args_list
        assert 0 == m_add_pinning.call_count
        assert subp_calls == m_subp.call_args_list
        unlink_calls = [
            mock.call("/etc/apt/preferences.d/ubuntu-esm-infra-trusty")
        ]
        assert unlink_calls == m_unlink.call_args_list

    def test_enable_cleans_up_apt_sources_and_auth_files_on_error(
        self, entitlement, caplog_text
    ):
        """When setup_apt_config fails, cleanup any apt artifacts."""
        original_exists = os.path.exists

        def fake_exists(path):
            if path == "/etc/apt/preferences.d/ubuntu-esm-infra-trusty":
                return True
            if path in (apt.APT_METHOD_HTTPS_FILE, apt.CA_CERTIFICATES_FILE):
                return True
            return original_exists(path)

        def fake_subp(cmd, capture=None, retry_sleeps=None):
            if cmd == ["apt-get", "update"]:
                raise util.ProcessExecutionError(
                    "Failure", stderr="Could not get lock /var/lib/dpkg/lock"
                )
            return "", ""

        with contextlib.ExitStack() as stack:
            m_add_apt = stack.enter_context(
                mock.patch("uaclient.apt.add_auth_apt_repo")
            )
            m_add_pinning = stack.enter_context(
                mock.patch("uaclient.apt.add_ppa_pinning")
            )
            m_subp = stack.enter_context(
                mock.patch("uaclient.util.subp", side_effect=fake_subp)
            )
            m_can_enable = stack.enter_context(
                mock.patch.object(entitlement, "can_enable")
            )
            m_remove_apt_config = stack.enter_context(
                mock.patch.object(entitlement, "remove_apt_config")
            )
            stack.enter_context(
                mock.patch(M_GETPLATFORM, return_value={"series": "trusty"})
            )
            stack.enter_context(
                mock.patch(
                    M_REPOPATH + "os.path.exists", side_effect=fake_exists
                )
            )
            m_unlink = stack.enter_context(
                mock.patch("uaclient.apt.os.unlink")
            )

            m_can_enable.return_value = True

            with pytest.raises(exceptions.UserFacingError) as excinfo:
                entitlement.enable()

        add_apt_calls = [
            mock.call(
                "/etc/apt/sources.list.d/ubuntu-{}-trusty.list".format(
                    entitlement.name
                ),
                "http://ESM-INFRA",
                "TOKEN",
                ["trusty"],
                entitlement.repo_key_file,
            )
        ]
        subp_calls = [
            mock.call(
                ["apt-get", "update"],
                capture=True,
                retry_sleeps=apt.APT_RETRIES,
            )
        ]

        error_msg = "APT update failed. Another process is running APT."
        assert error_msg == excinfo.value.msg
        assert [mock.call(silent=mock.ANY)] == m_can_enable.call_args_list
        assert add_apt_calls == m_add_apt.call_args_list
        assert 0 == m_add_pinning.call_count
        assert subp_calls == m_subp.call_args_list
        unlink_calls = [
            mock.call("/etc/apt/preferences.d/ubuntu-esm-infra-trusty")
        ]
        assert unlink_calls == m_unlink.call_args_list
        assert [mock.call()] == m_remove_apt_config.call_args_list


class TestESMEntitlementDisable:
    @pytest.mark.parametrize("silent", [False, True])
    @mock.patch("uaclient.util.get_platform_info")
    @mock.patch(M_PATH + "can_disable", return_value=False)
    def test_disable_returns_false_on_can_disable_false_and_does_nothing(
        self, m_can_disable, m_platform_info, silent
    ):
        """When can_disable is false disable returns false and noops."""
        entitlement = ESMEntitlement({})

        with mock.patch("uaclient.apt.remove_auth_apt_repo") as m_remove_apt:
            assert False is entitlement.disable(silent)
        assert [mock.call(silent)] == m_can_disable.call_args_list
        assert 0 == m_remove_apt.call_count

    @mock.patch("uaclient.apt.restore_commented_apt_list_file")
    @mock.patch("uaclient.apt.remove_repo_from_apt_auth_file")
    @mock.patch(
        "uaclient.util.get_platform_info", return_value={"series": "trusty"}
    )
    @mock.patch(M_PATH + "can_disable", return_value=True)
    def test_disable_removes_apt_config(
        self,
        m_can_disable,
        m_platform_info,
        m_rm_repo_from_auth,
        m_restore_commented_apt_list_file,
        entitlement,
        tmpdir,
    ):
        """When can_disable, disable removes apt configuration"""

        with mock.patch("uaclient.entitlements.repo.apt.run_apt_command"):
            with mock.patch("uaclient.util.subp"):
                with mock.patch("uaclient.util.write_file") as m_write:
                    assert entitlement.disable(True)

        # Disable esm repo again
        write_calls = [
            mock.call(
                "/etc/apt/preferences.d/ubuntu-esm-infra-trusty",
                "Package: *\nPin: release o=UbuntuESM, n=trusty\n"
                "Pin-Priority: never\n",
            )
        ]
        assert write_calls == m_write.call_args_list
        assert [mock.call(True)] == m_can_disable.call_args_list
        auth_call = mock.call("http://ESM-INFRA")
        assert [auth_call] == m_rm_repo_from_auth.call_args_list
        assert [
            mock.call("/etc/apt/sources.list.d/ubuntu-esm-infra-trusty.list")
        ] == m_restore_commented_apt_list_file.call_args_list
