from __future__ import annotations

import importlib
import os
import sys
import typing
from tempfile import gettempdir

import pytest

from platformdirs import unix
from platformdirs.unix import Unix

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Iterator
    from pathlib import Path

    from pytest_mock import MockerFixture


@pytest.fixture(autouse=True)
def _reload_after_test() -> typing.Iterator[None]:
    yield
    importlib.reload(unix)


@pytest.mark.parametrize(
    "prop",
    [
        "user_documents_dir",
        "user_downloads_dir",
        "user_pictures_dir",
        "user_videos_dir",
        "user_music_dir",
        "user_desktop_dir",
    ],
)
def test_user_media_dir(mocker: MockerFixture, prop: str) -> None:
    example_path = "/home/example/ExampleMediaFolder"
    mock = mocker.patch("platformdirs.unix._get_user_dirs_folder")
    mock.return_value = example_path
    assert getattr(Unix(), prop) == example_path


@pytest.mark.parametrize(
    ("env_var", "prop"),
    [
        pytest.param("XDG_DOCUMENTS_DIR", "user_documents_dir", id="user_documents_dir"),
        pytest.param("XDG_DOWNLOAD_DIR", "user_downloads_dir", id="user_downloads_dir"),
        pytest.param("XDG_PICTURES_DIR", "user_pictures_dir", id="user_pictures_dir"),
        pytest.param("XDG_VIDEOS_DIR", "user_videos_dir", id="user_videos_dir"),
        pytest.param("XDG_MUSIC_DIR", "user_music_dir", id="user_music_dir"),
        pytest.param("XDG_DESKTOP_DIR", "user_desktop_dir", id="user_desktop_dir"),
    ],
)
def test_user_media_dir_env_var(mocker: MockerFixture, env_var: str, prop: str) -> None:
    # Mock media dir not being in user-dirs.dirs file
    mock = mocker.patch("platformdirs.unix._get_user_dirs_folder")
    mock.return_value = None

    example_path = "/home/example/ExampleMediaFolder"
    mocker.patch.dict(os.environ, {env_var: example_path})

    assert getattr(Unix(), prop) == example_path


@pytest.mark.parametrize(
    ("env_var", "prop", "default_abs_path"),
    [
        pytest.param("XDG_DOCUMENTS_DIR", "user_documents_dir", "/home/example/Documents", id="user_documents_dir"),
        pytest.param("XDG_DOWNLOAD_DIR", "user_downloads_dir", "/home/example/Downloads", id="user_downloads_dir"),
        pytest.param("XDG_PICTURES_DIR", "user_pictures_dir", "/home/example/Pictures", id="user_pictures_dir"),
        pytest.param("XDG_VIDEOS_DIR", "user_videos_dir", "/home/example/Videos", id="user_videos_dir"),
        pytest.param("XDG_MUSIC_DIR", "user_music_dir", "/home/example/Music", id="user_music_dir"),
        pytest.param("XDG_DESKTOP_DIR", "user_desktop_dir", "/home/example/Desktop", id="user_desktop_dir"),
    ],
)
def test_user_media_dir_default(mocker: MockerFixture, env_var: str, prop: str, default_abs_path: str) -> None:
    # Mock media dir not being in user-dirs.dirs file
    mock = mocker.patch("platformdirs.unix._get_user_dirs_folder")
    mock.return_value = None

    # Mock no XDG env variable being set
    mocker.patch.dict(os.environ, {env_var: ""})

    # Mock home directory
    mocker.patch.dict(os.environ, {"HOME": "/home/example"})
    # Mock home directory for running the test on Windows
    mocker.patch.dict(os.environ, {"USERPROFILE": "/home/example"})

    assert getattr(Unix(), prop) == default_abs_path


class XDGVariable(typing.NamedTuple):
    name: str
    default_value: str


def _func_to_path(func: str) -> XDGVariable | None:
    mapping = {
        "user_data_dir": XDGVariable("XDG_DATA_HOME", "~/.local/share"),
        "site_data_dir": XDGVariable("XDG_DATA_DIRS", f"/usr/local/share{os.pathsep}/usr/share"),
        "user_config_dir": XDGVariable("XDG_CONFIG_HOME", "~/.config"),
        "site_config_dir": XDGVariable("XDG_CONFIG_DIRS", "/etc/xdg"),
        "user_cache_dir": XDGVariable("XDG_CACHE_HOME", "~/.cache"),
        "user_state_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"),
        "user_log_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"),
        "user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", f"{gettempdir()}/runtime-1234"),
        "user_bin_dir": None,
        "site_bin_dir": None,
        "user_applications_dir": None,
        "site_applications_dir": None,
        "site_log_dir": None,
        "site_state_dir": None,
        "site_runtime_dir": XDGVariable(
            "XDG_RUNTIME_DIR", "/var/run" if sys.platform.startswith(("freebsd", "openbsd", "netbsd")) else "/run"
        ),
    }
    return mapping.get(func)


@pytest.fixture
def dirs_instance() -> Unix:
    return Unix(multipath=True, opinion=False)


@pytest.fixture
def _getuid(mocker: MockerFixture) -> None:
    mocker.patch("platformdirs.unix.getuid", return_value=1234)


@pytest.mark.usefixtures("_getuid")
def test_xdg_variable_not_set(monkeypatch: pytest.MonkeyPatch, dirs_instance: Unix, func: str) -> None:
    xdg_variable = _func_to_path(func)
    if xdg_variable is None:
        return

    monkeypatch.delenv(xdg_variable.name, raising=False)
    result = getattr(dirs_instance, func)
    assert result == os.path.expanduser(xdg_variable.default_value)  # noqa: PTH111


@pytest.mark.usefixtures("_getuid")
def test_xdg_variable_empty_value(monkeypatch: pytest.MonkeyPatch, dirs_instance: Unix, func: str) -> None:
    xdg_variable = _func_to_path(func)
    if xdg_variable is None:
        return

    monkeypatch.setenv(xdg_variable.name, "")
    result = getattr(dirs_instance, func)
    assert result == os.path.expanduser(xdg_variable.default_value)  # noqa: PTH111


@pytest.mark.usefixtures("_getuid")
def test_xdg_variable_custom_value(monkeypatch: pytest.MonkeyPatch, dirs_instance: Unix, func: str) -> None:
    xdg_variable = _func_to_path(func)
    if xdg_variable is None:
        return

    monkeypatch.setenv(xdg_variable.name, "/custom-dir")
    result = getattr(dirs_instance, func)
    assert result == "/custom-dir"


@pytest.mark.parametrize("opinion", [True, False])
def test_site_log_dir_fixed_path(opinion: bool) -> None:
    result = Unix(appname="foo", opinion=opinion).site_log_dir
    assert result == os.path.join("/var/log", "foo")  # noqa: PTH118


def test_site_state_dir_fixed_path() -> None:
    result = Unix(appname="foo").site_state_dir
    assert result == os.path.join("/var/lib", "foo")  # noqa: PTH118


@pytest.mark.usefixtures("_getuid")
@pytest.mark.parametrize("platform", [pytest.param("freebsd", id="freebsd"), pytest.param("netbsd", id="netbsd")])
def test_freebsd_netbsd_site_runtime_dir(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", platform)
    assert Unix().site_runtime_dir == "/var/run"


@pytest.mark.usefixtures("_getuid")
@pytest.mark.parametrize("platform", [pytest.param("freebsd", id="freebsd"), pytest.param("netbsd", id="netbsd")])
def test_freebsd_netbsd_user_runtime_dir_writable(
    monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str
) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", platform)
    mocker.patch("os.access", return_value=True)
    assert Unix().user_runtime_dir == "/var/run/user/1234"


@pytest.mark.usefixtures("_getuid")
@pytest.mark.parametrize("platform", [pytest.param("freebsd", id="freebsd"), pytest.param("netbsd", id="netbsd")])
def test_freebsd_netbsd_user_runtime_dir_not_writable(
    monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str
) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", platform)
    mocker.patch("os.access", return_value=False)
    mocker.patch("tempfile.tempdir", "/tmp")  # noqa: S108
    assert Unix().user_runtime_dir == "/tmp/runtime-1234"  # noqa: S108


@pytest.mark.usefixtures("_getuid")
def test_openbsd_site_runtime_dir(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", "openbsd")
    assert Unix().site_runtime_dir == "/var/run"


@pytest.mark.usefixtures("_getuid")
def test_openbsd_user_runtime_dir_writable(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", "openbsd")
    mocker.patch("os.access", return_value=True)
    assert Unix().user_runtime_dir == "/tmp/run/user/1234"  # noqa: S108


@pytest.mark.usefixtures("_getuid")
def test_openbsd_user_runtime_dir_not_writable(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", "openbsd")
    mocker.patch("os.access", return_value=False)
    mocker.patch("tempfile.tempdir", "/tmp")  # noqa: S108
    assert Unix().user_runtime_dir == "/tmp/runtime-1234"  # noqa: S108


def test_platform_on_win32(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", "win32")
    prev_unix = unix
    importlib.reload(unix)
    try:
        with pytest.raises(RuntimeError, match="should only be used on Unix"):
            unix.Unix().user_runtime_dir  # noqa: B018
    finally:
        sys.modules["platformdirs.unix"] = prev_unix


@pytest.mark.usefixtures("_getuid")
@pytest.mark.parametrize(
    ("platform", "default_dir"),
    [
        ("freebsd", "/var/run/user/1234"),
        ("linux", "/run/user/1234"),
    ],
)
def test_xdg_runtime_dir_unset_writable(
    monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str, default_dir: str
) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", platform)
    mocker.patch("os.access", return_value=True)

    assert Unix().user_runtime_dir == default_dir


@pytest.mark.usefixtures("_getuid")
@pytest.mark.parametrize(
    ("platform", "default_dir"),
    [
        ("freebsd", "/var/run/user/1234"),
        ("linux", "/run/user/1234"),
    ],
)
def test_xdg_runtime_dir_unset_not_writable(
    monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str, default_dir: str
) -> None:
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("sys.platform", platform)
    mocker.patch("os.access", return_value=False)
    mocker.patch("tempfile.tempdir", "/tmp")  # noqa: S108

    result = Unix().user_runtime_dir
    assert not result.startswith(default_dir)
    assert result == "/tmp/runtime-1234"  # noqa: S108


def test_ensure_exists_creates_folder(mocker: MockerFixture, tmp_path: Path) -> None:
    mocker.patch.dict(os.environ, {"XDG_DATA_HOME": str(tmp_path)})
    data_path = Unix(appname="acme", ensure_exists=True).user_data_path
    assert data_path.exists()


def test_folder_not_created_without_ensure_exists(mocker: MockerFixture, tmp_path: Path) -> None:
    mocker.patch.dict(os.environ, {"XDG_DATA_HOME": str(tmp_path)})
    data_path = Unix(appname="acme", ensure_exists=False).user_data_path
    assert not data_path.exists()


def test_iter_data_dirs_xdg(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("XDG_DATA_HOME", "/xdg/data")
    monkeypatch.setenv("XDG_DATA_DIRS", f"/xdg/share1{os.pathsep}/xdg/share2")
    dirs = list(Unix().iter_data_dirs())
    assert dirs == ["/xdg/data", "/xdg/share1", "/xdg/share2"]


def test_iter_config_dirs_xdg(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("XDG_CONFIG_HOME", "/xdg/config")
    monkeypatch.setenv("XDG_CONFIG_DIRS", f"/xdg/etc1{os.pathsep}/xdg/etc2")
    dirs = list(Unix().iter_config_dirs())
    assert dirs == ["/xdg/config", "/xdg/etc1", "/xdg/etc2"]


def test_user_media_dir_from_user_dirs_file(
    mocker: MockerFixture, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.delenv("XDG_DOCUMENTS_DIR", raising=False)
    config_dir = tmp_path / ".config"
    config_dir.mkdir()
    user_dirs_file = config_dir / "user-dirs.dirs"
    user_dirs_file.write_text('XDG_DOCUMENTS_DIR="$HOME/MyDocs"\n')
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("USERPROFILE", str(tmp_path))
    mocker.patch.dict(os.environ, {"XDG_CONFIG_HOME": ""})
    assert Unix().user_documents_dir == f"{tmp_path}/MyDocs"


def test_user_media_dir_missing_key_in_user_dirs_file(
    mocker: MockerFixture, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.delenv("XDG_DOCUMENTS_DIR", raising=False)
    config_dir = tmp_path / ".config"
    config_dir.mkdir()
    user_dirs_file = config_dir / "user-dirs.dirs"
    user_dirs_file.write_text('XDG_DESKTOP_DIR="$HOME/Desktop"\n')
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("USERPROFILE", str(tmp_path))
    mocker.patch.dict(os.environ, {"XDG_CONFIG_HOME": ""})
    assert Unix().user_documents_dir == f"{tmp_path}/Documents"


def test_user_media_dir_no_user_dirs_file(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.delenv("XDG_DOCUMENTS_DIR", raising=False)
    monkeypatch.setenv("HOME", "/nonexistent/path")
    monkeypatch.setenv("USERPROFILE", "/nonexistent/path")
    monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
    assert Unix().user_documents_dir == "/nonexistent/path/Documents"


def test_user_dirs_respects_xdg_config_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("XDG_DOCUMENTS_DIR", raising=False)
    custom_config = tmp_path / "custom_config"
    custom_config.mkdir()
    user_dirs_file = custom_config / "user-dirs.dirs"
    user_dirs_file.write_text('XDG_DOCUMENTS_DIR="$HOME/CustomDocs"\n')
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("USERPROFILE", str(tmp_path))
    monkeypatch.setenv("XDG_CONFIG_HOME", str(custom_config))
    assert Unix().user_documents_dir == f"{tmp_path}/CustomDocs"


_SITE_REDIRECT_CASES: list[tuple[str, str]] = [
    ("user_data_dir", os.path.join("/usr/local/share", "foo")),  # noqa: PTH118
    ("user_config_dir", os.path.join("/etc/xdg", "foo")),  # noqa: PTH118
    ("user_cache_dir", os.path.join("/var/cache", "foo")),  # noqa: PTH118
    ("user_state_dir", os.path.join("/var/lib", "foo")),  # noqa: PTH118
    ("user_log_dir", os.path.join("/var/log", "foo")),  # noqa: PTH118
    (
        "user_runtime_dir",
        os.path.join(  # noqa: PTH118
            "/var/run" if sys.platform.startswith(("freebsd", "openbsd", "netbsd")) else "/run",
            "foo",
        ),
    ),
    ("user_bin_dir", "/usr/local/bin"),
]


@pytest.mark.parametrize(("prop", "expected"), _SITE_REDIRECT_CASES)
def test_use_site_for_root_as_root(
    mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, prop: str, expected: str
) -> None:
    mocker.patch("platformdirs.unix.getuid", return_value=0)
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    result = getattr(Unix(appname="foo", use_site_for_root=True), prop)
    assert result == expected


@pytest.mark.parametrize(("prop", "expected"), _SITE_REDIRECT_CASES)
def test_use_site_for_root_as_non_root(
    mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, prop: str, expected: str
) -> None:
    mocker.patch("platformdirs.unix.getuid", return_value=1000)
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("os.access", return_value=True)
    dirs = Unix(appname="foo", use_site_for_root=True)
    result = getattr(dirs, prop)
    assert result != expected


@pytest.mark.parametrize(("prop", "expected"), _SITE_REDIRECT_CASES)
def test_use_site_for_root_disabled_as_root(
    mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, prop: str, expected: str
) -> None:
    mocker.patch("platformdirs.unix.getuid", return_value=0)
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    mocker.patch("os.access", return_value=True)
    dirs = Unix(appname="foo", use_site_for_root=False)
    result = getattr(dirs, prop)
    assert result != expected


@pytest.mark.parametrize(
    ("xdg_var", "prop", "expected_site"),
    [
        ("XDG_DATA_HOME", "user_data_dir", os.path.join("/usr/local/share", "foo")),  # noqa: PTH118
        ("XDG_CONFIG_HOME", "user_config_dir", os.path.join("/etc/xdg", "foo")),  # noqa: PTH118
        ("XDG_CACHE_HOME", "user_cache_dir", os.path.join("/var/cache", "foo")),  # noqa: PTH118
        ("XDG_STATE_HOME", "user_state_dir", os.path.join("/var/lib", "foo")),  # noqa: PTH118
        ("XDG_STATE_HOME", "user_log_dir", os.path.join("/var/log", "foo")),  # noqa: PTH118
    ],
)
def test_use_site_for_root_bypasses_xdg_user_vars(
    mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, xdg_var: str, prop: str, expected_site: str
) -> None:
    mocker.patch("platformdirs.unix.getuid", return_value=0)
    monkeypatch.setenv(xdg_var, "/custom/xdg/path")
    monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
    result = getattr(Unix(appname="foo", use_site_for_root=True), prop)
    assert result == expected_site


@pytest.mark.parametrize(
    ("xdg_var", "func"),
    [
        ("XDG_DATA_DIRS", Unix.iter_data_dirs),
        ("XDG_CONFIG_DIRS", Unix.iter_config_dirs),
    ],
)
def test_use_site_iter_dirs_no_duplicates(
    mocker: MockerFixture,
    monkeypatch: pytest.MonkeyPatch,
    xdg_var: str,
    func: Callable[[Unix], Iterator[str]],
) -> None:
    mocker.patch("platformdirs.unix.getuid", return_value=0)
    monkeypatch.setenv(xdg_var, "/custom/xdg/path")
    result = func(Unix(appname="foo", use_site_for_root=True))
    assert list(result) == [os.path.join("/custom/xdg/path", "foo")]  # noqa: PTH118
