"""P1 tests for MCP proxy config schema and internal local policy engine.

These tests intentionally do not start MCP transport, do not call AVP backend,
and do not exercise approval UI. P1 is only the local config/policy foundation.
"""

from __future__ import annotations

import re

import pytest

from agentveil_mcp_proxy import (
    DecisionMode,
    PolicyDecision,
    PolicyEngine,
    PolicyRuntime,
    ProxyConfig,
    ProxyConfigError,
    RiskClass,
    ToolCallContext,
    builtin_policy_pack,
    policy_context_hash,
)


TRUSTED_SIGNER_DID = "did:key:z6MktrustedSigner"


def _base_config(**overrides):
    data = {
        "proxy_config_schema_version": 1,
        "avp": {
            "base_url": "https://agentveil.dev",
            "agent_name": "local-proxy",
            "trusted_signer_dids": [TRUSTED_SIGNER_DID],
        },
        "mode": "protect",
        "privacy": {
            "action": "redacted",
            "resource": "hash",
            "payload": "hash_only",
            "evidence_upload": False,
        },
        "fallback": {
            "read": "allow",
            "write": "approval",
            "destructive": "block",
            "production": "block",
            "financial": "block",
            "unknown": "approval",
        },
        "approval": {
            "approval_timeout_seconds": 300,
            "on_timeout": "deny",
        },
        "policy": {
            "id": "test-policy",
            "policy_schema_version": 1,
            "default_decision": "ask_backend",
            "default_risk_class": "unknown",
            "rules": [],
        },
    }
    data.update(overrides)
    return data


def _pack_config(name: str) -> ProxyConfig:
    pack = builtin_policy_pack(name)

    def match_dict(rule):
        data = {}
        if rule.match.server:
            data["server"] = list(rule.match.server)
        if rule.match.tool:
            data["tool"] = list(rule.match.tool)
        if rule.match.action:
            data["action"] = list(rule.match.action)
        if rule.match.risk_class:
            data["risk_class"] = [risk.value for risk in rule.match.risk_class]
        return data

    return ProxyConfig.from_dict(_base_config(policy={
        "id": pack.id,
        "policy_schema_version": 1,
        "rules": [
            {
                "id": rule.id,
                "source": rule.source,
                "decision": rule.decision.value,
                "risk_class": rule.risk_class.value if rule.risk_class else None,
                "match": match_dict(rule),
            }
            for rule in pack.rules
        ],
    }))


def _evaluate_builtin_pack(name: str, *, server: str, tool: str):
    return PolicyEngine(_pack_config(name)).evaluate({
        "server": server,
        "tool": tool,
    })


def test_proxy_config_schema_requires_version_and_trusted_signers():
    cfg = ProxyConfig.from_dict(_base_config())
    assert cfg.proxy_config_schema_version == 1
    assert cfg.policy.policy_schema_version == 1
    assert cfg.avp.trusted_signer_dids == (TRUSTED_SIGNER_DID,)
    assert cfg.privacy.payload == "hash_only"
    assert cfg.approval.approval_timeout_seconds == 300
    assert cfg.approval.on_timeout.value == "deny"

    with pytest.raises(ProxyConfigError, match="proxy_config_schema_version must be 1"):
        ProxyConfig.from_dict(_base_config(proxy_config_schema_version=2))

    bad_avp = dict(_base_config()["avp"])
    bad_avp["trusted_signer_dids"] = []
    with pytest.raises(ProxyConfigError, match="trusted_signer_dids"):
        ProxyConfig.from_dict(_base_config(avp=bad_avp))


def test_policy_schema_rejects_invalid_vocab_and_raw_payload_modes():
    bad_policy = dict(_base_config()["policy"])
    bad_policy["policy_schema_version"] = 2
    with pytest.raises(ProxyConfigError, match="policy_schema_version must be 1"):
        ProxyConfig.from_dict(_base_config(policy=bad_policy))

    bad_policy = dict(_base_config()["policy"])
    bad_policy["rules"] = [{"id": "bad", "decision": "permit"}]
    with pytest.raises(ProxyConfigError, match="decision"):
        ProxyConfig.from_dict(_base_config(policy=bad_policy))

    bad_privacy = dict(_base_config()["privacy"])
    bad_privacy["payload"] = "plain"
    with pytest.raises(ProxyConfigError, match="privacy.payload must be hash_only"):
        ProxyConfig.from_dict(_base_config(privacy=bad_privacy))

    bad_fallback = dict(_base_config()["fallback"])
    bad_fallback["write"] = "ask_backend"
    with pytest.raises(ProxyConfigError, match="fallback.write"):
        ProxyConfig.from_dict(_base_config(fallback=bad_fallback))


def test_ask_backend_semantics_in_protect_mode():
    policy = dict(_base_config()["policy"])
    policy["rules"] = [
        {
            "id": "github-write",
            "decision": "ask_backend",
            "risk_class": "write",
            "match": {"server": "github", "tool": "create_*"},
        }
    ]
    cfg = ProxyConfig.from_dict(_base_config(policy=policy))
    result = PolicyEngine(cfg).evaluate({
        "server": "github",
        "tool": "create_issue",
        "risk_class": "write",
    })
    assert result.decision is PolicyDecision.ASK_BACKEND
    assert result.would_decision is None
    assert result.risk_class is RiskClass.WRITE
    assert result.policy_rule_id == "github-write"


def test_observe_mode_returns_observe_with_would_decision():
    policy = dict(_base_config()["policy"])
    policy["rules"] = [
        {
            "id": "delete-needs-block",
            "decision": "block",
            "risk_class": "destructive",
            "match": {"server": "filesystem", "tool": "delete_*"},
        }
    ]
    cfg = ProxyConfig.from_dict(_base_config(mode="observe", policy=policy))
    result = PolicyEngine(cfg).evaluate(ToolCallContext(
        server="filesystem",
        tool="delete_file",
        risk_class=RiskClass.DESTRUCTIVE,
    ))
    assert result.decision is PolicyDecision.OBSERVE
    assert result.would_decision is PolicyDecision.BLOCK
    assert result.risk_class is RiskClass.DESTRUCTIVE
    assert result.policy_context_hash == policy_context_hash(
        policy_id="test-policy",
        policy_rule_id="delete-needs-block",
        risk_class=RiskClass.DESTRUCTIVE,
        decision_mode=DecisionMode.OBSERVE,
    )


def test_stricter_wins_by_default():
    policy = dict(_base_config()["policy"])
    policy["rules"] = [
        {
            "id": "broad-allow",
            "decision": "allow",
            "risk_class": "read",
            "match": {"server": "filesystem", "tool": "*"},
        },
        {
            "id": "delete-block",
            "decision": "block",
            "risk_class": "destructive",
            "match": {"server": "filesystem", "tool": "delete_*"},
        },
    ]
    cfg = ProxyConfig.from_dict(_base_config(policy=policy))
    result = PolicyEngine(cfg).evaluate({
        "server": "filesystem",
        "tool": "delete_file",
        "risk_class": "destructive",
    })
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "delete-block"
    assert set(result.matched_rule_ids) == {"broad-allow", "delete-block"}


def test_user_override_must_be_intentional_to_weaken_builtin_policy():
    policy = dict(_base_config()["policy"])
    policy["rules"] = [
        {
            "id": "builtin-delete-block",
            "source": "builtin",
            "decision": "block",
            "risk_class": "destructive",
            "match": {"server": "filesystem", "tool": "delete_*"},
        },
        {
            "id": "user-delete-allow",
            "source": "user",
            "decision": "allow",
            "risk_class": "destructive",
            "match": {"server": "filesystem", "tool": "delete_*"},
        },
    ]
    cfg = ProxyConfig.from_dict(_base_config(policy=policy))
    result = PolicyEngine(cfg).evaluate({
        "server": "filesystem",
        "tool": "delete_file",
        "risk_class": "destructive",
    })
    assert result.decision is PolicyDecision.BLOCK
    assert result.intentional_override_applied is False

    policy["rules"][1]["intentional_override"] = True
    cfg = ProxyConfig.from_dict(_base_config(policy=policy))
    result = PolicyEngine(cfg).evaluate({
        "server": "filesystem",
        "tool": "delete_file",
        "risk_class": "destructive",
    })
    assert result.decision is PolicyDecision.ALLOW
    assert result.policy_rule_id == "user-delete-allow"
    assert result.intentional_override_applied is True

    policy["rules"].append({
        "id": "user-delete-still-block",
        "source": "user",
        "decision": "block",
        "risk_class": "destructive",
        "match": {"server": "filesystem", "tool": "delete_*"},
    })
    cfg = ProxyConfig.from_dict(_base_config(policy=policy))
    result = PolicyEngine(cfg).evaluate({
        "server": "filesystem",
        "tool": "delete_file",
        "risk_class": "destructive",
    })
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "user-delete-still-block"
    assert result.intentional_override_applied is False


def test_malformed_hot_reload_keeps_last_good_policy_and_emits_event():
    good = ProxyConfig.from_dict(_base_config())
    runtime = PolicyRuntime(good)

    bad = _base_config()
    bad["policy"] = dict(bad["policy"])
    bad["policy"]["rules"] = [{"id": "bad", "decision": "permit"}]

    result = runtime.reload_from_dict(bad)
    assert result.applied is False
    assert result.config is good
    assert runtime.config is good
    assert result.event["type"] == "policy_reload_failed"
    assert result.event["kept_policy_id"] == "test-policy"

    reloaded = _base_config()
    reloaded["policy"] = dict(reloaded["policy"])
    reloaded["policy"]["id"] = "new-policy"
    result = runtime.reload_from_dict(reloaded)
    assert result.applied is True
    assert runtime.config.policy.id == "new-policy"
    assert runtime.events[-1]["type"] == "policy_reload_applied"


def test_runtime_events_buffer_is_bounded_and_drops_oldest():
    good = ProxyConfig.from_dict(_base_config())
    runtime = PolicyRuntime(good, max_events=3)

    bad = _base_config()
    bad["policy"] = dict(bad["policy"])
    bad["policy"]["rules"] = [{"id": "bad", "decision": "permit"}]

    for _ in range(5):
        runtime.reload_from_dict(bad)

    assert len(runtime.events) == 3
    assert all(event["type"] == "policy_reload_failed" for event in runtime.events)

    with pytest.raises(ValueError, match="max_events must be positive"):
        PolicyRuntime(good, max_events=0)


def test_policy_context_hash_is_stable_and_metadata_only():
    a = policy_context_hash(
        policy_id="p",
        policy_rule_id="r",
        risk_class="write",
        decision_mode="protect",
    )
    b = policy_context_hash(
        decision_mode="protect",
        risk_class=RiskClass.WRITE,
        policy_rule_id="r",
        policy_id="p",
    )
    c = policy_context_hash(
        policy_id="p",
        policy_rule_id="r",
        risk_class="read",
        decision_mode="protect",
    )
    assert a == b
    assert a != c
    assert re.fullmatch(r"[0-9a-f]{64}", a)


def test_builtin_policy_packs_are_metadata_only_and_match_expected_rules():
    cfg = _pack_config("github")
    read = PolicyEngine(cfg).evaluate({"server": "github", "tool": "get_file_contents"})
    write = PolicyEngine(cfg).evaluate({"server": "github", "tool": "create_issue"})
    destructive = PolicyEngine(cfg).evaluate({"server": "github", "tool": "delete_branch"})
    assert read.decision is PolicyDecision.ALLOW
    assert read.risk_class is RiskClass.READ
    assert write.decision is PolicyDecision.ASK_BACKEND
    assert write.risk_class is RiskClass.WRITE
    assert destructive.decision is PolicyDecision.APPROVAL
    assert destructive.risk_class is RiskClass.DESTRUCTIVE

    with pytest.raises(ProxyConfigError, match="unknown built-in policy pack"):
        builtin_policy_pack("aws")


def test_filesystem_pack_blocks_purge_tools():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="purge_logs")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_filesystem_pack_blocks_truncate_tools():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="truncate_table")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_filesystem_pack_blocks_wipe_tools():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="wipe_disk")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_filesystem_pack_blocks_format_tools():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="format_volume")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_filesystem_pack_blocks_rm_exact_tool():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="rm")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_filesystem_pack_blocks_rmdir_tools():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="rmdir_tree")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_filesystem_pack_blocks_unlink_tools():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="unlink_file")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_filesystem_pack_blocks_clean_tools():
    result = _evaluate_builtin_pack("filesystem", server="filesystem", tool="clean_temp")
    assert result.decision is PolicyDecision.BLOCK
    assert result.policy_rule_id == "filesystem-delete"


def test_github_pack_approval_required_for_revoke_tools():
    result = _evaluate_builtin_pack("github", server="github", tool="revoke_token")
    assert result.decision is PolicyDecision.APPROVAL
    assert result.policy_rule_id == "github-destructive"


def test_github_pack_approval_required_for_destroy_tools():
    result = _evaluate_builtin_pack("github", server="github", tool="destroy_repository")
    assert result.decision is PolicyDecision.APPROVAL
    assert result.policy_rule_id == "github-destructive"


def test_github_pack_approval_required_for_drop_tools():
    result = _evaluate_builtin_pack("github", server="github", tool="drop_ref")
    assert result.decision is PolicyDecision.APPROVAL
    assert result.policy_rule_id == "github-destructive"
