#
# Copyright (C) 2013  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
import locale as locale_mod
import unittest
import pytest

from unittest.mock import call, patch, MagicMock
from io import StringIO

from pyanaconda import localization
from pyanaconda.core.constants import DEFAULT_LANG
from pyanaconda.core.util import execWithCapture

# pylint: disable=environment-modify
# required due to mocking os.environ

class LangcodeLocaleParsingTests(unittest.TestCase):

    def tearDown(self):
        locale_mod.setlocale(locale_mod.LC_ALL, DEFAULT_LANG)

    def test_is_valid(self):

        assert localization.is_valid_langcode("en")
        assert localization.is_valid_langcode("eo")
        assert localization.is_valid_langcode("en_US")
        assert localization.is_valid_langcode("csb")
        assert localization.is_valid_langcode("cs_CZ.UTF-8@latin")
        assert localization.is_valid_langcode("ca_ES.UTF-8@valencia")

        assert localization.is_valid_langcode("cs_JA.iso8859-1@cyrl")
        # nonsensical but still valid - parses correctly and has a language name

        assert not localization.is_valid_langcode("blah")
        assert not localization.is_valid_langcode("")
        assert not localization.is_valid_langcode(None)

    def test_invalid_raise_decorator(self):
        with pytest.raises(localization.InvalidLocaleSpec):
            localization.get_native_name("blah")
        with pytest.raises(localization.InvalidLocaleSpec):
            localization.is_supported_locale("blah")

    def test_is_supported(self):
        assert localization.is_supported_locale("en")
        assert localization.is_supported_locale("en_US")
        assert localization.locale_supported_in_console("en")
        assert localization.locale_supported_in_console("en_US")

    def test_native_name(self):
        assert localization.get_native_name("de") == "Deutsch"
        assert localization.get_native_name("cs_CZ") == "Čeština (Česko)"

    def test_english_name(self):
        assert localization.get_english_name("de") == "German"
        assert localization.get_english_name("cs_CZ") == "Czech (Czechia)"

    def test_available_translations(self):
        assert "en" in localization.get_available_translations()

    def test_territory_locales(self):
        assert "en_US.UTF-8" in localization.get_territory_locales("US")
        assert "en_GB.UTF-8" in localization.get_territory_locales("GB")

    def test_locale_keyboards(self):
        assert localization.get_locale_keyboards("en_US") == ["us"]
        assert localization.get_locale_keyboards("en_GB") == ["gb"]

    def test_common_keyboard_layouts(self):
        layouts = localization.get_common_keyboard_layouts()
        assert "us" in layouts
        assert "fr(oss)" in layouts
        assert "de(nodeadkeys)" in layouts

    def test_locale_timezones(self):
        assert "Europe/Oslo" in localization.get_locale_timezones("no")

    @patch.dict("pyanaconda.localization.os.environ", dict())
    def test_xlated_tz(self):
        localization.os.environ["LANG"] = "en_US"
        assert "Europe/Barcelona" == localization.get_xlated_timezone("Europe/Barcelona")
        localization.os.environ["LANG"] = "cs_CZ"
        assert "Evropa/Praha" == localization.get_xlated_timezone("Europe/Prague")
        localization.os.environ["LANG"] = "blah"
        with pytest.raises(localization.InvalidLocaleSpec):
            localization.get_xlated_timezone("America/New_York")


class SetupLocaleTest(unittest.TestCase):

    def tearDown(self):
        locale_mod.setlocale(locale_mod.LC_ALL, DEFAULT_LANG)

    @patch("pyanaconda.localization.setenv")
    @patch("pyanaconda.localization.locale_mod.setlocale")
    @patch("pyanaconda.localization.set_modules_locale")
    def test_setup_locale_notext(self, set_modules_locale_mock, setlocale_mock, setenv_mock):
        """Test setup_locale in GUI mode"""

        loc_proxy = MagicMock()

        locale = localization.setup_locale("sk", localization_proxy=loc_proxy)

        assert loc_proxy.Language == "sk"
        setenv_mock.assert_called_once_with("LANG", "sk")
        setlocale_mock.assert_called_once_with(locale_mod.LC_ALL, "sk")
        set_modules_locale_mock.assert_called_once_with("sk")

        assert locale == "sk"

    @patch.dict("pyanaconda.localization.os.environ", dict())
    @patch("pyanaconda.localization.locale_supported_in_console", return_value=False)
    @patch("pyanaconda.localization.setenv")
    @patch("pyanaconda.localization.locale_mod.setlocale")
    @patch("pyanaconda.localization.set_modules_locale")
    def test_setup_locale_text(self, set_modules_locale_mock, setlocale_mock, setenv_mock,
                               locale_supported_in_console_mock):
        """Test setup_locale in TUI mode"""
        # note: to eliminate unpredictable support in console, mocking such that it always fails

        locale = localization.setup_locale("ja_JP", text_mode=True)

        locale_supported_in_console_mock.assert_called_once_with("ja_JP")
        assert localization.os.environ["LANG"] == DEFAULT_LANG
        setenv_mock.assert_called_once_with("LANG", DEFAULT_LANG)
        setlocale_mock.assert_called_once_with(locale_mod.LC_ALL, DEFAULT_LANG)
        set_modules_locale_mock.assert_called_once_with(DEFAULT_LANG)

        assert locale == DEFAULT_LANG

    @patch("pyanaconda.localization.setenv")
    @patch("pyanaconda.localization.locale_mod.setlocale", side_effect=[locale_mod.Error, None])
    @patch("pyanaconda.localization.set_modules_locale")
    def test_setup_locale_setlocale_fail(self, set_modules_locale_mock, setlocale_mock, setenv_mock):
        """Test setup_locale with failure in setlocale"""

        locale = localization.setup_locale("es_ES")

        setenv_mock.assert_has_calls([
            call("LANG", "es_ES"),
            call("LANG", DEFAULT_LANG)
        ])
        setlocale_mock.assert_has_calls([
            call(locale_mod.LC_ALL, "es_ES"),
            call(locale_mod.LC_ALL, DEFAULT_LANG)
        ])
        setlocale_mock.assert_called_with(locale_mod.LC_ALL, DEFAULT_LANG)
        set_modules_locale_mock.assert_called_once_with(DEFAULT_LANG)

        assert locale == DEFAULT_LANG


class SetupLocaleEnvironmentTest(unittest.TestCase):

    @patch("pyanaconda.localization.get_language_locales")
    @patch.dict("pyanaconda.localization.os.environ",
                {"LANGUAGE": "de", "LANG": "de", "LC_ALL": "de", "LC_MESSAGES": "de"})
    def test_setup_locale_environment_param_ok(self, locales_mock):
        """Test setup_locale_environment() with parameter"""
        # success case
        locales_mock.return_value = ["fr_FR.UTF-8"]

        localization.setup_locale_environment("fr")

        assert "fr_FR.UTF-8" == localization.os.environ["LANG"]
        assert "LANGUAGE" not in localization.os.environ
        assert "LC_MESSAGES" not in localization.os.environ
        assert "LC_ALL" not in localization.os.environ

        # mock a failure
        locales_mock.return_value = []
        locales_mock.side_effect = localization.InvalidLocaleSpec

        localization.setup_locale_environment("iu")

        assert DEFAULT_LANG in localization.os.environ["LANG"]

    @patch.dict("pyanaconda.localization.os.environ", dict())
    def test_setup_locale_environment_vars(self):
        """Test setup_locale_environment() with multiple environment variables"""
        for varname in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"):

            localization.os.environ.clear()
            localization.os.environ[varname] = "ko"

            localization.setup_locale_environment(None)

            assert "ko_KR.UTF-8" == localization.os.environ["LANG"]
            if varname != "LANG":
                assert varname not in localization.os.environ

    @patch.dict("pyanaconda.localization.os.environ", {"LANG": "blah"})
    def test_setup_locale_environment_vars_invalid(self):
        """Test setup_locale_environment() with invalid environment variable input"""
        localization.setup_locale_environment(None)

        assert DEFAULT_LANG == localization.os.environ["LANG"]

    @patch("pyanaconda.localization.open")
    @patch.dict("pyanaconda.localization.os.environ", dict())
    def test_setup_locale_environment_fallback_efi_ok(self, open_mock):
        """Test setup_locale_environment() fallback to EFI vars"""
        # success with valid data
        # first 4 bytes binary attributes, then language with - instead of _, minimum 10 bytes
        open_mock.return_value = StringIO("\x07\x00\x00\x00de-DE\x00")
        localization.os.environ.clear()

        localization.setup_locale_environment(None)

        assert open_mock.called
        assert "de" in localization.os.environ["LANG"]

    @patch("pyanaconda.localization.open")
    @patch.dict("pyanaconda.localization.os.environ", dict())
    def test_setup_locale_environment_fallback_efi_bad(self, open_mock):
        """Test setup_locale_environment() fallback to EFI vars with bad contents"""
        # failure with invalid data - too short
        open_mock.return_value = StringIO("\x00")

        localization.setup_locale_environment(None)

        assert DEFAULT_LANG == localization.os.environ["LANG"]


class LangcodeLocaleMatchingTests(unittest.TestCase):

    def tearDown(self):
        locale_mod.setlocale(locale_mod.LC_ALL, DEFAULT_LANG)

    def test_find_best_locale_match(self):
        """Finding best locale matches should work as expected."""
        # can find best matches
        assert localization.find_best_locale_match("cs_CZ", ["cs", "cs_CZ", "en", "en_US"]) == "cs_CZ"
        assert localization.find_best_locale_match("cs", ["cs_CZ", "cs", "en", "en_US"]) == "cs"
        assert localization.find_best_locale_match("pt_BR", ["pt", "pt_BR"]) == "pt_BR"
        assert localization.find_best_locale_match("pt_BR", ["pt", "pt_BR", "pt_PT"]) == "pt_BR"
        assert localization.find_best_locale_match("cs_CZ.UTF-8", ["cs", "cs_CZ", "cs_CZ.UTF-8"]) == \
            "cs_CZ.UTF-8"
        assert localization.find_best_locale_match("cs_CZ.UTF-8@latin",
                                                   ["cs", "cs_CZ@latin", "cs_CZ.UTF-8"]) == "cs_CZ@latin"

        # no matches
        assert localization.find_best_locale_match("pt_BR", ["en_BR", "en"]) is None
        assert localization.find_best_locale_match("cs_CZ.UTF-8", ["en", "en.UTF-8"]) is None

        # nonsense
        assert localization.find_best_locale_match("ja", ["blah"]) is None
        assert localization.find_best_locale_match("blah", ["en_US.UTF-8"]) is None

    def test_find_best_locale_match_posix(self):
        """Finding best POSIX matches should work as expected."""
        match = localization.find_best_locale_match("C", ["C.UTF-8"])
        assert match == "C.UTF-8"

        match = localization.find_best_locale_match("C.UTF-8", ["en_US"])
        assert match == "en_US"

        match = localization.find_best_locale_match("en_US", ["C.UTF-8"])
        assert match is None

        match = localization.find_best_locale_match("cs_CZ", ["C.UTF-8"])
        assert match is None

    def test_resolve_date_format(self):
        """All locales' date formats should be properly resolved."""
        locales = (line.strip() for line in execWithCapture("locale", ["-a"]).splitlines())
        for locale in locales:
            locale_mod.setlocale(locale_mod.LC_ALL, locale)
            order = localization.resolve_date_format(1, 2, 3, fail_safe=False)[0]
            for i in (1, 2, 3):
                assert i in order
