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

Tests cover edge cases, overlap behavior, and integration scenarios
beyond the contract doctests.
"""

import json

import pytest

from anima.core.sandbox import (
    NetworkBlockErrorInfo,
    SandboxPolicy,
    build_srt_config,
    format_network_block_error,
    merge_sandbox_allows,
    parse_sandbox_allow,
    resolve_sandbox_policy,
    wrap_command,
)


class TestParseSandboxAllowEdgeCases:
    """Edge cases for parse_sandbox_allow beyond basic contracts."""

    def test_empty_list_creates_enabled_policy(self):
        """Empty allows list should create enabled policy with empty permits."""
        policy = parse_sandbox_allow([])
        assert policy.enabled is True
        assert policy.allow_fs == []
        assert policy.allow_net == []

    def test_multiple_fs_paths_deduped(self):
        """Duplicate fs paths should be deduped while preserving order."""
        policy = parse_sandbox_allow(["fs:/tmp", "fs:/var", "fs:/tmp"])
        assert policy.allow_fs == ["/tmp", "/var"]

    def test_multiple_net_domains_deduped(self):
        """Duplicate net domains should be deduped while preserving order."""
        policy = parse_sandbox_allow(
            ["net:example.com", "net:api.example.com", "net:example.com"]
        )
        assert policy.allow_net == ["example.com", "api.example.com"]

    def test_net_star_and_domain_both_allowed(self):
        """Both net (star) and net:domain can coexist."""
        policy = parse_sandbox_allow(["net", "net:example.com"])
        # net -> ["*"] (full network)
        # net:example.com -> specific domain
        assert "*" in policy.allow_net
        assert "example.com" in policy.allow_net

    def test_whitespace_handling_in_paths(self):
        """Whitespace in fs paths should be stripped."""
        policy = parse_sandbox_allow(["fs:  /tmp/data  "])
        assert policy.allow_fs == ["/tmp/data"]

    def test_whitespace_handling_in_domains(self):
        """Whitespace in domains should be stripped."""
        policy = parse_sandbox_allow(["net:  api.example.com  "])
        assert policy.allow_net == ["api.example.com"]

    def test_ordering_preserved(self):
        """Order of capabilities should be preserved."""
        policy = parse_sandbox_allow(["fs:/var", "net:example.com", "fs:/tmp"])
        assert policy.allow_fs == ["/var", "/tmp"]
        assert policy.allow_net == ["example.com"]

    def test_complex_path_with_spaces(self):
        """Paths with legitimate spaces (after stripping) should work."""
        policy = parse_sandbox_allow(["fs:/tmp/my path"])
        assert policy.allow_fs == ["/tmp/my path"]

    def test_domain_with_subdomain(self):
        """Domains with subdomains should be preserved correctly."""
        policy = parse_sandbox_allow(["net:api.sub.example.com"])
        assert policy.allow_net == ["api.sub.example.com"]


class TestMergeSandboxAllowsOverlap:
    """Overlap and merging behavior for sandbox allow lists."""

    def test_cli_overlaps_config_deduped(self):
        """Overlapping entries between CLI and config should be deduped."""
        result = merge_sandbox_allows(
            ["fs:/tmp", "net"], ["fs:/tmp", "net:example.com"]
        )
        # First occurrence wins (CLI)
        assert result == ["fs:/tmp", "net", "net:example.com"]

    def test_config_only_entries_preserved(self):
        """Entries only in config should be preserved."""
        result = merge_sandbox_allows(["fs:/tmp"], ["fs:/var", "net"])
        assert "fs:/var" in result
        assert "net" in result

    def test_cli_only_entries_preserved(self):
        """Entries only in CLI should be preserved."""
        result = merge_sandbox_allows(["fs:/tmp", "net"], ["fs:/var"])
        assert "fs:/tmp" in result
        assert "net" in result

    def test_order_preserved_cli_first(self):
        """Order should preserve CLI entries first, then config entries."""
        result = merge_sandbox_allows(["fs:/tmp"], ["net", "fs:/var"])
        assert result == ["fs:/tmp", "net", "fs:/var"]

    def test_multiple_overlaps(self):
        """Multiple overlapping entries should all be deduped."""
        cli = ["fs:/tmp", "net", "fs:/var"]
        config = ["fs:/tmp", "net:example.com", "net"]
        result = merge_sandbox_allows(cli, config)
        assert result == ["fs:/tmp", "net", "fs:/var", "net:example.com"]

    def test_both_empty_returns_empty(self):
        """Both lists empty should return empty."""
        result = merge_sandbox_allows([], [])
        assert result == []

    def test_cli_empty_returns_config(self):
        """Empty CLI should return config entries."""
        result = merge_sandbox_allows([], ["fs:/tmp"])
        assert result == ["fs:/tmp"]

    def test_config_empty_returns_cli(self):
        """Empty config should return CLI entries."""
        result = merge_sandbox_allows(["net"], [])
        assert result == ["net"]


class TestBuildSrtConfigStructure:
    """Structure and field validation for build_srt_config."""

    def test_basic_structure(self):
        """Config dict should have required fields."""
        policy = SandboxPolicy(enabled=True, allow_fs=["/tmp"], allow_net=["*"])
        config = build_srt_config(policy)
        assert "allow_fs" in config
        assert "allow_net" in config
        assert isinstance(config["allow_fs"], list)
        assert isinstance(config["allow_net"], list)

    def test_disabled_policy_still_returns_config(self):
        """Even disabled policy should return valid config structure."""
        policy = SandboxPolicy(enabled=False, allow_fs=[], allow_net=[])
        config = build_srt_config(policy)
        assert config["allow_fs"] == []
        assert config["allow_net"] == []

    def test_empty_lists_copied_not_referenced(self):
        """Returned lists should be copies, not references to policy dataclass fields."""
        policy = SandboxPolicy(
            enabled=True, allow_fs=["/tmp"], allow_net=["example.com"]
        )
        config = build_srt_config(policy)
        # Modify the returned lists
        config["allow_fs"].append("/var")
        config["allow_net"].append("other.com")
        # Original policy should be unchanged
        assert policy.allow_fs == ["/tmp"]
        assert policy.allow_net == ["example.com"]

    def test_multiple_paths_and_domains(self):
        """Config should preserve all paths and domains."""
        policy = SandboxPolicy(
            enabled=True,
            allow_fs=["/tmp", "/var", "/home/user"],
            allow_net=["example.com", "api.example.com", "cdn.example.com"],
        )
        config = build_srt_config(policy)
        assert len(config["allow_fs"]) == 3
        assert len(config["allow_net"]) == 3


class TestWrapCommandBehavior:
    """wrap_command behavior tests (mocking srt dependency)."""

    def test_disabled_policy_returns_unmodified_command(self):
        """Disabled policy should return command unchanged."""
        policy = SandboxPolicy(enabled=False)
        result = wrap_command(["python", "-V"], policy)
        assert result == ["python", "-V"]

    def test_enabled_policy_creates_srt_wrapper(self):
        """Enabled policy should wrap command with srt."""
        policy = SandboxPolicy(enabled=True, allow_fs=["/tmp"], allow_net=["*"])
        result = wrap_command(["python", "-V"], policy)
        assert result[0] == "srt"
        assert result[1] == "exec"
        assert result[2] == "--config"
        assert result[4] == "--"
        assert result[5] == "python"
        assert result[6] == "-V"

    def test_config_json_format(self):
        """Config JSON should be compact (no spaces)."""
        policy = SandboxPolicy(
            enabled=True, allow_fs=["/tmp"], allow_net=["example.com"]
        )
        result = wrap_command(["python", "-V"], policy)
        config_json = result[3]
        # Should be compact JSON (no spaces after separators)
        assert ", " not in config_json
        assert ": " not in config_json
        # Should parse back correctly
        parsed = json.loads(config_json)
        assert parsed["allow_fs"] == ["/tmp"]
        assert parsed["allow_net"] == ["example.com"]

    def test_empty_allow_lists_in_config(self):
        """Empty allow lists should produce valid JSON config."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=[])
        result = wrap_command(["python", "-V"], policy)
        config_json = result[3]
        parsed = json.loads(config_json)
        assert parsed["allow_fs"] == []
        assert parsed["allow_net"] == []

    def test_complex_command_preserved(self):
        """Complex command with multiple arguments should be preserved."""
        policy = SandboxPolicy(enabled=True, allow_fs=["/tmp"])
        result = wrap_command(
            ["python", "-m", "pytest", "tests/", "-v", "--tb=short"], policy
        )
        # Command should start after '--'
        separator_idx = result.index("--")
        command = result[separator_idx + 1 :]
        assert command == ["python", "-m", "pytest", "tests/", "-v", "--tb=short"]


class TestFormatNetworkBlockError:
    """Network block error formatting tests."""

    def test_connection_refused_pattern(self):
        """Connection refused errors should be detected and return structured info."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=[])
        result = format_network_block_error("Connection refused", policy)
        assert result is not None
        assert isinstance(result, NetworkBlockErrorInfo)
        assert result.error_message == "Connection refused"
        assert result.is_sandbox_related is True
        assert "net" in result.suggestion

    def test_enetunreach_pattern(self):
        """ENETUNREACH errors should be detected."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=[])
        result = format_network_block_error(
            "ENETUNREACH: Network is unreachable", policy
        )
        assert result is not None
        assert isinstance(result, NetworkBlockErrorInfo)

    def test_non_network_error_returns_none(self):
        """Non-network errors should return None."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=[])
        result = format_network_block_error("File not found", policy)
        assert result is None

    def test_disabled_policy_returns_none(self):
        """Disabled sandbox should not suggest network block."""
        policy = SandboxPolicy(enabled=False)
        result = format_network_block_error("Connection refused", policy)
        assert result is None

    def test_shows_current_domains(self):
        """Should return currently allowed domains if specific ones are set."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=["api.example.com"])
        result = format_network_block_error("socket error", policy)
        assert result is not None
        assert result.allowed_domains == ["api.example.com"]

    def test_full_network_not_shown(self):
        """Should not include wildcard in allowed_domains list."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=["*"])
        result = format_network_block_error("socket error", policy)
        assert result is not None
        assert result.allowed_domains == []

    def test_mixed_case_patterns(self):
        """Mixed case error messages should be detected."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=[])
        result = format_network_block_error("NETWORK IS UNREACHABLE", policy)
        assert result is not None
        assert isinstance(result, NetworkBlockErrorInfo)

    def test_socket_pattern(self):
        """Socket-related errors should be detected."""
        policy = SandboxPolicy(enabled=True, allow_fs=[], allow_net=[])
        result = format_network_block_error("socket: Operation not permitted", policy)
        assert result is not None
        assert isinstance(result, NetworkBlockErrorInfo)


class TestResolveSandboxPolicy:
    """Tests for resolve_sandbox_policy shared logic."""

    def test_all_disabled(self):
        """No flags, no presets -> disabled."""
        result = resolve_sandbox_policy(False, False, [], False, [])
        assert result.enabled is False

    def test_sandbox_flag_enables(self):
        """--sandbox flag enables sandbox."""
        result = resolve_sandbox_policy(True, False, [], False, [])
        assert result.enabled is True
        assert result.allow_fs == []
        assert result.allow_net == []

    def test_no_sandbox_overrides_all(self):
        """--no-sandbox overrides --sandbox and preset."""
        result = resolve_sandbox_policy(False, True, [], True, ["net:example.com"])
        assert result.enabled is False

    def test_sandbox_allow_implies_sandbox(self):
        """--sandbox-allow without --sandbox implies sandbox enabled."""
        result = resolve_sandbox_policy(False, False, ["fs:/tmp"], False, [])
        assert result.enabled is True
        assert result.allow_fs == ["/tmp"]

    def test_preset_sandbox_enables(self):
        """Preset sandbox=true enables sandbox."""
        result = resolve_sandbox_policy(False, False, [], True, [])
        assert result.enabled is True

    def test_preset_allows_merged(self):
        """Preset sandbox_allow values are merged with CLI values."""
        result = resolve_sandbox_policy(
            True, False, ["fs:/tmp"], True, ["net:example.com"]
        )
        assert result.enabled is True
        assert "/tmp" in result.allow_fs
        assert "example.com" in result.allow_net

    def test_no_sandbox_overrides_sandbox_flag(self):
        """--no-sandbox with --sandbox is rejected by @pre contract."""
        import deal

        with pytest.raises(deal.PreContractError):
            resolve_sandbox_policy(True, True, [], False, [])

    def test_cli_and_preset_allows_deduped(self):
        """Overlapping CLI and preset allows are deduped."""
        result = resolve_sandbox_policy(
            True, False, ["fs:/tmp"], True, ["fs:/tmp"]
        )
        assert result.allow_fs == ["/tmp"]


class TestSandboxPolicyDataclass:
    """SandboxPolicy dataclass behavior tests."""

    def test_default_values(self):
        """Default SandboxPolicy should have disabled=False, empty lists."""
        policy = SandboxPolicy()
        assert policy.enabled is False
        assert policy.allow_fs == []
        assert policy.allow_net == []

    def test_equality(self):
        """Two policies with same values should be equal."""
        p1 = SandboxPolicy(enabled=True, allow_fs=["/tmp"], allow_net=["*"])
        p2 = SandboxPolicy(enabled=True, allow_fs=["/tmp"], allow_net=["*"])
        assert p1 == p2

    def test_inequality(self):
        """Policies with different values should not be equal."""
        p1 = SandboxPolicy(enabled=True, allow_fs=["/tmp"], allow_net=["*"])
        p2 = SandboxPolicy(enabled=True, allow_fs=["/var"], allow_net=["*"])
        assert p1 != p2
