"""pm.plugins_state: complete, order-preserving cross-profile discovery."""

from __future__ import annotations

import re
from pathlib import Path

import pytest

import pm.plugins_state as pstate


@pytest.fixture
def homes(tmp_path, monkeypatch):
    """Default home + one profile, each with a config.yaml."""
    default_home = tmp_path / "default-home"
    profile_home = tmp_path / "profiles" / "work"
    default_home.mkdir(parents=True)
    profile_home.mkdir(parents=True)

    import hermes_constants

    monkeypatch.setattr(
        hermes_constants, "get_default_hermes_root", lambda: default_home
    )
    monkeypatch.setattr(pstate, "_profiles_root", lambda: tmp_path / "profiles")
    return default_home, profile_home


def _write_config(home: Path, enabled: list) -> None:
    import hermes_yaml as yaml

    config = {"plugins": {"enabled": enabled}} if enabled else {"plugins": {}}
    with (home / "config.yaml").open("w", encoding="utf-8") as f:
        yaml.safe_dump(config, f)


def test_enabled_plugins_ordered_reads_all_homes(homes):
    default_home, profile_home = homes
    _write_config(default_home, ["a-plug", "b-plug"])
    _write_config(profile_home, ["c-plug"])

    by_root = pstate.enabled_plugins_ordered()
    assert by_root.get(default_home / "plugins") == ["a-plug", "b-plug"]
    assert by_root.get(profile_home / "plugins") == ["c-plug"]


def test_only_live_profiles_join_the_dependency_union(homes):
    from hermes_constants import mark_named_profile_deleted

    default_home, profile_home = homes
    profiles = profile_home.parent
    _write_config(default_home, ["a-plug"])
    _write_config(profile_home, ["live-plug"])
    for name in (".work.staging-123", "Bad Name", "retired", "ghost"):
        (profiles / name).mkdir()
    for name in (".work.staging-123", "Bad Name", "retired"):
        _write_config(profiles / name, [f"{name}-plug"])
    mark_named_profile_deleted(profiles / "retired")
    (profiles / "ghost" / "plugins").mkdir()  # runtime side-effect dir, no identity marker

    assert pstate.dependency_homes() == [default_home, profile_home]
    by_root = pstate.enabled_plugins_ordered()
    assert set(by_root) == {default_home / "plugins", profile_home / "plugins"}


@pytest.mark.parametrize("boundary", ["profile-listing", "profile-stat", "plugin-stat", "manifest-read", "manifest-stat", "provider-stat"])
def test_unreadable_profile_state_is_not_an_empty_selection(homes, monkeypatch, boundary):
    from pm.workspace import enabled_member_dirs

    default_home, profile_home = homes
    _write_config(profile_home, ["keep-plug"])
    plugin = profile_home / "plugins" / "keep-plug"
    plugin.mkdir(parents=True)
    manifest = plugin / "plugin.yaml"
    manifest.write_text("python_dependencies: [fixture-dep]\n", encoding="utf-8")
    if boundary == "provider-stat":
        (profile_home / "config.yaml").write_text("memory:\n  provider: keep-plug\n", encoding="utf-8")
    method, target = {
        "profile-listing": ("iterdir", profile_home.parent),
        "profile-stat": ("stat", profile_home),
        "plugin-stat": ("stat", plugin),
        "manifest-read": ("read_text", manifest),
        "manifest-stat": ("stat", manifest),
        "provider-stat": ("stat", plugin),
    }[boundary]
    original = getattr(Path, method)

    def unreadable(path, *args, **kwargs):
        if path == target:
            raise PermissionError("access denied by fixture")
        return original(path, *args, **kwargs)

    monkeypatch.setattr(Path, method, unreadable)
    with pytest.raises(ValueError, match=re.escape(str(target))):
        enabled_member_dirs()


def test_missing_config_parser_is_not_an_empty_plugin_selection(homes, monkeypatch):
    import sys

    default_home, _ = homes
    _write_config(default_home, ["keep-plug"])
    before = (default_home / "config.yaml").read_bytes()
    monkeypatch.setitem(sys.modules, "utils", None)
    with pytest.raises(ImportError):
        pstate.enabled_plugins_ordered()
    assert (default_home / "config.yaml").read_bytes() == before



@pytest.mark.parametrize("content", ["{ not yaml", "[]", "plugins: wrong", "plugins:\n  enabled: wrong", "memory: wrong"])
def test_enabled_read_refuses_invalid_existing_config(homes, content):
    default_home, _ = homes
    config = default_home / "config.yaml"
    config.write_text(content, encoding="utf-8")
    before = config.read_bytes()
    with pytest.raises(ValueError, match=re.escape(str(config))):
        pstate.enabled_plugins_ordered()
    assert config.read_bytes() == before

def test_verify_skips_bad_secondary_but_admission_refuses_it(homes, caplog):
    from pm.workspace import enabled_member_dirs, enabled_plugin_dirs

    default_home, profile_home = homes
    plugin = default_home / "plugins" / "working"
    plugin.mkdir(parents=True)
    (plugin / "plugin.yaml").write_text("name: working\npython_dependencies: [requests]\n", encoding="utf-8")
    _write_config(default_home, ["working"])
    bad = profile_home / "config.yaml"
    bad.write_text("plugins: [invalid]\n", encoding="utf-8")

    assert enabled_member_dirs() == [plugin]
    assert str(bad) in caplog.text
    with pytest.raises(ValueError, match=re.escape(str(bad))):
        enabled_plugin_dirs(proposed_home=default_home, enabled=["working"])
    bad.write_text("plugins: {enabled: []}\n", encoding="utf-8")
    assert enabled_member_dirs() == [plugin]


@pytest.mark.parametrize("content", ["", "# empty config\n", "null", "{}", "plugins: {}", "plugins:\n  enabled: []"])
def test_empty_config_is_an_explicit_empty_selection(homes, content):
    default_home, _ = homes
    (default_home / "config.yaml").write_text(content, encoding="utf-8")
    assert pstate.enabled_plugins_ordered() == {}


@pytest.mark.parametrize("enabled, provider, exists, expected", [
    (["z-first", "a-second"], "provider", True, ["z-first", "a-second", "provider"]),
    (["provider"], "provider", True, ["provider"]),
    ([], "ghost", False, []),
])
def test_memory_provider_joins_ordered_selection(homes, enabled, provider, exists, expected):
    import hermes_yaml as yaml
    home, sibling = homes
    if exists:
        (home / "plugins" / provider).mkdir(parents=True)
    (home / "config.yaml").write_text(yaml.safe_dump({"plugins": {"enabled": enabled}, "memory": {"provider": provider}}))
    _write_config(sibling, ["sibling"])
    by_root = pstate.enabled_plugins_ordered()
    assert by_root.get(home / "plugins", []) == expected
    assert by_root[sibling / "plugins"] == ["sibling"]


def test_member_discovery_uses_real_enabled_config(homes):
    from pm.workspace import enabled_member_dirs
    home, _ = homes
    manifests = {
        "modern/pyproject.toml": "[project]\n",
        "legacy/plugin.yaml": 'name: legacy\npip_dependencies: ["requests>=2"]\n',
        "plain/plugin.yaml": "name: plain\n",
        "orphan/pyproject.toml": "[project]\n",
    }
    for relative, body in manifests.items():
        path = home / "plugins" / relative
        path.parent.mkdir(parents=True)
        path.write_text(body)
    _write_config(home, ["legacy", "modern", "plain"])
    assert enabled_member_dirs() == [home / "plugins/legacy", home / "plugins/modern"]
    _write_config(home, [])
    assert enabled_member_dirs() == []




def test_read_parses_config_once_per_home(homes, monkeypatch):
    """enabled_plugins_ordered must parse each home's config.yaml once,
    not once for plugins.enabled and again for memory.provider."""
    default_home, profile_home = homes
    _write_config(default_home, ["a-plug"])
    _write_config(profile_home, ["c-plug"])

    import utils

    calls: list = []
    real = utils.fast_safe_load

    def counting(stream):
        calls.append(stream)
        return real(stream)

    monkeypatch.setattr(utils, "fast_safe_load", counting)
    pstate.enabled_plugins_ordered()
    assert len(calls) == 2  # one per home, not one per query
