from __future__ import annotations

import sys
import threading
import types as _types
import typing
from types import SimpleNamespace
from typing import TYPE_CHECKING, Any, cast
from unittest.mock import create_autospec

import pytest
from sphinx.config import Config

from sphinx_autodoc_typehints import format_annotation
from sphinx_autodoc_typehints._intersphinx import build_type_mapping

if TYPE_CHECKING:
    from sphinx.environment import BuildEnvironment


def _make_env(inventory_data: dict[str, dict[str, object]] | None = None) -> BuildEnvironment:
    if inventory_data is None:
        return cast("BuildEnvironment", SimpleNamespace())
    return cast("BuildEnvironment", SimpleNamespace(intersphinx_inventory=inventory_data))


def test_build_type_mapping_threading_local() -> None:
    inventory_data: dict[str, dict[str, Any]] = {
        "py:class": {"threading.local": ("", "", "", "")},
    }
    result = build_type_mapping(_make_env(inventory_data))
    assert result["_thread._local"] == "threading.local"


def test_build_type_mapping_no_intersphinx_inventory() -> None:
    assert build_type_mapping(_make_env()) == {}


def test_build_type_mapping_skips_already_documented() -> None:
    inventory_data: dict[str, dict[str, Any]] = {
        "py:class": {
            "threading.local": ("", "", "", ""),
            "_thread._local": ("", "", "", ""),
        },
    }
    result = build_type_mapping(_make_env(inventory_data))
    assert "_thread._local" not in result


def test_build_type_mapping_skips_unimportable() -> None:
    inventory_data: dict[str, dict[str, Any]] = {
        "py:class": {"nonexistent_module.SomeClass": ("", "", "", "")},
    }
    assert build_type_mapping(_make_env(inventory_data)) == {}


def test_build_type_mapping_skips_missing_attr() -> None:
    inventory_data: dict[str, dict[str, Any]] = {
        "py:class": {"threading.NoSuchAttribute": ("", "", "", "")},
    }
    assert build_type_mapping(_make_env(inventory_data)) == {}


def test_build_type_mapping_skips_no_qualname(monkeypatch: pytest.MonkeyPatch) -> None:
    obj_no_qualname = SimpleNamespace(__module__="os")
    monkeypatch.setattr("os.getcwd", obj_no_qualname)
    inventory_data: dict[str, dict[str, Any]] = {
        "py:function": {"os.getcwd": ("", "", "", "")},
    }
    assert build_type_mapping(_make_env(inventory_data)) == {}


def test_build_type_mapping_skips_same_path() -> None:
    inventory_data: dict[str, dict[str, Any]] = {
        "py:class": {"int": ("", "", "", "")},
    }
    result = build_type_mapping(_make_env(inventory_data))
    assert "builtins.int" not in result


def test_build_type_mapping_multiple_roles() -> None:
    inventory_data: dict[str, dict[str, Any]] = {
        "py:class": {"threading.local": ("", "", "", "")},
        "py:function": {},
        "py:exception": {},
    }
    result = build_type_mapping(_make_env(inventory_data))
    assert result["_thread._local"] == "threading.local"


def test_build_type_mapping_skips_builtin_alias() -> None:
    inventory_data: dict[str, dict[str, Any]] = {
        "py:class": {
            "dict": ("", "", "", ""),
            "wsgiref.types.WSGIEnvironment": ("", "", "", ""),
        },
    }
    result = build_type_mapping(_make_env(inventory_data))
    assert "builtins.dict" not in result


def test_format_annotation_applies_intersphinx_mapping() -> None:
    conf = create_autospec(
        Config,
        _intersphinx_type_mapping={"_thread._local": "threading.local"},
        always_use_bars_union=False,
    )
    result = format_annotation(threading.local, conf)
    assert result == ":py:class:`~threading.local`"


def test_format_annotation_without_mapping() -> None:
    conf = create_autospec(Config, always_use_bars_union=False)
    result = format_annotation(threading.local, conf)
    assert result == ":py:class:`~_thread._local`"


@pytest.mark.parametrize(
    "alias",
    [
        pytest.param(bytes | memoryview, id="X|Y"),
        pytest.param(typing.Union[bytes, memoryview], id="Union[X,Y]"),
    ],
)
def test_build_type_mapping_skips_union_alias(monkeypatch: pytest.MonkeyPatch, alias: object) -> None:
    # Union instances all resolve to the same union-type class path, so including
    # them in the mapping would remap every union annotation in the docs.
    mypkg = _types.ModuleType("mypkg")
    mypkg.MyAlias = alias  # type: ignore[attr-defined]
    monkeypatch.setitem(sys.modules, "mypkg", mypkg)
    inventory_data: dict[str, dict[str, Any]] = {"py:data": {"mypkg.MyAlias": ("", "", "", "")}}
    result = build_type_mapping(_make_env(inventory_data))
    assert "types.UnionType" not in result
    assert "typing.Union" not in result
