# coding=utf-8
"""Test certbot.display.ops."""
import sys
import unittest
from unittest import mock

import josepy as jose
import pytest

from acme import messages
from certbot import errors
from certbot._internal import account
from certbot._internal.display import obj as display_obj
from certbot.compat import filesystem
from certbot.compat import os
from certbot.display import ops
from certbot.display import util as display_util
import certbot.tests.util as test_util

KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))


class GetEmailTest(unittest.TestCase):
    """Tests for certbot.display.ops.get_email."""

    @classmethod
    def _call(cls, **kwargs):
        from certbot.display.ops import get_email
        return get_email(**kwargs)

    @test_util.patch_display_util()
    def test_cancel_none(self, mock_get_utility):
        mock_input = mock_get_utility().input
        mock_input.return_value = (display_util.CANCEL, "foo@bar.baz")
        with pytest.raises(errors.Error):
            self._call()
        with pytest.raises(errors.Error):
            self._call(optional=False)

    @test_util.patch_display_util()
    def test_ok_safe(self, mock_get_utility):
        mock_input = mock_get_utility().input
        mock_input.return_value = (display_util.OK, "foo@bar.baz")
        with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
            mock_safe_email.return_value = True
            assert self._call() == "foo@bar.baz"

    @test_util.patch_display_util()
    def test_ok_not_safe(self, mock_get_utility):
        mock_input = mock_get_utility().input
        mock_input.return_value = (display_util.OK, "foo@bar.baz")
        with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
            mock_safe_email.side_effect = [False, True]
            assert self._call() == "foo@bar.baz"

    @test_util.patch_display_util()
    def test_invalid_flag(self, mock_get_utility):
        invalid_txt = "There seem to be problems"
        mock_input = mock_get_utility().input
        mock_input.return_value = (display_util.OK, "foo@bar.baz")
        with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
            mock_safe_email.return_value = True
            self._call()
            assert invalid_txt not in mock_input.call_args[0][0]
            self._call(invalid=True)
            assert invalid_txt in mock_input.call_args[0][0]

    @test_util.patch_display_util()
    def test_optional_flag(self, mock_get_utility):
        mock_input = mock_get_utility().input
        mock_input.return_value = (display_util.OK, "foo@bar.baz")
        with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
            mock_safe_email.side_effect = [False, True]
            self._call(optional=False)
            for call in mock_input.call_args_list:
                assert "--register-unsafely-without-email" not in call[0][0]

    @test_util.patch_display_util()
    def test_optional_invalid_unsafe(self, mock_get_utility):
        invalid_txt = "There seem to be problems"
        mock_input = mock_get_utility().input
        mock_input.return_value = (display_util.OK, "foo@bar.baz")
        with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
            mock_safe_email.side_effect = [False, True]
            self._call(invalid=True)
            assert invalid_txt in mock_input.call_args[0][0]


class ChooseAccountTest(test_util.TempDirTestCase):
    """Tests for certbot.display.ops.choose_account."""
    def setUp(self):
        super().setUp()

        display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))

        self.account_keys_dir = os.path.join(self.tempdir, "keys")
        filesystem.makedirs(self.account_keys_dir, 0o700)

        self.config = mock.MagicMock(
            accounts_dir=self.tempdir,
            account_keys_dir=self.account_keys_dir,
            server="certbot-demo.org")
        self.key = KEY

        self.acc1 = account.Account(messages.RegistrationResource(
            uri=None, body=messages.Registration.from_data(
                email="email1@g.com")), self.key)
        self.acc2 = account.Account(messages.RegistrationResource(
            uri=None, body=messages.Registration.from_data(
                email="email2@g.com", phone="phone")), self.key)

    @classmethod
    def _call(cls, accounts):
        return ops.choose_account(accounts)

    @test_util.patch_display_util()
    def test_one(self, mock_util):
        mock_util().menu.return_value = (display_util.OK, 0)
        assert self._call([self.acc1]) == self.acc1

    @test_util.patch_display_util()
    def test_two(self, mock_util):
        mock_util().menu.return_value = (display_util.OK, 1)
        assert self._call([self.acc1, self.acc2]) == self.acc2

    @test_util.patch_display_util()
    def test_cancel(self, mock_util):
        mock_util().menu.return_value = (display_util.CANCEL, 1)
        assert self._call([self.acc1, self.acc2]) is None


class GenHttpsNamesTest(unittest.TestCase):
    """Test _gen_https_names."""
    def setUp(self):
        display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))

    @classmethod
    def _call(cls, domains):
        from certbot.display.ops import _gen_https_names
        return _gen_https_names(domains)

    def test_zero(self):
        assert self._call([]) == ""

    def test_one(self):
        doms = [
            "example.com",
            "asllkjsadfljasdf.c",
        ]
        for dom in doms:
            assert self._call([dom]) == "https://%s" % dom

    def test_two(self):
        domains_list = [
            ["foo.bar.org", "bar.org"],
            ["paypal.google.facebook.live.com", "*.zombo.example.com"],
        ]
        for doms in domains_list:
            assert self._call(doms) == \
                "https://{dom[0]} and https://{dom[1]}".format(dom=doms)

    def test_three(self):
        doms = ["a.org", "b.org", "c.org"]
        # We use an oxford comma
        assert self._call(doms) == \
            "https://{dom[0]}, https://{dom[1]}, and https://{dom[2]}".format(
                dom=doms)

    def test_four(self):
        doms = ["a.org", "b.org", "c.org", "d.org"]
        exp = ("https://{dom[0]}, https://{dom[1]}, https://{dom[2]}, "
               "and https://{dom[3]}".format(dom=doms))

        assert self._call(doms) == exp


class ChooseNamesTest(unittest.TestCase):
    """Test choose names."""
    def setUp(self):
        display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))
        self.mock_install = mock.MagicMock()

    @classmethod
    def _call(cls, installer, question=None):
        from certbot.display.ops import choose_names
        return choose_names(installer, question)

    @mock.patch("certbot.display.ops._choose_names_manually")
    def test_no_installer(self, mock_manual):
        self._call(None)
        assert mock_manual.call_count == 1

    @test_util.patch_display_util()
    def test_no_installer_cancel(self, mock_util):
        mock_util().input.return_value = (display_util.CANCEL, [])
        assert self._call(None) == []

    @test_util.patch_display_util()
    def test_no_names_choose(self, mock_util):
        self.mock_install().get_all_names.return_value = set()
        domain = "example.com"
        mock_util().input.return_value = (display_util.OK, domain)

        actual_doms = self._call(self.mock_install)
        assert mock_util().input.call_count == 1
        assert actual_doms == [domain]

    def test_sort_names_trivial(self):
        from certbot.display.ops import _sort_names

        #sort an empty list
        assert _sort_names([]) == []

        #sort simple domains
        some_domains = ["ex.com", "zx.com", "ax.com"]
        assert _sort_names(some_domains) == ["ax.com", "ex.com", "zx.com"]

        #Sort subdomains of a single domain
        domain = ".ex.com"
        unsorted_short = ["e", "a", "z", "y"]
        unsorted_long = [us + domain for us in unsorted_short]

        sorted_short = sorted(unsorted_short)
        sorted_long = [us + domain for us in sorted_short]

        assert _sort_names(unsorted_long) == sorted_long

    def test_sort_names_many(self):
        from certbot.display.ops import _sort_names

        unsorted_domains = [".cx.com", ".bx.com", ".ax.com", ".dx.com"]
        unsorted_short = ["www", "bnother.long.subdomain", "a", "a.long.subdomain", "z", "b"]
        #Of course sorted doesn't work here ;-)
        sorted_short = ["a", "b", "a.long.subdomain", "bnother.long.subdomain", "www", "z"]

        to_sort = []
        for short in unsorted_short:
            for domain in unsorted_domains:
                to_sort.append(short+domain)
        sortd = []
        for domain in sorted(unsorted_domains):
            for short in sorted_short:
                sortd.append(short+domain)
        assert _sort_names(to_sort) == sortd


    @test_util.patch_display_util()
    def test_filter_names_valid_return(self, mock_util):
        self.mock_install.get_all_names.return_value = {"example.com"}
        mock_util().checklist.return_value = (display_util.OK, ["example.com"])

        names = self._call(self.mock_install)
        assert names == ["example.com"]
        assert mock_util().checklist.call_count == 1

    @test_util.patch_display_util()
    def test_filter_namees_override_question(self, mock_util):
        self.mock_install.get_all_names.return_value = {"example.com"}
        mock_util().checklist.return_value = (display_util.OK, ["example.com"])
        names = self._call(self.mock_install, "Custom")
        assert names == ["example.com"]
        assert mock_util().checklist.call_count == 1
        assert mock_util().checklist.call_args[0][0] == "Custom"

    @test_util.patch_display_util()
    def test_filter_names_nothing_selected(self, mock_util):
        self.mock_install.get_all_names.return_value = {"example.com"}
        mock_util().checklist.return_value = (display_util.OK, [])

        assert self._call(self.mock_install) == []

    @test_util.patch_display_util()
    def test_filter_names_cancel(self, mock_util):
        self.mock_install.get_all_names.return_value = {"example.com"}
        mock_util().checklist.return_value = (
            display_util.CANCEL, ["example.com"])

        assert self._call(self.mock_install) == []

    def test_get_valid_domains(self):
        from certbot.display.ops import get_valid_domains
        all_valid = ["example.com", "second.example.com",
                     "also.example.com", "under_score.example.com",
                     "justtld", "*.wildcard.com"]
        all_invalid = ["öóòps.net", "uniçodé.com"]
        two_valid = ["example.com", "úniçøde.com", "also.example.com"]
        assert get_valid_domains(all_valid) == all_valid
        assert get_valid_domains(all_invalid) == []
        assert len(get_valid_domains(two_valid)) == 2

    @test_util.patch_display_util()
    def test_choose_manually(self, mock_util):
        from certbot.display.ops import _choose_names_manually
        utility_mock = mock_util()
        # No retry
        utility_mock.yesno.return_value = False
        # IDN and no retry
        utility_mock.input.return_value = (display_util.OK,
                                          "uniçodé.com")
        assert _choose_names_manually() == []
        # IDN exception with previous mocks
        with mock.patch(
                "certbot.display.ops.internal_display_util.separate_list_input"
        ) as mock_sli:
            unicode_error = UnicodeEncodeError('mock', u'', 0, 1, 'mock')
            mock_sli.side_effect = unicode_error
            assert _choose_names_manually() == []
        # Valid domains
        utility_mock.input.return_value = (display_util.OK,
                                          ("example.com,"
                                           "under_score.example.com,"
                                           "justtld,"
                                           "valid.example.com"))
        assert _choose_names_manually() == \
                         ["example.com", "under_score.example.com",
                          "justtld", "valid.example.com"]

    @test_util.patch_display_util()
    def test_choose_manually_retry(self, mock_util):
        from certbot.display.ops import _choose_names_manually
        utility_mock = mock_util()
        # Three iterations
        utility_mock.input.return_value = (display_util.OK,
                                          "uniçodé.com")
        utility_mock.yesno.side_effect = [True, True, False]
        _choose_names_manually()
        assert utility_mock.yesno.call_count == 3


class SuccessInstallationTest(unittest.TestCase):
    """Test the success installation message."""
    @classmethod
    def _call(cls, names):
        from certbot.display.ops import success_installation
        success_installation(names)

    @test_util.patch_display_util()
    @mock.patch("certbot.display.util.notify")
    def test_success_installation(self, mock_notify, mock_display):
        mock_display().notification.return_value = None
        names = ["example.com", "abc.com"]

        self._call(names)

        assert mock_notify.call_count == 1
        arg = mock_notify.call_args_list[0][0][0]

        for name in names:
            assert name in arg


class SuccessRenewalTest(unittest.TestCase):
    """Test the success renewal message."""
    @classmethod
    def _call(cls, names):
        from certbot.display.ops import success_renewal
        success_renewal(names)

    @test_util.patch_display_util()
    @mock.patch("certbot.display.util.notify")
    def test_success_renewal(self, mock_notify, mock_display):
        mock_display().notification.return_value = None
        names = ["example.com", "abc.com"]

        self._call(names)

        assert mock_notify.call_count == 1


class SuccessRevocationTest(unittest.TestCase):
    """Test the success revocation message."""
    @classmethod
    def _call(cls, path):
        from certbot.display.ops import success_revocation
        success_revocation(path)

    @test_util.patch_display_util()
    @mock.patch("certbot.display.util.notify")
    def test_success_revocation(self, mock_notify, unused_mock_display):
        path = "/path/to/cert.pem"
        self._call(path)
        mock_notify.assert_called_once_with(
            "Congratulations! You have successfully revoked the certificate "
            "that was located at {0}.".format(path)
        )


class ValidatorTests(unittest.TestCase):
    """Tests for `validated_input` and `validated_directory`."""

    __ERROR = "Must be non-empty"

    valid_input = "asdf"
    valid_directory = "/var/www/html"

    @staticmethod
    def __validator(m):
        if m == "":
            raise errors.PluginError(ValidatorTests.__ERROR)

    @test_util.patch_display_util()
    def test_input_blank_with_validator(self, mock_util):
        mock_util().input.side_effect = [(display_util.OK, ""),
                                         (display_util.OK, ""),
                                         (display_util.OK, ""),
                                         (display_util.OK, self.valid_input)]

        returned = ops.validated_input(self.__validator, "message", force_interactive=True)
        assert ValidatorTests.__ERROR == mock_util().notification.call_args[0][0]
        assert returned == (display_util.OK, self.valid_input)

    @test_util.patch_display_util()
    def test_input_validation_with_default(self, mock_util):
        mock_util().input.side_effect = [(display_util.OK, self.valid_input)]

        returned = ops.validated_input(self.__validator, "msg", default="other")
        assert returned == (display_util.OK, self.valid_input)

    @test_util.patch_display_util()
    def test_input_validation_with_bad_default(self, mock_util):
        mock_util().input.side_effect = [(display_util.OK, self.valid_input)]

        with pytest.raises(AssertionError):
            ops.validated_input(self.__validator, "msg", default="")

    @test_util.patch_display_util()
    def test_input_cancel_with_validator(self, mock_util):
        mock_util().input.side_effect = [(display_util.CANCEL, "")]

        code, unused_raw = ops.validated_input(self.__validator, "message", force_interactive=True)
        assert code == display_util.CANCEL

    @test_util.patch_display_util()
    def test_directory_select_validation(self, mock_util):
        mock_util().directory_select.side_effect = [(display_util.OK, ""),
                                                    (display_util.OK, self.valid_directory)]

        returned = ops.validated_directory(self.__validator, "msg", force_interactive=True)
        assert ValidatorTests.__ERROR == mock_util().notification.call_args[0][0]
        assert returned == (display_util.OK, self.valid_directory)

    @test_util.patch_display_util()
    def test_directory_select_validation_with_default(self, mock_util):
        mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)]

        returned = ops.validated_directory(self.__validator, "msg", default="other")
        assert returned == (display_util.OK, self.valid_directory)

    @test_util.patch_display_util()
    def test_directory_select_validation_with_bad_default(self, mock_util):
        mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)]

        with pytest.raises(AssertionError):
            ops.validated_directory(self.__validator, "msg", default="")


class ChooseValuesTest(unittest.TestCase):
    """Test choose_values."""
    @classmethod
    def _call(cls, values, question):
        from certbot.display.ops import choose_values
        return choose_values(values, question)

    @test_util.patch_display_util()
    def test_choose_names_success(self, mock_util):
        items = ["first", "second", "third"]
        mock_util().checklist.return_value = (display_util.OK, [items[2]])
        result = self._call(items, None)
        assert result == [items[2]]
        assert mock_util().checklist.called is True
        assert mock_util().checklist.call_args[0][0] == ""

    @test_util.patch_display_util()
    def test_choose_names_success_question(self, mock_util):
        items = ["first", "second", "third"]
        question = "Which one?"
        mock_util().checklist.return_value = (display_util.OK, [items[1]])
        result = self._call(items, question)
        assert result == [items[1]]
        assert mock_util().checklist.called is True
        assert mock_util().checklist.call_args[0][0] == question

    @test_util.patch_display_util()
    def test_choose_names_user_cancel(self, mock_util):
        items = ["first", "second", "third"]
        question = "Want to cancel?"
        mock_util().checklist.return_value = (display_util.CANCEL, [])
        result = self._call(items, question)
        assert result == []
        assert mock_util().checklist.called is True
        assert mock_util().checklist.call_args[0][0] == question


@mock.patch('certbot.display.ops.logger')
@mock.patch('certbot.display.util.notify')
class ReportExecutedCommand(unittest.TestCase):
    """Test report_executed_command"""
    @classmethod
    def _call(cls, cmd_name: str, rc: int, out: str, err: str):
        from certbot.display.ops import report_executed_command
        report_executed_command(cmd_name, rc, out, err)

    def test_mixed_success(self, mock_notify, mock_logger):
        self._call("some-hook", 0, "Did a thing", "Some warning")
        assert mock_logger.warning.call_count == 1
        assert mock_notify.call_count == 1

    def test_mixed_error(self, mock_notify, mock_logger):
        self._call("some-hook", -127, "Did a thing", "Some warning")
        assert mock_logger.warning.call_count == 2
        assert mock_notify.call_count == 1

    def test_empty_success(self, mock_notify, mock_logger):
        self._call("some-hook", 0, "\n", " ")
        assert mock_logger.warning.call_count == 0
        assert mock_notify.call_count == 0

if __name__ == "__main__":
    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover
