"""`app.keywords.countries` (spec « Multi-sources et cloisonnement pays » §4.1, §4.4).

Trois adaptateurs et le pipeline d'import lisent ce module une fois par ligne : c'est lui qui
décide quelles lignes d'un fichier entrent dans une cible et lesquelles sont ignorées. Une
erreur ici ne casse rien de visible — elle verse des mots-clés dans le mauvais pays, ce qu'on
ne découvre qu'à la lecture des données.

Les cas ci-dessous sont donc de deux sortes : ceux du §6 de la spec, et ceux qui figent un
comportement **mesuré** de `pycountry` / Babel que rien d'autre ne rattraperait.
"""

import re

import pycountry
import pytest

from app.keywords.countries import ALIASES, AMBIGUOUS, label, resolve, suggested_language


@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        # §6 : les cas nommés par la spec.
        ("United States", "US"),
        ("uk", "GB"),
        ("Hong Kong", "HK"),
        ("New York", None),
        # Casse et espaces indifférents. `pycountry` ne détoure pas : sans le `.strip()`
        # explicite de `resolve`, « '  united states  ' » lève LookupError.
        ("  united states  ", "US"),
        ("UNITED STATES", "US"),
        ("\tUnited States\n", "US"),
        ("UK", "GB"),
        ("Uk", "GB"),
        # Un code alpha-2 déjà écrit passe tel quel — c'est la forme d'un futur export
        # Semrush Positions (`Database = us`) autant que celle de notre propre sortie.
        ("us", "US"),
        ("US", "US"),
        ("gb", "GB"),
        # Autres pays du fichier de onze pays du §1.
        ("Singapore", "SG"),
        ("Israel", "IL"),
        ("Japan", "JP"),
        ("Germany", "DE"),
        ("Australia", "AU"),
        ("New Zealand", "NZ"),
        ("Canada", "CA"),
        ("United Kingdom", "GB"),
        # Alias obligatoires : mesuré, `pycountry` lève LookupError sur les trois.
        ("Turkey", "TR"),
        ("Russia", "RU"),
        # Vide, blanc, absent : jamais une erreur, jamais un pays.
        ("", None),
        ("   ", None),
        (None, None),
        # Localités et libellés inconnus : rendus tels quels au rapport, jamais devinés.
        ("Paris", None),
        ("Californie", None),
        ("Zzzzz", None),
    ],
)
def test_resolve(raw: str | None, expected: str | None) -> None:
    assert resolve(raw) == expected


@pytest.mark.parametrize("raw", ["Georgia", "georgia", "  GEORGIA ", "Jersey", "jersey"])
def test_ambiguous_localities_are_refused(raw: str) -> None:
    """Le faux positif le plus coûteux : `pycountry` résout ces libellés, on les refuse.

    Mesuré : `lookup("Georgia")` rend `GE` et `lookup("Jersey")` rend `JE`. Sur une source qui
    liste des localités (Mangools/KWFinder), l'État américain deviendrait la Géorgie et les
    lignes partiraient dans une cible qui n'a rien à voir — sans une ligne de rapport. Ils
    tombent donc dans « pays non reconnu », qui est visible.
    """
    assert pycountry.countries.lookup(raw.strip()) is not None  # pycountry, lui, résout
    assert resolve(raw) is None


def test_korea_is_not_guessed() -> None:
    """« Korea » désigne deux pays : le résoudre serait deviner (§4.1)."""
    assert resolve("Korea") is None
    assert resolve("South Korea") == "KR"
    assert resolve("North Korea") == "KP"


def test_resolve_is_idempotent_on_its_own_output() -> None:
    """Le code rendu se re-résout en lui-même : le pipeline peut relire ce qu'il a écrit."""
    for raw in ("United States", "uk", "Hong Kong", "Turkey", "Germany"):
        code = resolve(raw)
        assert code is not None
        assert resolve(code) == code


def test_every_iso_country_resolves_to_itself() -> None:
    """Le nom officiel de chacun des 249 pays ISO rend son propre code — sauf les refusés.

    C'est le garde-fou de la table `AMBIGUOUS` : elle est la SEULE exception tolérée. Y ajouter
    un libellé qui n'est pas un vrai faux positif ferait tomber ce test.
    """
    for country in pycountry.countries:
        got = resolve(country.name)
        if country.name.strip().casefold() in AMBIGUOUS:
            assert got is None, f"{country.name} est dans AMBIGUOUS mais résout en {got}"
        else:
            assert got == country.alpha_2, f"{country.name} → {got}, attendu {country.alpha_2}"


def test_resolve_always_returns_an_upper_alpha_2() -> None:
    """`ck_site_targets_country` (`^[A-Z]{2}$`) : hors gabarit, la base lèverait une 23514."""
    for country in pycountry.countries:
        for raw in (country.name, country.name.upper(), f"  {country.name.lower()}  "):
            code = resolve(raw)
            assert code is None or re.fullmatch(r"[A-Z]{2}", code)


def test_alias_table_is_normalized_and_necessary() -> None:
    """Une clé mal normalisée serait morte, une valeur fausse serait pire (§4.1)."""
    for key, code in ALIASES.items():
        assert key == key.strip().casefold(), f"clé non normalisée : {key!r}"
        assert re.fullmatch(r"[A-Z]{2}", code), f"code hors gabarit : {code!r}"
        assert pycountry.countries.get(alpha_2=code) is not None, f"code inconnu : {code!r}"
        # Un alias qui ne rattrape rien est un alias à supprimer.
        with pytest.raises(LookupError):
            pycountry.countries.lookup(key)


def test_ambiguous_table_is_normalized() -> None:
    for key in AMBIGUOUS:
        assert key == key.strip().casefold(), f"clé non normalisée : {key!r}"


@pytest.mark.parametrize(
    ("code", "expected"),
    [
        # Les pays du fichier du §1 : la langue proposée doit être celle qu'on attend.
        ("US", "en"),
        ("GB", "en"),
        ("AU", "en"),
        ("NZ", "en"),
        ("CA", "en"),
        ("SG", "en"),
        ("DE", "de"),
        ("JP", "ja"),
        ("IL", "he"),
        ("HK", "zh"),
        ("FR", "fr"),
        ("ES", "es"),
        # Casse indifférente à l'entrée, minuscules en sortie.
        ("us", "en"),
        ("  de  ", "de"),
    ],
)
def test_suggested_language(code: str, expected: str) -> None:
    assert suggested_language(code) == expected


def test_suggested_language_refuses_what_the_check_would_refuse() -> None:
    """CLDR rend parfois un code hors gabarit : on ne propose alors rien.

    Mesuré : les *likely subtags* rendent `fil` pour PH. `ck_site_targets_language`
    (`^[a-z]{2}$`) le refuserait à l'insertion. Mieux vaut ne rien pré-remplir et laisser
    l'utilisateur choisir que proposer une valeur qui fera échouer la création de la cible.
    """
    assert suggested_language("PH") is None


@pytest.mark.parametrize("code", ["", "   ", "X", "USA", "New York", "12", "u1", "XX", "QQ"])
def test_suggested_language_returns_none_on_garbage(code: str) -> None:
    assert suggested_language(code) is None


def test_suggested_language_rejects_the_babel_four_letter_trap() -> None:
    """`Locale.parse("und_None")` rend « en » : Babel lit « None » comme un code de script.

    Sans le garde `^[A-Za-z]{2}$` à l'ENTRÉE, une valeur aberrante proposerait l'anglais avec
    aplomb. Le garde de sortie seul ne l'attraperait pas — « en » est un code parfaitement
    valide.
    """
    assert suggested_language("None") is None
    assert suggested_language("Latn") is None


def test_suggested_language_is_always_a_lower_alpha_2_or_none() -> None:
    """`ck_site_targets_language` (`^[a-z]{2}$`) sur les 249 pays ISO, pas sur un échantillon."""
    for country in pycountry.countries:
        language = suggested_language(country.alpha_2)
        assert language is None or re.fullmatch(r"[a-z]{2}", language)


@pytest.mark.parametrize(
    ("code", "expected"),
    [
        ("US", "États-Unis"),
        ("us", "États-Unis"),
        ("GB", "Royaume-Uni"),
        ("DE", "Allemagne"),
        ("JP", "Japon"),
        ("IL", "Israël"),
        ("NZ", "Nouvelle-Zélande"),
        # Repli sur le code brut : le web affiche quelque chose, jamais « undefined ».
        ("QQ", "QQ"),
        ("", ""),
    ],
)
def test_label(code: str, expected: str) -> None:
    assert label(code) == expected


def test_label_names_every_iso_country() -> None:
    """Aucun des 249 pays ne tombe sur le repli : le web n'affiche jamais un code nu.

    C'est la raison d'être de la fonction — `web/lib/locales.ts` ne connaît que 30 pays et
    remplace silencieusement tout code inconnu par `FR`.
    """
    for country in pycountry.countries:
        assert label(country.alpha_2) != country.alpha_2, f"{country.alpha_2} sans nom français"
