"""Tests for ievo.core.deps — MCP/plugin dependency system."""

from __future__ import annotations

import json
from pathlib import Path

from ievo.core.deps import (
    EnvVarSpec,
    McpDependency,
    McpType,
    PluginDependency,
    check_mcp_deps,
    check_plugin_deps,
    ensure_mcp_configured,
)

# ── McpType ──────────────────────────────────────────────────


def test_mcp_type_values() -> None:
    assert McpType.BUILTIN == "builtin"
    assert McpType.NPM == "npm"
    assert McpType.PIP == "pip"
    assert McpType.HTTP == "http"


# ── EnvVarSpec ───────────────────────────────────────────────


def test_env_var_spec_defaults() -> None:
    e = EnvVarSpec(name="API_KEY")
    assert e.name == "API_KEY"
    assert e.description == ""
    assert e.required is True


# ── McpDependency ────────────────────────────────────────────


def test_mcp_dependency_builtin() -> None:
    m = McpDependency.from_dict({"name": "filesystem", "type": "builtin"})
    assert m.name == "filesystem"
    assert m.type == McpType.BUILTIN
    assert m.package is None
    assert m.env == []


def test_mcp_dependency_legacy_builtin() -> None:
    """Support old format: builtin: true instead of type: builtin."""
    m = McpDependency.from_dict({"name": "filesystem", "builtin": True})
    assert m.type == McpType.BUILTIN


def test_mcp_dependency_npm() -> None:
    m = McpDependency.from_dict(
        {
            "name": "pubmed",
            "type": "npm",
            "package": "@cyanheads/pubmed-mcp-server",
            "env": {
                "NCBI_API_KEY": {"description": "PubMed API key", "required": True},
            },
        }
    )
    assert m.type == McpType.NPM
    assert m.package == "@cyanheads/pubmed-mcp-server"
    assert len(m.env) == 1
    assert m.env[0].name == "NCBI_API_KEY"
    assert m.env[0].description == "PubMed API key"
    assert m.env[0].required is True


def test_mcp_dependency_pip() -> None:
    m = McpDependency.from_dict(
        {
            "name": "atlassian",
            "type": "pip",
            "package": "mcp-atlassian",
        }
    )
    assert m.type == McpType.PIP
    assert m.package == "mcp-atlassian"


def test_mcp_dependency_http() -> None:
    m = McpDependency.from_dict(
        {
            "name": "greptile",
            "type": "http",
            "url": "https://api.greptile.com/mcp",
            "headers": {"Authorization": "Bearer ${TOKEN}"},
        }
    )
    assert m.type == McpType.HTTP
    assert m.url == "https://api.greptile.com/mcp"
    assert "Authorization" in m.headers


def test_mcp_dependency_to_dict() -> None:
    m = McpDependency(
        name="pubmed",
        type=McpType.NPM,
        package="@cyanheads/pubmed-mcp-server",
        env=[EnvVarSpec(name="API_KEY", description="Key", required=True)],
    )
    d = m.to_dict()
    assert d["name"] == "pubmed"
    assert d["type"] == "npm"
    assert d["package"] == "@cyanheads/pubmed-mcp-server"
    assert "API_KEY" in d["env"]
    assert d["env"]["API_KEY"]["description"] == "Key"


def test_mcp_dependency_to_dict_minimal() -> None:
    m = McpDependency(name="fs", type=McpType.BUILTIN)
    d = m.to_dict()
    assert d == {"name": "fs", "type": "builtin"}
    assert "package" not in d
    assert "env" not in d


def test_mcp_roundtrip() -> None:
    original = {
        "name": "test",
        "type": "npm",
        "package": "@test/mcp",
        "args": ["--port", "3000"],
        "env": {"KEY": {"description": "desc", "required": False}},
    }
    m = McpDependency.from_dict(original)
    d = m.to_dict()
    assert d["name"] == "test"
    assert d["package"] == "@test/mcp"
    assert d["args"] == ["--port", "3000"]
    assert d["env"]["KEY"]["required"] is False


# ── PluginDependency ─────────────────────────────────────────


def test_plugin_dependency_from_string() -> None:
    p = PluginDependency.from_dict("claude-mem")
    assert p.name == "claude-mem"
    assert p.marketplace == "claude-plugins-official"
    assert p.required is True


def test_plugin_dependency_from_dict() -> None:
    p = PluginDependency.from_dict(
        {
            "name": "claude-mem",
            "marketplace": "thedotmack",
            "description": "Persistent memory",
            "required": False,
        }
    )
    assert p.name == "claude-mem"
    assert p.marketplace == "thedotmack"
    assert p.description == "Persistent memory"
    assert p.required is False


def test_plugin_dependency_to_dict() -> None:
    p = PluginDependency(name="greptile", marketplace="claude-plugins-official")
    d = p.to_dict()
    assert d["name"] == "greptile"
    assert d["marketplace"] == "claude-plugins-official"
    assert "required" not in d  # only serialized when False


def test_plugin_dependency_to_dict_optional() -> None:
    p = PluginDependency(name="test", required=False)
    d = p.to_dict()
    assert d["required"] is False


# ── check_mcp_deps ───────────────────────────────────────────


def test_check_mcp_builtin(tmp_path: Path) -> None:
    deps = [McpDependency(name="filesystem", type=McpType.BUILTIN)]
    results = check_mcp_deps(tmp_path, deps)
    assert len(results) == 1
    assert results[0].satisfied is True
    assert results[0].message == "builtin"


def test_check_mcp_missing(tmp_path: Path) -> None:
    deps = [McpDependency(name="pubmed", type=McpType.NPM, package="@test/mcp")]
    results = check_mcp_deps(tmp_path, deps)
    assert len(results) == 1
    assert results[0].satisfied is False
    assert results[0].fixable is True


def test_check_mcp_configured(tmp_path: Path) -> None:
    # Write .mcp.json with pubmed configured
    config = {"mcpServers": {"pubmed": {"type": "stdio", "command": "npx"}}}
    (tmp_path / ".mcp.json").write_text(json.dumps(config))

    deps = [McpDependency(name="pubmed", type=McpType.NPM, package="@test/mcp")]
    results = check_mcp_deps(tmp_path, deps)
    assert results[0].satisfied is True
    assert results[0].message == "configured"


def test_check_mcp_missing_env(tmp_path: Path, monkeypatch) -> None:
    config = {"mcpServers": {"pubmed": {"type": "stdio", "command": "npx"}}}
    (tmp_path / ".mcp.json").write_text(json.dumps(config))

    # Ensure env var is NOT set
    monkeypatch.delenv("NCBI_API_KEY", raising=False)

    deps = [
        McpDependency(
            name="pubmed",
            type=McpType.NPM,
            package="@test/mcp",
            env=[EnvVarSpec(name="NCBI_API_KEY", required=True)],
        )
    ]
    results = check_mcp_deps(tmp_path, deps)
    assert results[0].satisfied is False
    assert "NCBI_API_KEY" in results[0].message


def test_check_mcp_env_present(tmp_path: Path, monkeypatch) -> None:
    config = {"mcpServers": {"pubmed": {"type": "stdio", "command": "npx"}}}
    (tmp_path / ".mcp.json").write_text(json.dumps(config))

    monkeypatch.setenv("NCBI_API_KEY", "test-value")

    deps = [
        McpDependency(
            name="pubmed",
            type=McpType.NPM,
            package="@test/mcp",
            env=[EnvVarSpec(name="NCBI_API_KEY", required=True)],
        )
    ]
    results = check_mcp_deps(tmp_path, deps)
    assert results[0].satisfied is True


# ── check_plugin_deps ────────────────────────────────────────


def test_check_plugin_returns_advisory() -> None:
    plugins = [PluginDependency(name="claude-mem", marketplace="thedotmack")]
    results = check_plugin_deps(plugins)
    assert len(results) == 1
    assert results[0].dep_type == "plugin"
    assert "/plugin install" in results[0].fix_command


def test_check_plugin_empty() -> None:
    results = check_plugin_deps([])
    assert results == []


# ── ensure_mcp_configured ────────────────────────────────────


def test_ensure_creates_mcp_json(tmp_path: Path) -> None:
    deps = [
        McpDependency(
            name="pubmed",
            type=McpType.NPM,
            package="@cyanheads/pubmed-mcp-server",
        )
    ]
    ensure_mcp_configured(tmp_path, deps)

    mcp_path = tmp_path / ".mcp.json"
    assert mcp_path.exists()

    config = json.loads(mcp_path.read_text())
    assert "pubmed" in config["mcpServers"]
    server = config["mcpServers"]["pubmed"]
    assert server["type"] == "stdio"
    assert server["command"] == "npx"
    assert "-y" in server["args"]
    assert "@cyanheads/pubmed-mcp-server" in server["args"]


def test_ensure_pip_uses_uvx(tmp_path: Path) -> None:
    deps = [
        McpDependency(
            name="atlassian",
            type=McpType.PIP,
            package="mcp-atlassian",
        )
    ]
    ensure_mcp_configured(tmp_path, deps)

    config = json.loads((tmp_path / ".mcp.json").read_text())
    server = config["mcpServers"]["atlassian"]
    assert server["command"] == "uvx"
    assert "mcp-atlassian" in server["args"]


def test_ensure_http(tmp_path: Path) -> None:
    deps = [
        McpDependency(
            name="greptile",
            type=McpType.HTTP,
            url="https://api.greptile.com/mcp",
            headers={"Authorization": "Bearer ${TOKEN}"},
        )
    ]
    ensure_mcp_configured(tmp_path, deps)

    config = json.loads((tmp_path / ".mcp.json").read_text())
    server = config["mcpServers"]["greptile"]
    assert server["type"] == "http"
    assert server["url"] == "https://api.greptile.com/mcp"
    assert server["headers"]["Authorization"] == "Bearer ${TOKEN}"


def test_ensure_skips_builtin(tmp_path: Path) -> None:
    deps = [McpDependency(name="filesystem", type=McpType.BUILTIN)]
    ensure_mcp_configured(tmp_path, deps)
    assert not (tmp_path / ".mcp.json").exists()


def test_ensure_skips_existing(tmp_path: Path) -> None:
    # Pre-existing config
    config = {"mcpServers": {"pubmed": {"type": "stdio", "command": "custom"}}}
    (tmp_path / ".mcp.json").write_text(json.dumps(config))

    deps = [
        McpDependency(
            name="pubmed",
            type=McpType.NPM,
            package="@test/mcp",
        )
    ]
    ensure_mcp_configured(tmp_path, deps)

    # Should NOT overwrite existing config
    updated = json.loads((tmp_path / ".mcp.json").read_text())
    assert updated["mcpServers"]["pubmed"]["command"] == "custom"


def test_ensure_adds_env_vars(tmp_path: Path, monkeypatch) -> None:
    monkeypatch.delenv("API_KEY", raising=False)

    deps = [
        McpDependency(
            name="test-server",
            type=McpType.NPM,
            package="@test/mcp",
            env=[EnvVarSpec(name="API_KEY", description="Test key", required=True)],
        )
    ]
    warnings = ensure_mcp_configured(tmp_path, deps)

    config = json.loads((tmp_path / ".mcp.json").read_text())
    assert config["mcpServers"]["test-server"]["env"]["API_KEY"] == "${API_KEY}"
    assert len(warnings) == 1
    assert "API_KEY" in warnings[0]


def test_ensure_no_warning_when_env_set(tmp_path: Path, monkeypatch) -> None:
    monkeypatch.setenv("API_KEY", "secret")

    deps = [
        McpDependency(
            name="test-server",
            type=McpType.NPM,
            package="@test/mcp",
            env=[EnvVarSpec(name="API_KEY", required=True)],
        )
    ]
    warnings = ensure_mcp_configured(tmp_path, deps)
    assert warnings == []


def test_ensure_preserves_other_servers(tmp_path: Path) -> None:
    config = {"mcpServers": {"existing": {"type": "stdio", "command": "foo"}}}
    (tmp_path / ".mcp.json").write_text(json.dumps(config))

    deps = [McpDependency(name="new", type=McpType.NPM, package="@test/new")]
    ensure_mcp_configured(tmp_path, deps)

    updated = json.loads((tmp_path / ".mcp.json").read_text())
    assert "existing" in updated["mcpServers"]
    assert "new" in updated["mcpServers"]


def test_ensure_with_args(tmp_path: Path) -> None:
    deps = [
        McpDependency(
            name="custom",
            type=McpType.NPM,
            package="@test/mcp",
            args=["--port", "3000"],
        )
    ]
    ensure_mcp_configured(tmp_path, deps)

    config = json.loads((tmp_path / ".mcp.json").read_text())
    args = config["mcpServers"]["custom"]["args"]
    assert args == ["-y", "@test/mcp", "--port", "3000"]


# ── AgentPackage integration ─────────────────────────────────


def test_agent_package_with_mcp_roundtrip(tmp_path: Path) -> None:
    """Verify AgentPackage load/save with typed mcp/plugins."""
    import yaml as _yaml

    from ievo.core.agent import AgentPackage

    agent_yaml = {
        "name": "research-agent",
        "mcp": [
            {"name": "filesystem", "type": "builtin"},
            {"name": "pubmed", "type": "npm", "package": "@test/pubmed"},
        ],
        "plugins": [
            {"name": "claude-mem", "marketplace": "thedotmack"},
            "greptile",  # legacy string format
        ],
    }
    (tmp_path / "agent.yaml").write_text(_yaml.dump(agent_yaml, sort_keys=False))

    pkg = AgentPackage.load(tmp_path)
    assert len(pkg.mcp) == 2
    assert pkg.mcp[0].type == McpType.BUILTIN
    assert pkg.mcp[1].type == McpType.NPM
    assert pkg.mcp[1].package == "@test/pubmed"

    assert len(pkg.plugins) == 2
    assert pkg.plugins[0].name == "claude-mem"
    assert pkg.plugins[0].marketplace == "thedotmack"
    assert pkg.plugins[1].name == "greptile"
    assert pkg.plugins[1].marketplace == "claude-plugins-official"

    # Roundtrip
    pkg.save(tmp_path)
    loaded = AgentPackage.load(tmp_path)
    assert len(loaded.mcp) == 2
    assert len(loaded.plugins) == 2
    assert loaded.mcp[1].package == "@test/pubmed"


# ── Additional gap-closing tests ─────────────────────────────


def test_mcp_dependency_env_simple_value() -> None:
    """Env value as non-dict (e.g. string) creates EnvVarSpec with name only."""
    m = McpDependency.from_dict(
        {
            "name": "test",
            "type": "npm",
            "package": "@test/mcp",
            "env": {"API_KEY": "just-a-string"},
        }
    )
    assert len(m.env) == 1
    assert m.env[0].name == "API_KEY"
    assert m.env[0].description == ""


def test_mcp_dependency_to_dict_http() -> None:
    """to_dict serializes HTTP type with url and headers."""
    m = McpDependency(
        name="greptile",
        type=McpType.HTTP,
        url="https://api.greptile.com/mcp",
        headers={"Authorization": "Bearer ${TOKEN}"},
    )
    d = m.to_dict()
    assert d["url"] == "https://api.greptile.com/mcp"
    assert d["headers"]["Authorization"] == "Bearer ${TOKEN}"
    assert "package" not in d


def test_plugin_dependency_to_dict_with_description() -> None:
    """to_dict includes description when set."""
    p = PluginDependency(
        name="claude-mem",
        marketplace="thedotmack",
        description="Persistent memory plugin",
    )
    d = p.to_dict()
    assert d["description"] == "Persistent memory plugin"


def test_mcp_dependency_env_as_list() -> None:
    """env as non-dict (e.g. list) is silently skipped."""
    m = McpDependency.from_dict(
        {
            "name": "test",
            "type": "npm",
            "package": "@test/mcp",
            "env": ["API_KEY", "SECRET"],
        }
    )
    assert m.env == []


def test_ensure_mcp_http_no_headers(tmp_path: Path) -> None:
    """HTTP MCP without headers creates server config without headers key."""
    mcp_json = tmp_path / ".mcp.json"
    mcp_json.write_text(json.dumps({"mcpServers": {}}))

    dep = McpDependency(
        name="http-api",
        type=McpType.HTTP,
        url="https://api.example.com/mcp",
    )
    warnings = ensure_mcp_configured(tmp_path, [dep])
    data = json.loads(mcp_json.read_text())
    server = data["mcpServers"]["http-api"]
    assert server["type"] == "http"
    assert server["url"] == "https://api.example.com/mcp"
    assert "headers" not in server
    assert warnings == []
