"""Pytest tests for anima.core.tool_resolver module.

Tests cover realistic configs, comma-separated parsing, error paths
beyond the contract doctests.
"""

import pytest

from anima.core.tool_resolver import (
    ToolPresetInput,
    parse_comma_separated,
    resolve_tool_presets,
    substitute_variables,
)


class TestResolveToolPresetsRealisticConfigs:
    """Realistic configuration scenarios for tool preset resolution."""

    def test_single_preset_basic(self):
        """Single preset with minimal fields should work."""
        presets = {
            "basic": ToolPresetInput(command="mcp-basic"),
        }
        result = resolve_tool_presets(["basic"], presets)
        assert len(result) == 1
        assert result[0]["command"] == "mcp-basic"
        assert result[0]["args"] == []
        assert result[0]["env"] == {}
        assert result[0]["sandbox"] is False
        assert result[0]["sandbox_allow"] == []

    def test_single_preset_full_config(self):
        """Single preset with all fields should work."""
        presets = {
            "docker": ToolPresetInput(
                command="mcp-docker",
                args=["--privileged", "--tcp"],
                env={"DOCKER_HOST": "unix:///var/run/docker.sock"},
                sandbox=True,
                sandbox_allow=["fs:/var/lib/docker", "net:registry.docker.com"],
            ),
        }
        result = resolve_tool_presets(["docker"], presets)
        assert result[0]["command"] == "mcp-docker"
        assert result[0]["args"] == ["--privileged", "--tcp"]
        assert result[0]["env"]["DOCKER_HOST"] == "unix:///var/run/docker.sock"
        assert result[0]["sandbox"] is True
        assert "fs:/var/lib/docker" in result[0]["sandbox_allow"]
        assert "net:registry.docker.com" in result[0]["sandbox_allow"]

    def test_multiple_presets_order_preserved(self):
        """Multiple presets should preserve order."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem"),
            "shell": ToolPresetInput(command="mcp-shell"),
            "docker": ToolPresetInput(command="mcp-docker"),
        }
        result = resolve_tool_presets(["fs", "shell", "docker"], presets)
        assert len(result) == 3
        assert result[0]["command"] == "mcp-filesystem"
        assert result[1]["command"] == "mcp-shell"
        assert result[2]["command"] == "mcp-docker"

    def test_complex_args_and_env(self):
        """Complex arguments and environment variables should be preserved."""
        presets = {
            "tool": ToolPresetInput(
                command="mcp-tool",
                args=[
                    "--root",
                    "/data",
                    "--verbose",
                    "--config",
                    "/etc/config.yaml",
                ],
                env={
                    "PATH": "/usr/local/bin:/usr/bin",
                    "DEBUG": "1",
                    "TOOL_CONFIG": "/etc/config.yaml",
                },
            ),
        }
        result = resolve_tool_presets(["tool"], presets)
        assert len(result[0]["args"]) == 5
        assert "-v" not in result[0]["args"]  # Check exact args
        assert result[0]["env"]["DEBUG"] == "1"

    def test_sandbox_with_network_allow(self):
        """Sandbox with network allow configuration."""
        presets = {
            "webtool": ToolPresetInput(
                command="mcp-webtool",
                sandbox=True,
                sandbox_allow=["net:api.example.com", "net:cdn.example.com"],
            ),
        }
        result = resolve_tool_presets(["webtool"], presets)
        assert result[0]["sandbox"] is True
        assert len(result[0]["sandbox_allow"]) == 2


class TestParseCommaSeparated:
    """Comma-separated string parsing tests."""

    def test_single_name(self):
        """Single name should return single-element list."""
        result = parse_comma_separated("fs")
        assert result == ["fs"]

    def test_two_names(self):
        """Two comma-separated names should work."""
        result = parse_comma_separated("fs,shell")
        assert result == ["fs", "shell"]

    def test_multiple_names(self):
        """Multiple comma-separated names should work."""
        result = parse_comma_separated("fs,shell,docker,git")
        assert result == ["fs", "shell", "docker", "git"]

    def test_whitespace_after_comma(self):
        """Whitespace after comma should be stripped."""
        result = parse_comma_separated("fs, shell, docker")
        assert result == ["fs", "shell", "docker"]

    def test_whitespace_before_comma(self):
        """Whitespace before comma should be stripped."""
        result = parse_comma_separated("fs ,shell ,docker")
        assert result == ["fs", "shell", "docker"]

    def test_whitespace_around_names(self):
        """Whitespace around names should be stripped."""
        result = parse_comma_separated("  fs  ,  shell  ,  docker  ")
        assert result == ["fs", "shell", "docker"]

    def test_empty_string(self):
        """Empty string should return empty list."""
        result = parse_comma_separated("")
        assert result == []

    def test_whitespace_only(self):
        """Whitespace-only string should return empty list."""
        result = parse_comma_separated("   ")
        assert result == []

    def test_trailing_comma(self):
        """Trailing comma should result in empty name stripped away."""
        result = parse_comma_separated("fs,shell,")
        # Empty strings after stripping are filtered out
        assert result == ["fs", "shell"]

    def test_leading_comma(self):
        """Leading comma should result in empty name stripped away."""
        result = parse_comma_separated(",fs,shell")
        assert result == ["fs", "shell"]

    def test_double_comma(self):
        """Double comma should work (empty name stripped)."""
        result = parse_comma_separated("fs,,shell")
        assert result == ["fs", "shell"]


class TestResolveToolPresetsCommaInList:
    """Comma-separated names embedded in list elements."""

    def test_comma_separated_single_element(self):
        """Single element with comma-separated names should expand."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem"),
            "shell": ToolPresetInput(command="mcp-shell"),
        }
        result = resolve_tool_presets(["fs,shell"], presets)
        assert len(result) == 2
        assert result[0]["command"] == "mcp-filesystem"
        assert result[1]["command"] == "mcp-shell"

    def test_comma_separated_multiple_elements(self):
        """Multiple elements with comma-separated names should expand all."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem"),
            "shell": ToolPresetInput(command="mcp-shell"),
            "docker": ToolPresetInput(command="mcp-docker"),
            "git": ToolPresetInput(command="mcp-git"),
        }
        result = resolve_tool_presets(["fs,shell", "docker,git"], presets)
        assert len(result) == 4
        commands = [r["command"] for r in result]
        assert commands == ["mcp-filesystem", "mcp-shell", "mcp-docker", "mcp-git"]

    def test_mixed_single_and_comma(self):
        """Mix of single names and comma-separated names should work."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem"),
            "shell": ToolPresetInput(command="mcp-shell"),
            "docker": ToolPresetInput(command="mcp-docker"),
        }
        result = resolve_tool_presets(["fs", "shell,docker"], presets)
        assert len(result) == 3


class TestResolveToolPresetsErrorPaths:
    """Error scenarios for tool resolver."""

    def test_unknown_preset_raises_value_error(self):
        """Unknown preset should raise ValueError with helpful message."""
        presets = {"fs": ToolPresetInput(command="mcp-filesystem")}
        with pytest.raises(ValueError) as exc_info:
            resolve_tool_presets(["unknown"], presets)
        assert "Unknown tool preset" in str(exc_info.value)
        assert "unknown" in str(exc_info.value)
        assert "fs" in str(exc_info.value)  # Available presets listed

    def test_unknown_preset_among_known(self):
        """Unknown preset in list of known should raise ValueError."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem"),
            "shell": ToolPresetInput(command="mcp-shell"),
        }
        with pytest.raises(ValueError) as exc_info:
            resolve_tool_presets(["fs", "unknown", "shell"], presets)
        assert "unknown" in str(exc_info.value)

    def test_unknown_in_comma_separated(self):
        """Unknown preset in comma-separated string should raise ValueError."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem"),
            "shell": ToolPresetInput(command="mcp-shell"),
        }
        with pytest.raises(ValueError) as exc_info:
            resolve_tool_presets(["fs,unknown,shell"], presets)
        assert "unknown" in str(exc_info.value)

    def test_empty_list_returns_empty_result(self):
        """Empty names list should return empty result."""
        presets = {"fs": ToolPresetInput(command="mcp-filesystem")}
        result = resolve_tool_presets([], presets)
        assert result == []

    def test_empty_presets_dict_raises_error_on_unknown(self):
        """Empty presets dict should raise ValueError for any name."""
        with pytest.raises(ValueError) as exc_info:
            resolve_tool_presets(["any"], {})
        assert "Unknown tool preset" in str(exc_info.value)


class TestToolPresetInputTypedDict:
    """ToolPresetInput TypedDict behavior and defaults."""

    def test_required_command_field(self):
        """Command field is required in ToolPresetInput."""
        preset = ToolPresetInput(command="mcp-tool")
        assert preset["command"] == "mcp-tool"

    def test_optional_args_default(self):
        """Args field is optional, defaults to empty list in resolution."""
        preset = ToolPresetInput(command="mcp-tool")
        result = resolve_tool_presets(["test"], {"test": preset})
        assert result[0]["args"] == []

    def test_optional_env_default(self):
        """Env field is optional, defaults to empty dict in resolution."""
        preset = ToolPresetInput(command="mcp-tool")
        result = resolve_tool_presets(["test"], {"test": preset})
        assert result[0]["env"] == {}

    def test_optional_sandbox_default(self):
        """Sandbox field is optional, defaults to False in resolution."""
        preset = ToolPresetInput(command="mcp-tool")
        result = resolve_tool_presets(["test"], {"test": preset})
        assert result[0]["sandbox"] is False

    def test_optional_sandbox_allow_default(self):
        """sandbox_allow field is optional, defaults to empty list in resolution."""
        preset = ToolPresetInput(command="mcp-tool")
        result = resolve_tool_presets(["test"], {"test": preset})
        assert result[0]["sandbox_allow"] == []

    def test_all_fields_preserved(self):
        """All provided fields should be preserved in resolution."""
        preset = ToolPresetInput(
            command="mcp-tool",
            args=["--verbose"],
            env={"DEBUG": "1"},
            sandbox=True,
            sandbox_allow=["fs:/tmp"],
        )
        result = resolve_tool_presets(["test"], {"test": preset})
        assert result[0]["command"] == "mcp-tool"
        assert result[0]["args"] == ["--verbose"]
        assert result[0]["env"]["DEBUG"] == "1"
        assert result[0]["sandbox"] is True
        assert result[0]["sandbox_allow"] == ["fs:/tmp"]


class TestResolvedToolTypedDict:
    """ResolvedTool TypedDict structure and usage."""

    def test_resolved_tool_structure(self):
        """ResolvedTool should have all expected fields."""
        presets = {
            "tool": ToolPresetInput(
                command="mcp-tool",
                args=["--flag"],
                env={"KEY": "value"},
                sandbox=True,
                sandbox_allow=["net:example.com"],
            ),
        }
        result = resolve_tool_presets(["tool"], presets)
        tool = result[0]

        # Check all fields exist and have correct types
        assert isinstance(tool["command"], str)
        assert isinstance(tool["args"], list)
        assert isinstance(tool["env"], dict)
        assert isinstance(tool["sandbox"], bool)
        assert isinstance(tool["sandbox_allow"], list)

    def test_resolved_tool_is_dict(self):
        """ResolvedTool is a TypedDict, should behave like dict."""
        presets = {"tool": ToolPresetInput(command="mcp-tool")}
        result = resolve_tool_presets(["tool"], presets)
        tool = result[0]

        # Can access like a dict
        assert "command" in tool
        assert tool.get("command") == "mcp-tool"
        assert tool.get("nonexistent") is None


class TestWhitespaceInPresetNames:
    """Whitespace handling in preset names."""

    def test_whitespace_in_comma_list(self):
        """Whitespace in comma-separated list should be stripped."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem"),
            "shell": ToolPresetInput(command="mcp-shell"),
        }
        result = resolve_tool_presets(["  fs  ,  shell  "], presets)
        assert len(result) == 2
        assert result[0]["command"] == "mcp-filesystem"
        assert result[1]["command"] == "mcp-shell"

    def test_single_name_with_whitespace_not_stripped(self):
        """Single name with whitespace is NOT stripped (raises ValueError).

        Whitespace stripping only happens for comma-separated names.
        Single names with whitespace will fail to match preset keys.
        """
        presets = {"fs": ToolPresetInput(command="mcp-filesystem")}
        # Whitespace in single names is not stripped - this will raise ValueError
        # because "  fs  " != "fs"
        with pytest.raises(ValueError) as exc_info:
            resolve_tool_presets(["  fs  "], presets)
        assert "fs" in str(exc_info.value)  # Available presets in error message

    def test_whitespace_preserves_order(self):
        """Whitespace stripping should preserve order."""
        presets = {
            "a": ToolPresetInput(command="mcp-a"),
            "b": ToolPresetInput(command="mcp-b"),
            "c": ToolPresetInput(command="mcp-c"),
        }
        result = resolve_tool_presets(["  a  ,  b  ,  c  "], presets)
        commands = [r["command"] for r in result]
        assert commands == ["mcp-a", "mcp-b", "mcp-c"]


class TestSubstituteVariablesBehavior:
    """Variable substitution behavior tests."""

    def test_basic_substitution(self):
        """Basic variable substitution should work."""
        result = substitute_variables("Hello $NAME", {"NAME": "World"})
        assert result == "Hello World"

    def test_multiple_variables(self):
        """Multiple different variables should all be substituted."""
        result = substitute_variables("$FOO and $BAR", {"FOO": "X", "BAR": "Y"})
        assert result == "X and Y"

    def test_no_variables_returns_unchanged(self):
        """Empty variables dict should return text unchanged."""
        result = substitute_variables("No variables here", {})
        assert result == "No variables here"

    def test_unknown_variable_unchanged(self):
        """Unknown variable should pass through unchanged."""
        result = substitute_variables("Hello $UNKNOWN", {"OTHER": "Value"})
        assert result == "Hello $UNKNOWN"

    def test_repeated_variable(self):
        """Same variable used multiple times should be replaced each time."""
        result = substitute_variables("$NAME $NAME", {"NAME": "Echo"})
        assert result == "Echo Echo"

    def test_case_sensitivity(self):
        """Variable substitution is case-sensitive."""
        result = substitute_variables(
            "Mixed $CASE and $Camel", {"case": "lower", "Camel": "camel"}
        )
        assert result == "Mixed $CASE and camel"

    def test_underscore_variable(self):
        """Variables can contain underscores."""
        result = substitute_variables("Leading $UNDER_", {"UNDER_": "score"})
        assert result == "Leading score"

    def test_alphanumeric_variable(self):
        """Variables can contain alphanumeric characters."""
        result = substitute_variables("Trailing $NUM123", {"NUM123": "456"})
        assert result == "Trailing 456"

    def test_escaped_dollar_sign(self):
        """Double dollar sign should pass through unchanged."""
        result = substitute_variables("Multiple $$escapes", {})
        assert result == "Multiple $$escapes"

    def test_empty_text(self):
        """Empty text should return empty string."""
        result = substitute_variables("", {"KEY": "value"})
        assert result == ""


class TestResolveToolPresetsSubstitutionBehavior:
    """Variable substitution through resolve_tool_presets."""

    def test_substitution_in_args(self):
        """Variables should be substituted in args field."""
        presets = {
            "fs": ToolPresetInput(
                command="mcp-filesystem",
                args=["--root", "$HOME"],
            ),
        }
        result = resolve_tool_presets(["fs"], presets, {"HOME": "/Users/test"})
        assert result[0]["args"] == ["--root", "/Users/test"]

    def test_substitution_in_sandbox_allow(self):
        """Variables should be substituted in sandbox_allow field."""
        presets = {
            "fs": ToolPresetInput(
                command="mcp-filesystem",
                sandbox=True,
                sandbox_allow=["fs:$DATA_DIR"],
            ),
        }
        result = resolve_tool_presets(["fs"], presets, {"DATA_DIR": "/tmp/data"})
        assert result[0]["sandbox_allow"] == ["fs:/tmp/data"]

    def test_variables_none_no_substitution(self):
        """variables=None should skip substitution (backward compatibility)."""
        presets = {
            "fs": ToolPresetInput(
                command="mcp-filesystem",
                args=["--root", "$UNKNOWN"],
            ),
        }
        result = resolve_tool_presets(["fs"], presets, None)
        assert result[0]["args"] == ["--root", "$UNKNOWN"]

    def test_empty_variables_no_substitution(self):
        """Empty variables dict should skip substitution."""
        presets = {
            "fs": ToolPresetInput(
                command="mcp-filesystem",
                args=["--root", "$UNKNOWN"],
            ),
        }
        result = resolve_tool_presets(["fs"], presets, {})
        assert result[0]["args"] == ["--root", "$UNKNOWN"]

    def test_substitution_with_comma_separated(self):
        """Variable substitution should work with comma-separated presets."""
        presets = {
            "fs": ToolPresetInput(command="mcp-filesystem", args=["--root", "$HOME"]),
            "shell": ToolPresetInput(
                command="mcp-shell", env={"SHELL_TYPE": "$SHELL_TYPE"}
            ),
        }
        result = resolve_tool_presets(
            ["fs,shell"], presets, {"HOME": "/home/user", "SHELL_TYPE": "bash"}
        )
        # args are substituted
        assert result[0]["args"] == ["--root", "/home/user"]
        # env values are NOT substituted (current behavior)
        assert result[1]["env"]["SHELL_TYPE"] == "$SHELL_TYPE"

    def test_substitution_multiple_vars(self):
        """Multiple variables in same field should all be substituted."""
        presets = {
            "tool": ToolPresetInput(
                command="mcp-tool",
                args=["--config", "$CONFIG_DIR/app.yaml", "--data", "$DATA_DIR"],
            ),
        }
        result = resolve_tool_presets(
            ["tool"],
            presets,
            {"CONFIG_DIR": "/etc", "DATA_DIR": "/var/data"},
        )
        assert result[0]["args"] == ["--config", "/etc/app.yaml", "--data", "/var/data"]

    def test_substitution_env_not_affected(self):
        """Variable substitution only applies to args and sandbox_allow, not env."""
        presets = {
            "tool": ToolPresetInput(
                command="mcp-tool",
                env={"PATH": "$PATH"},
            ),
        }
        # Note: env values are NOT substituted - this test verifies current behavior
        result = resolve_tool_presets(["tool"], presets, {"PATH": "/custom/path"})
        assert result[0]["env"]["PATH"] == "$PATH"

    def test_substitution_preserves_other_fields(self):
        """Substitution should not affect command, env, sandbox fields."""
        presets = {
            "fs": ToolPresetInput(
                command="mcp-filesystem",
                args=["--root", "$HOME"],
                env={"DEBUG": "1"},
                sandbox=True,
                sandbox_allow=["fs:$DATA"],
            ),
        }
        result = resolve_tool_presets(
            ["fs"],
            presets,
            {"HOME": "/home", "DATA": "/data"},
        )
        assert result[0]["command"] == "mcp-filesystem"
        assert result[0]["env"] == {"DEBUG": "1"}
        assert result[0]["sandbox"] is True
        assert result[0]["args"] == ["--root", "/home"]
        assert result[0]["sandbox_allow"] == ["fs:/data"]
