from __future__ import annotations

from pathlib import Path
import subprocess

import pytest

import rimpack.launching as launching_module
from rimpack.config import RimpackConfig, get_pack_runtime_root, get_pack_save_data_root
from rimpack.core.modpack import (
    Modpack,
    ModpackFile,
    ModpackLoadOrderEntry,
    PackInfo,
    PackLayout,
    PackPaths,
    new_pack_toml_manifest,
    parse_modpack_list,
    write_modpack,
)
from rimpack.core.mods.mod_folder import ModFolder, load_mod_folder
from rimpack.launching import launch_modpack
from tests._helpers import write_about_xml


def test_launch_modpack_subscribes_generates_launch_tree_and_cleans_extras(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    pack_root = tmp_path / "LaunchPack"
    rimworld_root = tmp_path / "RimWorld"
    workshop_root = tmp_path / "Workshop"
    local_mods_root = pack_root / "local" / "mods"

    _ = (rimworld_root / "Mods").mkdir(parents=True)
    _ = (rimworld_root / "RimWorldWin64.exe").write_text("", encoding="utf-8")
    _ = (rimworld_root / "Version.txt").write_text("1.6.9999 rev1", encoding="utf-8")
    write_about_xml(rimworld_root / "Data" / "Core", "ludeon.rimworld", "Core")
    write_about_xml(local_mods_root / "LocalOne", "author.local", "Local One")
    write_about_xml(workshop_root / "999999", "author.extra", "Extra Workshop")
    _ = (pack_root / "config" / "Mod_Configs").mkdir(parents=True, exist_ok=True)
    _ = (pack_root / "config" / "Mod_Configs" / "HugsLib.xml").write_text(
        "<settings />",
        encoding="utf-8",
    )
    _ = (pack_root / "config" / "Mod_Configs" / "RimHUD").mkdir()
    _ = (pack_root / "config" / "Mod_Configs" / "RimHUD" / "Config.xml").write_text(
        "<rimhud />",
        encoding="utf-8",
    )

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Launch Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/00_core.list", "mods/10_mods.list")),
            paths=PackPaths(
                local_mods_dir="local/mods",
                config_dir="config/Mod_Configs",
                resolved_load_order_file="generated/resolved_load_order.txt",
            ),
        ),
        mod_files=(
            ModpackFile(
                path="mods/00_core.list",
                document=parse_modpack_list("packageid:ludeon.rimworld\n"),
            ),
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list("local:LocalOne\nsteam:2009463077\n"),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    subscribed: list[int] = []
    unsubscribed: list[int] = []
    symlinks: list[tuple[Path, Path, bool]] = []
    popen_calls: list[tuple[list[str], str | None]] = []
    wait_calls: list[int] = []

    monkeypatch.setattr(launching_module, "create_steamworks_client", lambda: object())

    def fake_resolve_workshop_by_id(
        workshop_id: int | str,
        *,
        cleanup: bool = True,
        steam_workshop_folder: Path | None = None,
        steamworks_client: object | None = None,
    ):
        published_file_id = int(workshop_id)
        subscribed.append(published_file_id)
        assert cleanup is False
        assert steam_workshop_folder == workshop_root
        assert steamworks_client is not None
        mod_root = workshop_root / str(published_file_id)
        write_about_xml(mod_root, "author.workshop", "Workshop Mod")
        return load_mod_folder(mod_root)

    monkeypatch.setattr(launching_module, "resolve_workshop_by_id", fake_resolve_workshop_by_id)

    def fake_unsubscribe_from_workshop_item(
        workshop_id: int | str,
        steamworks_client: object | None = None,
    ) -> bool:
        _ = steamworks_client
        unsubscribed.append(int(workshop_id))
        return True

    def fake_create_symlink(
        *,
        target: Path,
        link_path: Path,
        target_is_directory: bool,
    ) -> None:
        symlinks.append((target, link_path, target_is_directory))

    def fake_resolve_launch_executable(
        launch_root: Path,
        *,
        rimworld_folder: Path,
    ) -> Path:
        _ = rimworld_folder
        return launch_root / "RimWorldWin64.exe"

    monkeypatch.setattr(
        launching_module,
        "unsubscribe_from_workshop_item",
        fake_unsubscribe_from_workshop_item,
    )
    monkeypatch.setattr(launching_module, "_create_symlink", fake_create_symlink)
    monkeypatch.setattr(
        launching_module,
        "_resolve_launch_executable",
        fake_resolve_launch_executable,
    )

    class _DummyPopen:
        def __init__(self, args: list[str], cwd: str | None = None) -> None:
            popen_calls.append((args, cwd))

        def wait(self) -> int:
            wait_calls.append(1)
            return 0

    monkeypatch.setattr(subprocess, "Popen", _DummyPopen)

    result = launch_modpack(
        modpack,
        root_directory=pack_root,
        config=RimpackConfig(
            rimworld_folder=rimworld_root,
            steam_workshop_folder=workshop_root,
        ),
        clean=True,
    )

    expected_launch_root = get_pack_runtime_root(pack_root).resolve()
    expected_save_data_root = get_pack_save_data_root(pack_root).resolve()
    assert result.launch_root == expected_launch_root
    assert result.save_data_root == expected_save_data_root
    assert result.subscribed_workshop_ids == (2009463077,)
    assert subscribed == [2009463077]
    assert unsubscribed == [999999]
    assert wait_calls == [1]
    assert [entry.package_id for entry in result.load_order] == [
        "ludeon.rimworld",
        "author.local",
        "author.workshop",
    ]
    assert (pack_root / "generated" / "resolved_load_order.txt").read_text(
        encoding="utf-8"
    ) == (
        "0001 | ludeon.rimworld | Core | packageid:ludeon.rimworld | mods/00_core.list:1\n"
        "0002 | author.local | Local One | local:LocalOne | mods/10_mods.list:1\n"
        "0003 | author.workshop | Workshop Mod | steam:2009463077 | mods/10_mods.list:2\n"
    )
    assert (expected_save_data_root / "Config" / "HugsLib.xml").read_text(
        encoding="utf-8"
    ) == "<settings />"
    assert (expected_save_data_root / "Config" / "RimHUD" / "Config.xml").read_text(
        encoding="utf-8"
    ) == "<rimhud />"
    assert (expected_save_data_root / "Config" / "ModsConfig.xml").read_text(
        encoding="utf-8"
    ) == (
        '<?xml version="1.0" encoding="utf-8"?>\n'
        "<ModsConfigData>\n"
        "  <version>1.6.9999 rev1</version>\n"
        "  <activeMods>\n"
        "    <li>ludeon.rimworld</li>\n"
        "    <li>author.local</li>\n"
        "    <li>author.workshop</li>\n"
        "  </activeMods>\n"
        "  <knownExpansions>\n"
        "  </knownExpansions>\n"
        "</ModsConfigData>\n"
    )
    assert not any(link_path == expected_launch_root / "Mods" / "0001_Core" for _, link_path, _ in symlinks)
    assert any(link_path == expected_launch_root / "Mods" / "0002_LocalOne" for _, link_path, _ in symlinks)
    assert any(link_path == expected_launch_root / "Mods" / "0003_2009463077" for _, link_path, _ in symlinks)
    assert popen_calls == [
        (
            [
                str(expected_launch_root / "RimWorldWin64.exe"),
                f"-savedatafolder={expected_save_data_root}",
            ],
            str(expected_launch_root),
        )
    ]


def test_launch_modpack_uses_pack_specific_runtime_and_save_data_roots(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    pack_root = tmp_path / "DefaultLaunchRootPack"
    rimworld_root = tmp_path / "RimWorld"

    _ = (rimworld_root / "Mods").mkdir(parents=True)
    _ = (rimworld_root / "RimWorldWin64.exe").write_text("", encoding="utf-8")
    _ = (rimworld_root / "Version.txt").write_text("1.6", encoding="utf-8")
    write_about_xml(rimworld_root / "Data" / "Core", "ludeon.rimworld", "Core")

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Default Launch Root Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/00_core.list",)),
            paths=PackPaths(),
        ),
        mod_files=(
            ModpackFile(
                path="mods/00_core.list",
                document=parse_modpack_list("packageid:ludeon.rimworld\n"),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    def ignore_create_symlink(
        *,
        target: Path,
        link_path: Path,
        target_is_directory: bool,
    ) -> None:
        _ = (target, link_path, target_is_directory)

    def fake_resolve_launch_executable(
        launch_root: Path,
        *,
        rimworld_folder: Path,
    ) -> Path:
        _ = rimworld_folder
        return launch_root / "RimWorldWin64.exe"

    def fake_popen(args: list[str], cwd: str | None = None) -> object:
        _ = (args, cwd)
        return object()

    monkeypatch.setattr(launching_module, "_create_symlink", ignore_create_symlink)
    monkeypatch.setattr(
        launching_module,
        "_resolve_launch_executable",
        fake_resolve_launch_executable,
    )
    class _DummyPopen:
        def __init__(self, args: list[str], cwd: str | None = None) -> None:
            _ = fake_popen(args, cwd)

        def wait(self) -> int:
            return 0

    monkeypatch.setattr(subprocess, "Popen", _DummyPopen)

    result = launch_modpack(
        modpack,
        root_directory=pack_root,
        config=RimpackConfig(rimworld_folder=rimworld_root),
        clean=False,
    )

    assert result.launch_root == get_pack_runtime_root(pack_root).resolve()
    assert result.save_data_root == get_pack_save_data_root(pack_root).resolve()


def test_launch_modpack_preserves_existing_pack_config_and_only_rewrites_mods_config(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    pack_root = tmp_path / "PersistentConfigPack"
    rimworld_root = tmp_path / "RimWorld"

    _ = (rimworld_root / "Mods").mkdir(parents=True)
    _ = (rimworld_root / "RimWorldWin64.exe").write_text("", encoding="utf-8")
    _ = (rimworld_root / "Version.txt").write_text("1.6", encoding="utf-8")
    write_about_xml(rimworld_root / "Data" / "Core", "ludeon.rimworld", "Core")
    _ = (pack_root / "config" / "Mod_Configs").mkdir(parents=True, exist_ok=True)
    _ = (pack_root / "config" / "Mod_Configs" / "Prefs.xml").write_text(
        "<pack-default />",
        encoding="utf-8",
    )

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Persistent Config Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/00_core.list",)),
            paths=PackPaths(config_dir="config/Mod_Configs"),
        ),
        mod_files=(
            ModpackFile(
                path="mods/00_core.list",
                document=parse_modpack_list("packageid:ludeon.rimworld\n"),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    def ignore_create_symlink(
        *,
        target: Path,
        link_path: Path,
        target_is_directory: bool,
    ) -> None:
        _ = (target, link_path, target_is_directory)

    def fake_resolve_launch_executable(
        launch_root: Path,
        *,
        rimworld_folder: Path,
    ) -> Path:
        _ = rimworld_folder
        return launch_root / "RimWorldWin64.exe"

    class _DummyPopen:
        def __init__(self, args: list[str], cwd: str | None = None) -> None:
            _ = (args, cwd)

        def wait(self) -> int:
            return 0

    state_root = tmp_path / "machine-state"

    def fake_pack_runtime_root(_root_directory: Path) -> Path:
        return state_root / "runtime" / "RimWorld"

    def fake_pack_save_data_root(_root_directory: Path) -> Path:
        return state_root / "save-data"

    monkeypatch.setattr(launching_module, "_create_symlink", ignore_create_symlink)
    monkeypatch.setattr(
        launching_module,
        "_resolve_launch_executable",
        fake_resolve_launch_executable,
    )
    monkeypatch.setattr(
        launching_module,
        "get_pack_runtime_root",
        fake_pack_runtime_root,
    )
    monkeypatch.setattr(
        launching_module,
        "get_pack_save_data_root",
        fake_pack_save_data_root,
    )
    monkeypatch.setattr(subprocess, "Popen", _DummyPopen)

    first_result = launch_modpack(
        modpack,
        root_directory=pack_root,
        config=RimpackConfig(rimworld_folder=rimworld_root),
        clean=False,
    )
    config_root = first_result.save_data_root / "Config"
    prefs_path = config_root / "Prefs.xml"
    mods_config_path = config_root / "ModsConfig.xml"

    assert prefs_path.read_text(encoding="utf-8") == "<pack-default />"
    _ = prefs_path.write_text("<user-customized />", encoding="utf-8", newline="\n")
    _ = mods_config_path.write_text("<stale />", encoding="utf-8", newline="\n")
    new_pack_file = pack_root / "config" / "Mod_Configs" / "NewlyAdded.xml"
    _ = new_pack_file.write_text("<new-pack-default />", encoding="utf-8", newline="\n")

    _ = launch_modpack(
        modpack,
        root_directory=pack_root,
        config=RimpackConfig(rimworld_folder=rimworld_root),
        clean=False,
    )

    assert prefs_path.read_text(encoding="utf-8") == "<user-customized />"
    assert (config_root / "NewlyAdded.xml").read_text(encoding="utf-8") == "<new-pack-default />"
    assert "<stale />" not in mods_config_path.read_text(encoding="utf-8")


def test_launch_modpack_defers_cleanup_until_after_rimworld_exits(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    pack_root = tmp_path / "DeferredCleanupPack"
    rimworld_root = tmp_path / "RimWorld"
    workshop_root = tmp_path / "Workshop"

    _ = (rimworld_root / "Mods").mkdir(parents=True)
    _ = (rimworld_root / "RimWorldWin64.exe").write_text("", encoding="utf-8")
    _ = (rimworld_root / "Version.txt").write_text("1.6", encoding="utf-8")
    write_about_xml(rimworld_root / "Data" / "Core", "ludeon.rimworld", "Core")
    write_about_xml(workshop_root / "2009463077", "author.workshop", "Workshop Mod")
    write_about_xml(workshop_root / "999999", "author.extra", "Extra Workshop")

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Deferred Cleanup Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/10_mods.list",)),
            paths=PackPaths(),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list("steam:2009463077\n"),
            ),
        )
    )
    write_modpack(pack_root, modpack)

    events: list[str] = []

    class _FakeSteamworksClient:
        pass

    def fake_create_steamworks_client() -> _FakeSteamworksClient:
        return _FakeSteamworksClient()

    def fake_resolve_workshop_by_id(
        workshop_id: int | str,
        *,
        cleanup: bool = True,
        steam_workshop_folder: Path | None = None,
        steamworks_client: object | None = None,
    ) -> ModFolder:
        _ = (workshop_id, cleanup, steam_workshop_folder, steamworks_client)
        return load_mod_folder(workshop_root / "2009463077")

    monkeypatch.setattr(
        launching_module,
        "create_steamworks_client",
        fake_create_steamworks_client,
    )
    monkeypatch.setattr(
        launching_module,
        "resolve_workshop_by_id",
        fake_resolve_workshop_by_id,
    )

    def fake_unsubscribe_from_workshop_item(
        workshop_id: int | str,
        steamworks_client: object | None = None,
    ) -> bool:
        _ = steamworks_client
        events.append(f"unsubscribe:{int(workshop_id)}")
        return True

    monkeypatch.setattr(
        launching_module,
        "unsubscribe_from_workshop_item",
        fake_unsubscribe_from_workshop_item,
    )

    def ignore_create_symlink(
        *,
        target: Path,
        link_path: Path,
        target_is_directory: bool,
    ) -> None:
        _ = (target, link_path, target_is_directory)

    def fake_resolve_launch_executable(
        launch_root: Path,
        *,
        rimworld_folder: Path,
    ) -> Path:
        _ = rimworld_folder
        return launch_root / "RimWorldWin64.exe"

    monkeypatch.setattr(launching_module, "_create_symlink", ignore_create_symlink)
    monkeypatch.setattr(
        launching_module,
        "_resolve_launch_executable",
        fake_resolve_launch_executable,
    )

    class _DummyPopen:
        def __init__(self, args: list[str], cwd: str | None = None) -> None:
            _ = (args, cwd)
            events.append("spawn")

        def wait(self) -> int:
            events.append("wait")
            return 0

    monkeypatch.setattr(subprocess, "Popen", _DummyPopen)

    _ = launch_modpack(
        modpack,
        root_directory=pack_root,
        config=RimpackConfig(
            rimworld_folder=rimworld_root,
            steam_workshop_folder=workshop_root,
        ),
        clean=True,
    )

    assert events == ["spawn", "wait", "unsubscribe:999999"]


def test_render_mods_config_deduplicates_case_insensitive_package_ids() -> None:
    rendered = launching_module._render_mods_config(  # pyright: ignore[reportPrivateUsage]
        load_order=(
            ModpackLoadOrderEntry(
                path=Path("RimWorld") / "Data" / "Core",
                source_file="mods/00_core.list",
                source_line=1,
                requested="packageid:Ludeon.RimWorld",
                package_id="Ludeon.RimWorld",
                name="Core",
            ),
            ModpackLoadOrderEntry(
                path=Path("RimWorld") / "Data" / "Royalty",
                source_file="mods/00_core.list",
                source_line=2,
                requested="packageid:Ludeon.RimWorld.Royalty",
                package_id="Ludeon.RimWorld.Royalty",
                name="Royalty",
            ),
            ModpackLoadOrderEntry(
                path=Path("Workshop") / "costel.layeredapparel",
                source_file="mods/10_mods.list",
                source_line=1,
                requested="packageid:costel.layeredapparel",
                package_id="costel.layeredapparel",
                name="Layered Apparel",
            ),
            ModpackLoadOrderEntry(
                path=Path("RimWorld") / "Data" / "Royalty2",
                source_file="mods/10_mods.list",
                source_line=2,
                requested="packageid:ludeon.rimworld.royalty",
                package_id="ludeon.rimworld.royalty",
                name="Royalty Duplicate",
            ),
            ModpackLoadOrderEntry(
                path=Path("RimWorld") / "Data" / "Odyssey",
                source_file="mods/10_mods.list",
                source_line=3,
                requested="packageid:ludeon.rimworld.odyssey",
                package_id="ludeon.rimworld.odyssey",
                name="Odyssey",
            ),
        ),
        rimworld_version="1.6.4633 rev1261",
        rimworld_folder=Path("RimWorld"),
    )

    assert rendered == (
        '<?xml version="1.0" encoding="utf-8"?>\n'
        "<ModsConfigData>\n"
        "  <version>1.6.4633 rev1261</version>\n"
        "  <activeMods>\n"
        "    <li>ludeon.rimworld</li>\n"
        "    <li>ludeon.rimworld.royalty</li>\n"
        "    <li>costel.layeredapparel</li>\n"
        "    <li>ludeon.rimworld.odyssey</li>\n"
        "  </activeMods>\n"
        "  <knownExpansions>\n"
        "    <li>ludeon.rimworld.royalty</li>\n"
        "    <li>ludeon.rimworld.odyssey</li>\n"
        "  </knownExpansions>\n"
        "</ModsConfigData>\n"
    )
