from __future__ import annotations

from pathlib import Path

import pytest

from rimpack.core.modpack import (
    AliasLine,
    CommentLine,
    ModpackEntryAddition,
    Modpack,
    ModpackError,
    ModpackFile,
    ModpackListReference,
    ModpackResolutionRoots,
    PackInfo,
    PackLayout,
    PackPaths,
    add_entries_to_modpack_file,
    generate_modpack_load_order,
    load_modpack,
    new_modpack_list_document,
    new_pack_toml_manifest,
    parse_modpack_list,
    render_modpack_list,
    resolve_modpack_file_target,
    validate_modpack,
    write_modpack,
)
from tests._helpers import write_about_xml


def test_write_and_load_modpack_round_trip(tmp_path: Path) -> None:
    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Round Trip Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(
                mod_files=("mods/00_core.list", "mods/10_libraries.list"),
            ),
            paths=PackPaths(
                config_dir="config/Mod_Configs",
            ),
        ),
        mod_files=(
            ModpackFile(
                path="mods/00_core.list",
                document=new_modpack_list_document(
                    (
                        CommentLine(
                            raw_text="# Core game",
                            leading_whitespace="",
                            comment_text=" Core game",
                            line_ending="\n",
                        ),
                    ),
                ),
            ),
            ModpackFile(
                path="mods/10_libraries.list",
                document=new_modpack_list_document(
                    (
                        CommentLine(
                            raw_text="# Libraries",
                            leading_whitespace="",
                            comment_text=" Libraries",
                            line_ending="\n",
                        ),
                    ),
                ),
            ),
        ),
    )

    write_modpack(tmp_path, modpack)

    loaded = load_modpack(tmp_path)

    assert loaded == modpack
    assert (tmp_path / "pack.toml").is_file()
    assert (tmp_path / "mods" / "00_core.list").is_file()
    assert (tmp_path / "mods" / "10_libraries.list").is_file()


def test_load_modpack_requires_declared_mod_files_to_exist(tmp_path: Path) -> None:
    _ = (tmp_path / "pack.toml").write_text(
        """\
[pack]
name = "Broken Pack"
rimworld_version = "1.6"
pack_format_version = 1

[layout]
mod_files = ["mods/00_core.list"]
""",
        encoding="utf-8",
    )

    with pytest.raises(ModpackError, match=r"declared mod list file does not exist"):
        _ = load_modpack(tmp_path)


def test_write_modpack_requires_manifest_and_documents_to_match() -> None:
    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Mismatch Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/00_core.list",)),
            paths=PackPaths(),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_libraries.list",
                document=new_modpack_list_document(()),
            ),
        ),
    )

    with pytest.raises(
        ModpackError,
        match=r"must exactly match manifest\.layout\.mod_files order",
    ):
        write_modpack(Path("unused"), modpack)


def test_resolve_modpack_file_target_supports_alias_and_path(tmp_path: Path) -> None:
    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Target Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(
                mod_files=("mods/00_core.list", "mods/10_libraries.list"),
            ),
            paths=PackPaths(),
        ),
        mod_files=(
            ModpackFile(
                path="mods/00_core.list",
                document=new_modpack_list_document(
                    (
                        AliasLine(
                            raw_text="@alias core",
                            leading_whitespace="",
                            whitespace_after_directive=" ",
                            alias="core",
                            trailing_whitespace="",
                            line_ending="\n",
                        ),
                    ),
                ),
            ),
            ModpackFile(
                path="mods/10_libraries.list",
                document=new_modpack_list_document(
                    (
                        AliasLine(
                            raw_text="@alias libraries",
                            leading_whitespace="",
                            whitespace_after_directive=" ",
                            alias="libraries",
                            trailing_whitespace="",
                            line_ending="\n",
                        ),
                    ),
                ),
            ),
        ),
    )

    assert resolve_modpack_file_target(
        modpack,
        root_directory=tmp_path,
        target="@libraries",
    ) == 1
    assert resolve_modpack_file_target(
        modpack,
        root_directory=tmp_path,
        target="mods/00_core.list",
    ) == 0


def test_add_entries_to_modpack_file_appends_canonical_lines() -> None:
    modpack_file = ModpackFile(
        path="mods/10_libraries.list",
        document=new_modpack_list_document(
            (
                CommentLine(
                    raw_text="# Libraries",
                    leading_whitespace="",
                    comment_text=" Libraries",
                    line_ending="\n",
                ),
            ),
        ),
    )

    updated = add_entries_to_modpack_file(
        modpack_file,
        (
            ModpackEntryAddition(
                reference=ModpackListReference(kind="steam", value="2009463077"),
                trailing_comment="Harmony",
            ),
            ModpackEntryAddition(
                reference=ModpackListReference(kind="steam", value="818773962"),
            ),
        ),
    )

    assert render_modpack_list(updated.document) == (
        "# Libraries\n"
        "steam:2009463077  # Harmony\n"
        "steam:818773962\n"
    )


def test_generate_modpack_load_order_uses_metadata_rules_and_skips_disabled_entries(
    tmp_path: Path,
) -> None:
    pack_root = tmp_path / "LoadOrderPack"
    rimworld_root = tmp_path / "RimWorld"
    local_mods_root = pack_root / "local" / "mods"
    ordering_rules_path = pack_root / "rules" / "ordering" / "10_patches.toml"
    ordering_rules_path.parent.mkdir(parents=True, exist_ok=True)
    _ = ordering_rules_path.write_text(
        """\
[[rules]]
type = "after"
mod = "packageid:author.patch"
target = "packageid:author.addon"
""",
        encoding="utf-8",
    )

    write_about_xml(rimworld_root / "Data" / "Core", "ludeon.rimworld", "Core")
    write_about_xml(
        local_mods_root / "Addon",
        "author.addon",
        "Addon",
        extra_xml="""\
  <loadBefore>
    <li>author.framework</li>
  </loadBefore>""",
    )
    write_about_xml(local_mods_root / "Framework", "author.framework", "Framework")
    write_about_xml(
        local_mods_root / "Patch",
        "author.patch",
        "Patch",
        extra_xml="""\
  <loadAfter>
    <li>author.framework</li>
  </loadAfter>""",
    )
    write_about_xml(local_mods_root / "Disabled", "author.disabled", "Disabled")

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Load Order 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",
                ordering_rule_files=("rules/ordering/10_patches.toml",),
            ),
        ),
        mod_files=(
            ModpackFile(
                path="mods/00_core.list",
                document=new_modpack_list_document(()),
            ),
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list(
                    "local:Patch\nlocal:Framework\n!local:Disabled\nlocal:Addon\n",
                ),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    load_order = generate_modpack_load_order(
        modpack,
        root_directory=pack_root,
        resolution_roots=ModpackResolutionRoots(
            local_mods_dir=local_mods_root.resolve(),
            rimworld_data_folder=(rimworld_root / "Data").resolve(),
        ),
    )

    assert tuple(item.package_id for item in load_order) == (
        "author.addon",
        "author.framework",
        "author.patch",
    )


def test_generate_modpack_load_order_preserves_authored_order_without_constraints(
    tmp_path: Path,
) -> None:
    pack_root = tmp_path / "StableLoadOrderPack"
    local_mods_root = pack_root / "local" / "mods"

    write_about_xml(local_mods_root / "First", "author.first", "First")
    write_about_xml(local_mods_root / "Second", "author.second", "Second")
    write_about_xml(local_mods_root / "Third", "author.third", "Third")

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Stable Load Order Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/10_mods.list",)),
            paths=PackPaths(local_mods_dir="local/mods"),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list(
                    "local:Second\nlocal:First\nlocal:Third\n",
                ),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    load_order = generate_modpack_load_order(
        modpack,
        root_directory=pack_root,
        resolution_roots=ModpackResolutionRoots(local_mods_dir=local_mods_root.resolve()),
    )

    assert tuple(item.package_id for item in load_order) == (
        "author.second",
        "author.first",
        "author.third",
    )


def test_generate_modpack_load_order_applies_activation_rules_before_ordering(
    tmp_path: Path,
) -> None:
    pack_root = tmp_path / "ActivationLoadOrderPack"
    local_mods_root = pack_root / "local" / "mods"
    activation_rules_path = pack_root / "rules" / "activation" / "40_patches.toml"
    ordering_rules_path = pack_root / "rules" / "ordering" / "10_patch_order.toml"
    activation_rules_path.parent.mkdir(parents=True, exist_ok=True)
    ordering_rules_path.parent.mkdir(parents=True, exist_ok=True)
    _ = activation_rules_path.write_text(
        """\
[[rules]]
mod = "packageid:author.patch.active"
when_all_present = ["packageid:author.base"]

[[rules]]
mod = "packageid:author.patch.inactive"
when_all_present = ["packageid:author.missing"]
""",
        encoding="utf-8",
    )
    _ = ordering_rules_path.write_text(
        """\
[[rules]]
type = "after"
mod = "packageid:author.patch.active"
target = "packageid:author.base"

[[rules]]
type = "after"
mod = "packageid:author.patch.inactive"
target = "packageid:author.base"
""",
        encoding="utf-8",
    )

    write_about_xml(local_mods_root / "Base", "author.base", "Base")
    write_about_xml(local_mods_root / "PatchActive", "author.patch.active", "Patch Active")
    write_about_xml(
        local_mods_root / "PatchInactive",
        "author.patch.inactive",
        "Patch Inactive",
    )

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Activation Load Order Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/10_mods.list",)),
            paths=PackPaths(
                local_mods_dir="local/mods",
                activation_rule_files=("rules/activation/40_patches.toml",),
                ordering_rule_files=("rules/ordering/10_patch_order.toml",),
            ),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list(
                    "local:PatchActive\nlocal:PatchInactive\nlocal:Base\n",
                ),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    load_order = generate_modpack_load_order(
        modpack,
        root_directory=pack_root,
        resolution_roots=ModpackResolutionRoots(local_mods_dir=local_mods_root.resolve()),
    )

    assert tuple(item.package_id for item in load_order) == (
        "author.base",
        "author.patch.active",
    )


def test_generate_modpack_load_order_rejects_duplicate_activation_rules(
    tmp_path: Path,
) -> None:
    pack_root = tmp_path / "DuplicateActivationRulesPack"
    local_mods_root = pack_root / "local" / "mods"
    activation_rules_path = pack_root / "rules" / "activation" / "40_patches.toml"
    activation_rules_path.parent.mkdir(parents=True, exist_ok=True)
    _ = activation_rules_path.write_text(
        """\
[[rules]]
mod = "packageid:author.patch"
when_all_present = ["packageid:author.base"]

[[rules]]
mod = "local:Patch"
when_all_present = ["packageid:author.base"]
""",
        encoding="utf-8",
    )

    write_about_xml(local_mods_root / "Base", "author.base", "Base")
    write_about_xml(local_mods_root / "Patch", "author.patch", "Patch")

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Duplicate Activation Rules Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/10_mods.list",)),
            paths=PackPaths(
                local_mods_dir="local/mods",
                activation_rule_files=("rules/activation/40_patches.toml",),
            ),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list("local:Patch\nlocal:Base\n"),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    with pytest.raises(
        ModpackError,
        match=r"duplicate activation rule for resolved mod",
    ):
        _ = generate_modpack_load_order(
            modpack,
            root_directory=pack_root,
            resolution_roots=ModpackResolutionRoots(local_mods_dir=local_mods_root.resolve()),
        )


def test_generate_modpack_load_order_fails_when_enabled_entries_are_unresolved(
    tmp_path: Path,
) -> None:
    pack_root = tmp_path / "UnresolvedLoadOrderPack"
    local_mods_root = pack_root / "local" / "mods"

    write_about_xml(local_mods_root / "Resolved", "author.resolved", "Resolved")

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Unresolved Load Order Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/10_mods.list",)),
            paths=PackPaths(local_mods_dir="local/mods"),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list(
                    "local:Resolved\npackageid:missing.mod\n",
                ),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    with pytest.raises(
        ModpackError,
        match=r"is not available locally",
    ):
        _ = generate_modpack_load_order(
            modpack,
            root_directory=pack_root,
            resolution_roots=ModpackResolutionRoots(local_mods_dir=local_mods_root.resolve()),
        )


def test_validate_modpack_reports_unresolved_dependencies_and_incompatibilities(
    tmp_path: Path,
) -> None:
    pack_root = tmp_path / "ValidatePack"
    local_mods_root = pack_root / "local" / "mods"

    write_about_xml(
        local_mods_root / "NeedsDependency",
        "author.needsdependency",
        "Needs Dependency",
        extra_xml="""\
  <modDependencies>
    <li>
      <packageId>author.missingdependency</packageId>
      <displayName>Missing Dependency</displayName>
    </li>
  </modDependencies>""",
    )
    write_about_xml(
        local_mods_root / "Conflict",
        "author.conflict",
        "Conflict",
        extra_xml="""\
  <incompatibleWith>
    <li>author.needsdependency</li>
  </incompatibleWith>""",
    )

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Validate Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/10_mods.list",)),
            paths=PackPaths(local_mods_dir="local/mods"),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list(
                    "local:NeedsDependency\nlocal:Conflict\npackageid:missing.mod\n",
                ),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    result = validate_modpack(
        modpack,
        root_directory=pack_root,
        resolution_roots=ModpackResolutionRoots(local_mods_dir=local_mods_root.resolve()),
    )

    messages = {diagnostic.message for diagnostic in result.diagnostics}
    assert any("enabled mod entry is not available locally: packageid:missing.mod" in message for message in messages)
    assert "missing dependency for author.needsdependency: Missing Dependency" in messages
    assert "incompatible mods are both active: author.conflict and author.needsdependency" in messages
    assert tuple(entry.package_id for entry in result.load_order) == (
        "author.needsdependency",
        "author.conflict",
    )


def test_validate_modpack_reports_cycles_and_returns_no_load_order(
    tmp_path: Path,
) -> None:
    pack_root = tmp_path / "ValidateCyclePack"
    local_mods_root = pack_root / "local" / "mods"

    write_about_xml(local_mods_root / "Alpha", "author.alpha", "Alpha")
    write_about_xml(local_mods_root / "Beta", "author.beta", "Beta")
    ordering_rules_path = pack_root / "rules" / "ordering" / "10_cycle.toml"
    ordering_rules_path.parent.mkdir(parents=True, exist_ok=True)
    _ = ordering_rules_path.write_text(
        """\
[[rules]]
type = "before"
mod = "packageid:author.alpha"
target = "packageid:author.beta"

[[rules]]
type = "before"
mod = "packageid:author.beta"
target = "packageid:author.alpha"
""",
        encoding="utf-8",
    )

    modpack = Modpack(
        manifest=new_pack_toml_manifest(
            pack=PackInfo(
                name="Validate Cycle Pack",
                rimworld_version="1.6",
                pack_format_version=1,
            ),
            layout=PackLayout(mod_files=("mods/10_mods.list",)),
            paths=PackPaths(
                local_mods_dir="local/mods",
                ordering_rule_files=("rules/ordering/10_cycle.toml",),
            ),
        ),
        mod_files=(
            ModpackFile(
                path="mods/10_mods.list",
                document=parse_modpack_list("local:Alpha\nlocal:Beta\n"),
            ),
        ),
    )
    write_modpack(pack_root, modpack)

    result = validate_modpack(
        modpack,
        root_directory=pack_root,
        resolution_roots=ModpackResolutionRoots(local_mods_dir=local_mods_root.resolve()),
    )

    assert result.load_order == ()
    assert any(
        diagnostic.message == "load-order cycle detected among active mods: author.alpha, author.beta"
        for diagnostic in result.diagnostics
    )
