import importlib
import os
import sys
from pathlib import Path
import time
from types import ModuleType

import pytest

import rimpack.steamworks as steamworks_module
from rimpack.steamworks import (
    SteamworksResolutionError,
    SteamworksSubscriptionError,
    SteamworksUnavailableError,
    coerce_workshop_item_id,
    create_steamworks_client,
    is_steamworks_available,
    resolve_workshop_by_id,
    subscribe_to_workshop_item,
    unsubscribe_from_workshop_item,
)
from tests._helpers import write_about_xml


class _FakeWorkshopApi:
    def __init__(self) -> None:
        self.calls: list[tuple[str, int]] = []

    def SubscribeItem(self, published_file_id: int) -> bool:
        self.calls.append(("subscribe", published_file_id))
        return True


class _FakeWorkshopClient:
    def __init__(self) -> None:
        self.Workshop: _FakeWorkshopApi = _FakeWorkshopApi()


class _FakeDirectClient:
    def __init__(self) -> None:
        self.calls: list[tuple[str, int]] = []

    def unsubscribe_item(self, published_file_id: int) -> None:
        self.calls.append(("unsubscribe", published_file_id))


def test_subscribe_to_workshop_item_uses_workshop_api_and_normalizes_string_id() -> None:
    client = _FakeWorkshopClient()

    result = subscribe_to_workshop_item(" 2009463077 ", steamworks_client=client)

    assert result is True
    assert client.Workshop.calls == [("subscribe", 2009463077)]


def test_unsubscribe_from_workshop_item_supports_direct_client_method() -> None:
    client = _FakeDirectClient()

    result = unsubscribe_from_workshop_item(818773962, steamworks_client=client)

    assert result is True
    assert client.calls == [("unsubscribe", 818773962)]


def test_subscribe_to_workshop_item_accepts_sharedfiles_url() -> None:
    client = _FakeWorkshopClient()

    result = subscribe_to_workshop_item(
        "https://steamcommunity.com/sharedfiles/filedetails/?id=3685058533",
        steamworks_client=client,
    )

    assert result is True
    assert client.Workshop.calls == [("subscribe", 3685058533)]


def test_coerce_workshop_item_id_extracts_sharedfiles_url_id() -> None:
    assert (
        coerce_workshop_item_id("https://steamcommunity.com/sharedfiles/filedetails/?id=3685058533")
        == 3685058533
    )


def test_subscribe_to_workshop_item_rejects_invalid_ids() -> None:
    with pytest.raises(ValueError, match="positive integer"):
        _ = subscribe_to_workshop_item("steam:2009463077", steamworks_client=_FakeWorkshopClient())


def test_unsubscribe_from_workshop_item_raises_when_steamworks_rejects_request() -> None:
    class _RejectingWorkshopApi:
        def UnsubscribeItem(self, _published_file_id: int) -> bool:
            return False

    class _RejectingClient:
        def __init__(self) -> None:
            self.Workshop: _RejectingWorkshopApi = _RejectingWorkshopApi()

    with pytest.raises(SteamworksSubscriptionError, match="refused to unsubscribe"):
        _ = unsubscribe_from_workshop_item(2009463077, steamworks_client=_RejectingClient())


def test_resolve_workshop_by_id_returns_existing_subscribed_mod_without_subscription(
    tmp_path: Path,
) -> None:
    workshop_root = tmp_path / "Workshop"
    write_about_xml(workshop_root / "2009463077", "author.workshop", "Workshop Mod")

    mod = resolve_workshop_by_id(2009463077, steam_workshop_folder=workshop_root)

    assert mod.path == (workshop_root / "2009463077").resolve()
    assert mod.about.package_id == "author.workshop"
    assert mod.about.name == "Workshop Mod"


def test_resolve_workshop_by_id_subscribes_then_unsubscribes_when_cleanup_is_true(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    workshop_root = tmp_path / "Workshop"
    workshop_root.mkdir()
    calls: list[tuple[str, int]] = []

    def fake_subscribe(workshop_id: int | str, steamworks_client: object | None = None) -> bool:
        assert steamworks_client is fake_client
        published_file_id = coerce_workshop_item_id(workshop_id)
        calls.append(("subscribe", published_file_id))
        write_about_xml(workshop_root / str(published_file_id), "author.subscribed", "Subscribed Mod")
        return True

    def fake_unsubscribe(workshop_id: int | str, steamworks_client: object | None = None) -> bool:
        assert steamworks_client is fake_client
        calls.append(("unsubscribe", coerce_workshop_item_id(workshop_id)))
        return True

    fake_client = object()
    monkeypatch.setattr(steamworks_module, "create_steamworks_client", lambda: fake_client)
    monkeypatch.setattr(steamworks_module, "subscribe_to_workshop_item", fake_subscribe)
    monkeypatch.setattr(steamworks_module, "unsubscribe_from_workshop_item", fake_unsubscribe)

    mod = resolve_workshop_by_id(2009463077, steam_workshop_folder=workshop_root)

    assert mod.about.package_id == "author.subscribed"
    assert calls == [("subscribe", 2009463077), ("unsubscribe", 2009463077)]


def test_resolve_workshop_by_id_waits_for_subscribed_mod_to_appear(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    workshop_root = tmp_path / "Workshop"
    workshop_root.mkdir()
    calls: list[tuple[str, int]] = []
    monotonic_values = iter((0.0, 0.1, 0.2))

    def fake_subscribe(workshop_id: int | str, steamworks_client: object | None = None) -> bool:
        assert steamworks_client is fake_client
        calls.append(("subscribe", coerce_workshop_item_id(workshop_id)))
        return True

    def fake_unsubscribe(workshop_id: int | str, steamworks_client: object | None = None) -> bool:
        assert steamworks_client is fake_client
        calls.append(("unsubscribe", coerce_workshop_item_id(workshop_id)))
        return True

    def fake_sleep(_seconds: float) -> None:
        write_about_xml(workshop_root / "2009463077", "author.delayed", "Delayed Mod")

    fake_client = object()
    monkeypatch.setattr(steamworks_module, "create_steamworks_client", lambda: fake_client)
    monkeypatch.setattr(steamworks_module, "subscribe_to_workshop_item", fake_subscribe)
    monkeypatch.setattr(steamworks_module, "unsubscribe_from_workshop_item", fake_unsubscribe)
    monkeypatch.setattr(time, "monotonic", lambda: next(monotonic_values))
    monkeypatch.setattr(time, "sleep", fake_sleep)

    mod = resolve_workshop_by_id(2009463077, steam_workshop_folder=workshop_root)

    assert mod.about.package_id == "author.delayed"
    assert calls == [("subscribe", 2009463077), ("unsubscribe", 2009463077)]


def test_resolve_workshop_by_id_keeps_subscription_when_cleanup_is_false(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    workshop_root = tmp_path / "Workshop"
    workshop_root.mkdir()
    calls: list[tuple[str, int]] = []

    def fake_subscribe(workshop_id: int | str, steamworks_client: object | None = None) -> bool:
        assert steamworks_client is fake_client
        published_file_id = coerce_workshop_item_id(workshop_id)
        calls.append(("subscribe", published_file_id))
        write_about_xml(workshop_root / str(published_file_id), "author.keep", "Kept Mod")
        return True

    fake_client = object()
    monkeypatch.setattr(steamworks_module, "create_steamworks_client", lambda: fake_client)
    monkeypatch.setattr(steamworks_module, "subscribe_to_workshop_item", fake_subscribe)

    mod = resolve_workshop_by_id(
        818773962,
        cleanup=False,
        steam_workshop_folder=workshop_root,
    )

    assert mod.about.package_id == "author.keep"
    assert calls == [("subscribe", 818773962)]


def test_resolve_workshop_by_id_raises_when_subscribed_mod_is_not_available(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    workshop_root = tmp_path / "Workshop"
    workshop_root.mkdir()
    calls: list[tuple[str, int]] = []

    def fake_subscribe(workshop_id: int | str, steamworks_client: object | None = None) -> bool:
        assert steamworks_client is fake_client
        calls.append(("subscribe", coerce_workshop_item_id(workshop_id)))
        return True

    def fake_unsubscribe(workshop_id: int | str, steamworks_client: object | None = None) -> bool:
        assert steamworks_client is fake_client
        calls.append(("unsubscribe", coerce_workshop_item_id(workshop_id)))
        return True

    fake_client = object()
    monkeypatch.setattr(steamworks_module, "create_steamworks_client", lambda: fake_client)
    monkeypatch.setattr(steamworks_module, "subscribe_to_workshop_item", fake_subscribe)
    monkeypatch.setattr(steamworks_module, "unsubscribe_from_workshop_item", fake_unsubscribe)
    def fake_sleep(_seconds: float) -> None:
        return None

    monotonic_values = iter((0.0, 30.0, 61.0))
    monkeypatch.setattr(time, "monotonic", lambda: next(monotonic_values))
    monkeypatch.setattr(time, "sleep", fake_sleep)

    with pytest.raises(SteamworksResolutionError, match="timed out waiting"):
        _ = resolve_workshop_by_id(2009463077, steam_workshop_folder=workshop_root)

    assert calls == [("subscribe", 2009463077), ("unsubscribe", 2009463077)]


def test_create_steamworks_client_raises_when_package_is_missing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    _ = sys.modules.pop("steamworks", None)
    _ = sys.modules.pop("steamworkspy", None)
    monkeypatch.setattr(steamworks_module, "_load_steamworkspy_package", lambda: None)
    with pytest.raises(SteamworksUnavailableError, match="package is not installed"):
        _ = create_steamworks_client()


def test_is_steamworks_available_supports_steamworkspy_package_layout(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    site_packages = tmp_path / "site-packages"
    steamworkspy_root = site_packages / "steamworkspy"
    steamworks_root = steamworkspy_root / "steamworks"
    steamworks_root.mkdir(parents=True)
    _ = (steamworkspy_root / "__init__.py").write_text("", encoding="utf-8")
    _ = (steamworks_root / "__init__.py").write_text("VALUE = 1\n", encoding="utf-8")

    try:
        _ = sys.modules.pop("steamworks", None)
        _ = sys.modules.pop("steamworkspy", None)
        sys.path.insert(0, str(site_packages))
        importlib.invalidate_caches()

        def fake_import_module(name: str) -> ModuleType:
            if name == "steamworks":
                raise ModuleNotFoundError(name)
            return importlib.import_module(name)

        monkeypatch.setattr(steamworks_module, "import_module", fake_import_module)

        assert is_steamworks_available() is True

        module = importlib.import_module("steamworks.steamworks")
        module_file = getattr(module, "__file__", None)
        assert isinstance(module_file, str)
        assert Path(module_file).resolve() == (steamworks_root / "__init__.py").resolve()
    finally:
        if str(site_packages) in sys.path:
            sys.path.remove(str(site_packages))
        _ = sys.modules.pop("steamworks", None)
        _ = sys.modules.pop("steamworkspy", None)


def test_create_steamworks_client_prepares_windows_runtime(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    module_root = tmp_path / "steamworkspy"
    steamworks_package = module_root / "steamworks"
    steamworks_package.mkdir(parents=True)
    _ = (module_root / "SteamworksPy64.dll").write_text("", encoding="utf-8")
    _ = (module_root / "steam_api64.dll").write_text("", encoding="utf-8")

    class _FakeDllHandle:
        def __init__(self) -> None:
            self.closed: bool = False

        def close(self) -> None:
            self.closed = True

    class _FakeClient:
        def initialize(self) -> bool:
            return True

    fake_dll_handle = _FakeDllHandle()
    fake_module = ModuleType("steamworks")
    fake_module.__file__ = str(steamworks_package / "__init__.py")
    setattr(fake_module, "STEAMWORKS", _FakeClient)

    monkeypatch.setattr(steamworks_module, "_load_steamworks_module", lambda: fake_module)
    monkeypatch.setattr(sys, "platform", "win32")
    monkeypatch.setattr(Path, "cwd", staticmethod(lambda: tmp_path))

    def fake_add_dll_directory(path: str) -> _FakeDllHandle:
        assert path == str(module_root)
        return fake_dll_handle

    monkeypatch.setattr(os, "add_dll_directory", fake_add_dll_directory)

    client = create_steamworks_client()

    assert isinstance(client, _FakeClient)
    assert fake_dll_handle.closed is True
    assert not (tmp_path / "steam_appid.txt").exists()


def test_create_steamworks_client_preserves_existing_steam_appid_file(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    existing_app_id_path = tmp_path / "steam_appid.txt"
    _ = existing_app_id_path.write_text("123\n", encoding="utf-8")
    module_root = tmp_path / "steamworkspy"
    steamworks_package = module_root / "steamworks"
    steamworks_package.mkdir(parents=True)
    _ = (module_root / "SteamworksPy64.dll").write_text("", encoding="utf-8")
    _ = (module_root / "steam_api64.dll").write_text("", encoding="utf-8")

    class _FakeClient:
        def initialize(self) -> bool:
            return True

    fake_module = ModuleType("steamworks")
    fake_module.__file__ = str(steamworks_package / "__init__.py")
    setattr(fake_module, "STEAMWORKS", _FakeClient)

    monkeypatch.setattr(steamworks_module, "_load_steamworks_module", lambda: fake_module)
    monkeypatch.setattr(sys, "platform", "win32")
    monkeypatch.setattr(Path, "cwd", staticmethod(lambda: tmp_path))

    class _NoopDllHandle:
        def close(self) -> None:
            return None

    def fake_add_dll_directory(_path: str) -> _NoopDllHandle:
        return _NoopDllHandle()

    monkeypatch.setattr(os, "add_dll_directory", fake_add_dll_directory)

    _ = create_steamworks_client()

    assert existing_app_id_path.read_text(encoding="utf-8") == "123\n"


def test_create_steamworks_client_raises_when_native_files_are_missing(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    module_root = tmp_path / "steamworkspy"
    steamworks_package = module_root / "steamworks"
    steamworks_package.mkdir(parents=True)

    fake_module = ModuleType("steamworks")
    fake_module.__file__ = str(steamworks_package / "__init__.py")
    setattr(fake_module, "STEAMWORKS", object)

    monkeypatch.setattr(steamworks_module, "_load_steamworks_module", lambda: fake_module)
    monkeypatch.setattr(sys, "platform", "win32")
    monkeypatch.setattr(Path, "cwd", staticmethod(lambda: tmp_path))

    class _NoopDllHandle:
        def close(self) -> None:
            return None

    def fake_add_dll_directory(_path: str) -> _NoopDllHandle:
        return _NoopDllHandle()

    monkeypatch.setattr(os, "add_dll_directory", fake_add_dll_directory)

    with pytest.raises(SteamworksUnavailableError, match="native libraries are missing"):
        _ = create_steamworks_client()
