# tests/test_policies.py
import pytest
import yaml
from pydantic import ValidationError

from agentsh_secure_sandbox.policies.schema import (
    PolicyDefinition,
    FileAllowRule,
    FileDenyRule,
    NetworkAllowRule,
    CommandRedirectRule,
    CommandRedirectTarget,
    EnvRule,
    DnsRedirect,
    ConnectRedirect,
    validate_policy,
)
from agentsh_secure_sandbox.policies.serialize import serialize_policy, system_policy_yaml
from agentsh_secure_sandbox.policies.merge import merge, merge_prepend
from agentsh_secure_sandbox.policies.presets import (
    agent_default,
    dev_safe,
    ci_strict,
    agent_sandbox,
)


def test_empty_policy():
    p = PolicyDefinition()
    assert p.file is None
    assert p.network is None


def test_file_allow_rule():
    r = FileAllowRule(allow="/workspace/**", ops=["read", "write"])
    assert r.allow == "/workspace/**"


def test_file_deny_rule_list():
    r = FileDenyRule(deny=["**/.env", "**/*.key"])
    assert len(r.deny) == 2


def test_network_allow_with_ports():
    r = NetworkAllowRule(allow="pypi.org", ports=[443])
    assert r.ports == [443]


def test_network_port_validation():
    with pytest.raises(ValidationError):
        NetworkAllowRule(allow="x", ports=[0])
    with pytest.raises(ValidationError):
        NetworkAllowRule(allow="x", ports=[70000])


def test_command_redirect_with_target():
    target = CommandRedirectTarget(cmd="agentsh-fetch", args=["--audit"])
    r = CommandRedirectRule(redirect="curl", to=target)
    assert r.to.cmd == "agentsh-fetch"


def test_command_redirect_with_string():
    r = CommandRedirectRule(redirect="curl", to="safe-curl")
    assert r.to == "safe-curl"


def test_extra_fields_rejected():
    with pytest.raises(ValidationError):
        FileAllowRule(allow="/x", bogus="nope")  # type: ignore[call-arg]


def test_policy_from_dict():
    p = PolicyDefinition(
        file=[{"allow": "/workspace/**"}],
        network=[{"deny": "*"}],
    )
    assert len(p.file) == 1
    assert len(p.network) == 1


def test_validate_policy_dict():
    p = validate_policy({"file": [{"allow": "/tmp"}]})
    assert isinstance(p, PolicyDefinition)


def test_validate_policy_passthrough():
    p = PolicyDefinition()
    assert validate_policy(p) is p


def test_env_rule():
    r = EnvRule(commands=["node"], allow=["PATH", "HOME"])
    assert r.commands == ["node"]


def test_dns_redirect():
    r = DnsRedirect(match="evil.com", resolve_to="127.0.0.1")
    assert r.resolve_to == "127.0.0.1"


def test_connect_redirect():
    r = ConnectRedirect(match="evil.com:443", redirect_to="proxy:8080")
    assert r.redirect_to == "proxy:8080"


# ─── Serialization tests ────────────────────────────────


def test_serialize_empty_policy():
    p = PolicyDefinition()
    result = serialize_policy(p)
    doc = yaml.safe_load(result)
    assert doc["version"] == 1
    assert doc["name"] == "secure-sandbox-policy"
    assert "file_rules" not in doc


def test_serialize_file_allow():
    p = PolicyDefinition(file=[{"allow": "/workspace/**", "ops": ["read", "write"]}])
    doc = yaml.safe_load(serialize_policy(p))
    assert doc["file_rules"][0]["paths"] == ["/workspace/**"]
    assert doc["file_rules"][0]["decision"] == "allow"
    assert doc["file_rules"][0]["operations"] == ["read", "write"]


def test_serialize_file_deny():
    p = PolicyDefinition(file=[{"deny": ["**/.env", "**/*.key"]}])
    doc = yaml.safe_load(serialize_policy(p))
    assert doc["file_rules"][0]["decision"] == "deny"
    assert len(doc["file_rules"][0]["paths"]) == 2


def test_serialize_network_allow_with_ports():
    p = PolicyDefinition(network=[{"allow": "pypi.org", "ports": [443]}])
    doc = yaml.safe_load(serialize_policy(p))
    assert doc["network_rules"][0]["hosts"] == ["pypi.org"]
    assert doc["network_rules"][0]["ports"] == [443]


def test_serialize_network_deny():
    p = PolicyDefinition(network=[{"deny": "*"}])
    doc = yaml.safe_load(serialize_policy(p))
    assert doc["network_rules"][0]["decision"] == "deny"


def test_serialize_command_redirect_target():
    p = PolicyDefinition(
        commands=[
            {
                "redirect": "curl",
                "to": {"cmd": "agentsh-fetch", "args": ["--audit"]},
            }
        ]
    )
    doc = yaml.safe_load(serialize_policy(p))
    rule = doc["command_rules"][0]
    assert rule["decision"] == "redirect"
    assert rule["redirect_to"]["cmd"] == "agentsh-fetch"


def test_serialize_command_redirect_string():
    p = PolicyDefinition(commands=[{"redirect": "curl", "to": "safe-curl"}])
    doc = yaml.safe_load(serialize_policy(p))
    assert doc["command_rules"][0]["redirect_to"] == "safe-curl"


def test_serialize_env_rules():
    p = PolicyDefinition(env=[{"commands": ["node"], "allow": ["PATH"]}])
    doc = yaml.safe_load(serialize_policy(p))
    assert doc["env_rules"][0]["commands"] == ["node"]


def test_serialize_dns_redirects():
    p = PolicyDefinition(dns=[{"match": "evil.com", "resolve_to": "127.0.0.1"}])
    doc = yaml.safe_load(serialize_policy(p))
    assert doc["dns_redirects"][0]["resolve_to"] == "127.0.0.1"


def test_system_policy_yaml():
    doc = yaml.safe_load(system_policy_yaml())
    assert doc["name"] == "_system-protection"
    assert len(doc["file_rules"]) == 3
    assert len(doc["command_rules"]) == 1
    # Verify self-protection
    assert doc["file_rules"][0]["decision"] == "deny"
    assert "/etc/agentsh/**" in doc["file_rules"][0]["paths"]


# ─── Merge tests ────────────────────────────────────────


def test_merge_append():
    base = PolicyDefinition(file=[{"allow": "/workspace/**"}])
    ext = PolicyDefinition(file=[{"allow": "/data/**"}])
    result = merge(base, ext)
    assert len(result.file) == 2
    # Base rule first (first-match-wins: base takes priority)
    assert result.file[0].allow == "/workspace/**"
    assert result.file[1].allow == "/data/**"


def test_merge_prepend():
    base = PolicyDefinition(file=[{"deny": "**/.env"}])
    override = PolicyDefinition(file=[{"allow": "**/.env", "ops": ["read"]}])
    result = merge_prepend(base, override)
    assert len(result.file) == 2
    # Override first (overrides take priority)
    assert result.file[0].allow == "**/.env"


def test_merge_multiple_categories():
    base = PolicyDefinition(
        file=[{"allow": "/a"}],
        network=[{"deny": "*"}],
    )
    ext = PolicyDefinition(
        file=[{"allow": "/b"}],
        commands=[{"allow": "ls"}],
    )
    result = merge(base, ext)
    assert len(result.file) == 2
    assert len(result.network) == 1
    assert len(result.commands) == 1


def test_merge_no_overrides():
    base = PolicyDefinition(file=[{"allow": "/a"}])
    result = merge(base)
    assert len(result.file) == 1


def test_merge_validates_result():
    """Merged result goes through Pydantic validation."""
    base = PolicyDefinition(file=[{"allow": "/a"}])
    ext = PolicyDefinition(file=[{"allow": "/b"}])
    result = merge(base, ext)
    assert isinstance(result, PolicyDefinition)


# ─── Preset tests ───────────────────────────────────────


def test_agent_default_has_workspace_allow():
    p = agent_default()
    allows = [
        r
        for r in p.file
        if hasattr(r, "allow")
        and "/workspace/**" in (r.allow if isinstance(r.allow, list) else [r.allow])
    ]
    assert len(allows) == 1


def test_agent_default_denies_env_files():
    p = agent_default()
    denies = [r for r in p.file if hasattr(r, "deny")]
    paths = []
    for r in denies:
        d = r.deny if isinstance(r.deny, list) else [r.deny]
        paths.extend(d)
    assert "**/.env" in paths


def test_agent_default_network_deny_all():
    p = agent_default()
    last = p.network[-1]
    assert hasattr(last, "deny")
    assert last.deny == "*"


def test_agent_default_with_extensions():
    ext = PolicyDefinition(network=[{"allow": "api.stripe.com", "ports": [443]}])
    p = agent_default(extensions=ext)
    # Base network rules + extension
    assert len(p.network) == 3  # allow registries, deny *, plus extension


def test_agent_default_workspace_allows_delete():
    """Spec review fix: workspace allow includes delete op."""
    p = agent_default()
    # Workspace allow is after deny rules
    ws_rules = [
        r
        for r in p.file
        if hasattr(r, "allow")
        and "/workspace/**" in (r.allow if isinstance(r.allow, list) else [r.allow])
    ]
    assert len(ws_rules) == 1
    assert "delete" in ws_rules[0].ops


def test_dev_safe_allows_broad_network():
    p = dev_safe()
    allows = [r for r in p.network if hasattr(r, "allow")]
    assert any(r.allow == "*" for r in allows)


def test_ci_strict_read_only_workspace():
    p = ci_strict()
    # Workspace read-only allow is after deny rules
    ws_rules = [
        r
        for r in p.file
        if hasattr(r, "allow")
        and "/workspace/**" in (r.allow if isinstance(r.allow, list) else [r.allow])
    ]
    assert len(ws_rules) == 1
    assert ws_rules[0].ops == ["read"]


def test_agent_sandbox_no_network():
    p = agent_sandbox()
    assert len(p.network) == 1
    assert p.network[0].deny == "*"


def test_agent_sandbox_read_only():
    p = agent_sandbox()
    # Workspace read-only allow is after deny rules
    ws_rules = [
        r
        for r in p.file
        if hasattr(r, "allow")
        and "/workspace/**" in (r.allow if isinstance(r.allow, list) else [r.allow])
    ]
    assert len(ws_rules) == 1
    assert ws_rules[0].ops == ["read"]
