# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: BSD-2-Clause
"""Test the UTF-8 locale detection functions."""

from __future__ import annotations

import dataclasses
import functools
import json
import os
import pathlib
import subprocess  # noqa: S404
import typing
from unittest import mock

import pytest

import utf8_locale


if typing.TYPE_CHECKING:
    from collections.abc import Iterable
    from typing import Final


@dataclasses.dataclass(frozen=True)
class TLangData:
    """The test data for a single preferred languages test case."""

    env: dict[str, str]
    expected: list[str]


@dataclasses.dataclass(frozen=True)
class TData:
    """The test data loaded from the JSON definitions file."""

    locales: list[str]
    languages: list[TLangData]


@functools.lru_cache
def load_test_data() -> TData:
    """Load the test data from the JSON definitions file."""
    raw: Final = json.loads(
        (pathlib.Path(__file__).absolute().parent.parent.parent / "tests/data.json").read_text(
            encoding="UTF-8",
        ),
    )
    assert raw["format"]["version"] == {"major": 0, "minor": 1}

    return TData(
        locales=raw["locales"],
        languages=[
            TLangData(env=item["env"], expected=item["expected"]) for item in raw["languages"]
        ],
    )


LANG_KEYS: Final = {"LC_ALL", "LANGUAGE"}

LANG_EXPECTED: Final = [
    (["C", "en"], "C.UTF-8"),
    (["en", "C"], "en_XX.UTF-8"),
    (["es", "bg", "*"], "it_IT.UTF-8"),
    (["en", "bg", "*"], "en_XX.UTF-8"),
    (["es", "*", "en"], "it_IT.UTF-8"),
    (["es", "*", "it"], "de_DE.UTF-8"),
    (["en", "bg", "en"], "en_XX.UTF-8"),
    (["it", "en", "it"], "it_IT.UTF-8"),
    (["xy", "yz", "xy", "en"], "en_XX.UTF-8"),
]


def check_env(env: dict[str, str]) -> None:
    """Make sure a UTF8-capable environment was setup correctly."""
    # Everything except LANG_KEYS is the same as in os.environ
    assert {key: value for key, value in env.items() if key not in LANG_KEYS} == {
        key: value for key, value in os.environ.items() if key not in LANG_KEYS
    }

    # The rest of this function makes sure that locale(1) and date(1), when
    # run in this environment, output reasonable values
    loc_all: Final = {
        fields[0]: fields[1].strip('"')
        for fields in (
            line.split("=", 1)
            for line in subprocess.check_output(
                ["locale"],  # noqa: S607
                shell=False,
                env=env,
                encoding="UTF-8",
            ).splitlines()
        )
    }
    non_lc: Final = {name for name in loc_all if not name.startswith("LC_")}
    assert non_lc.issubset({"LANG", "LANGUAGE"})
    loc: Final = {name: value for name, value in loc_all.items() if name.startswith("LC_")}
    match sorted(set(loc.values())):
        case [single] if single.lower().endswith(".utf8") or single.lower().endswith(".utf-8"):
            pass

        case other:
            pytest.fail(repr(other))

    utc_env: Final = env | {"TZ": "UTC"}
    lines: Final = subprocess.check_output(
        ["date", "-d", "@1000000000", "+%A"],  # noqa: S607
        shell=False,
        env=utc_env,
        encoding="UTF-8",
    ).splitlines()
    assert lines in [["Sunday"], ["Sonntag"], ["domenica"], ["domingo"]]


def get_mod_env() -> dict[str, str]:
    """Get a slightly modified copy of os.environ for test purposes."""
    return {
        key: value
        for key, value in os.environ.items()
        if key not in {"HOME", "USER", "PS1", "PATH"}
    } | {"TEST_KEY": "test value"}


def check_mod_env(env: dict[str, str], mod_env: dict[str, str], env2: dict[str, str]) -> None:
    """Make sure very little has changed in the prepared environment."""
    # Nothing besides LANG_KEYS has changed
    assert {key: value for key, value in env2.items() if key not in LANG_KEYS} == {
        key: value for key, value in mod_env.items() if key not in LANG_KEYS
    }

    # LANG_KEYS have changed in the same way as before
    assert {key: value for key, value in env2.items() if key in LANG_KEYS} == {
        key: value for key, value in env.items() if key in LANG_KEYS
    }


def test_utf8_env() -> None:
    """Test get_utf8_env() and, indirectly, detect_utf8_locale()."""
    env: Final = utf8_locale.get_utf8_env()
    check_env(env)

    mod_env: Final = get_mod_env()
    env2: Final = utf8_locale.get_utf8_env(mod_env)
    check_mod_env(env, mod_env, env2)


def mock_locale() -> mock._patch[mock.Mock]:
    """Mock subprocess.check_output("locale -a")."""
    locales: Final = load_test_data().locales
    mock_check_output: Final = mock.Mock(spec=["__call__"])
    mock_check_output.return_value = "".join(item + "\n" for item in locales)
    return mock.patch("subprocess.check_output", new=mock_check_output)


class TestLanguages:
    """Test the language preference handling of detect_utf8_locale()."""

    @staticmethod
    def detect_locale(languages: Iterable[str]) -> str:
        """Get the locale name using the appropriate mechanism."""
        with mock_locale():
            return utf8_locale.detect_utf8_locale(languages=languages)

    @staticmethod
    def get_vars(languages: Iterable[str]) -> dict[str, str]:
        """Get the variables dict using the appropriate mechanism."""
        with mock_locale():
            return utf8_locale.get_utf8_vars(languages=languages)

    @staticmethod
    def get_langs(env: dict[str, str]) -> list[str]:
        """Get the preferred languages using the appropriate mechanism."""
        return utf8_locale.get_preferred_languages(env)

    @pytest.mark.parametrize(("languages", "result"), LANG_EXPECTED)
    def test_language(self, languages: list[str], result: str) -> None:
        """Test detect_utf8_locale() with some languages specified."""
        assert self.detect_locale(iter(languages)) == result

    @pytest.mark.parametrize(("languages", "result"), LANG_EXPECTED)
    def test_language_vars(self, languages: list[str], result: str) -> None:
        """Test detect_utf8_locale() with some languages specified."""
        assert self.get_vars(iter(languages)) == {
            "LC_ALL": result,
            "LANGUAGE": "",
        }

    def test_no_languages(self) -> None:
        """Test detect_utf8_locale() with no languages specified."""
        with pytest.raises(utf8_locale.NoLanguagesError):
            self.detect_locale(languages=iter([]))

    @pytest.mark.parametrize("tcase", load_test_data().languages)
    def test_preferred(self, tcase: TLangData) -> None:
        """Test get_preferred_languages() with the specified environment."""
        assert self.get_langs(tcase.env) == tcase.expected
