"""Tests for ``events show`` evidence visibility."""

from __future__ import annotations

import io
import json
from pathlib import Path

import pytest

from agentveil_mcp_proxy.cli import ProxyCliError, init_proxy, main, proxy_paths, show_events
from agentveil_mcp_proxy.evidence.events_show import (
    LOCAL_PROOF_AGENT_INSPECTION_HINT,
    LOCAL_PROOF_MCP_TOOL_NAME,
    build_event_show_entry,
    build_events_show_payload,
    build_local_proof_mcp_payload,
    format_local_proof_human_summary,
    render_local_proof_mcp_content,
    verify_local_evidence_chain,
)
from agentveil_mcp_proxy.evidence.store import (
    GENESIS_PREV_EVENT_HASH,
    ApprovalEvidenceStore,
    PendingApproval,
    record_hash,
)

TEST_PASSPHRASE = "test-passphrase-123"


def _record(
    request_id: str,
    *,
    session_id: str = "session-a",
    status: str = "pending",
    tool_name: str = "write_file",
    risk_class: str = "write",
    action_class: str = "write",
    error_class: str | None = None,
    metadata_jcs: str | None = None,
    prev_event_hash: str | None = None,
    granted_by: str | None = None,
    created_at: int = 1_700_000_000,
    resource_hash: str = "sha256:" + "a" * 64,
    payload_hash: str = "sha256:" + "b" * 64,
) -> PendingApproval:
    return PendingApproval(
        request_id=request_id,
        session_id=session_id,
        client_id="claude_code",
        downstream_server="filesystem",
        tool_name=tool_name,
        action_class=action_class,
        risk_class=risk_class,
        resource_hash=resource_hash,
        payload_hash=payload_hash,
        policy_id="filesystem",
        policy_rule_id="write-approval",
        policy_context_hash="c" * 64,
        status=status,
        created_at=created_at,
        expires_at=created_at + 300,
        error_class=error_class,
        action_gate_metadata_jcs=metadata_jcs,
        prev_event_hash=prev_event_hash,
        granted_by_request_id=granted_by,
    )


def test_events_show_empty_state_is_friendly(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    out = io.StringIO()
    count = show_events(home=home, last=1, out=out)
    text = out.getvalue()

    assert count == 0
    assert "No local evidence yet" in text
    assert LOCAL_PROOF_MCP_TOOL_NAME in text
    assert "events show --last --verify" in text
    assert "unknown" not in text.lower()


def test_events_show_human_output_is_readable_and_bounded(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    metadata = json.dumps({
        "action_family": "write",
        "policy_decision": "approval",
        "approval_status": "pending",
        "execution_status": "not_reached",
        "target_reached": False,
    }, separators=(",", ":"))
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record("req-write", metadata_jcs=metadata))

    out = io.StringIO()
    show_events(home=home, last=1, out=out)
    text = out.getvalue()

    assert "decision=approval_required" in text
    assert "tool=write_file" in text
    assert "policy=write-approval" in text
    assert "sha256:" not in text
    assert "b" * 64 not in text
    assert "unknown" not in text.lower()


def test_events_show_json_returns_bounded_fields(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record("req-json"))

    out = io.StringIO()
    show_events(home=home, last=1, output_json=True, out=out)
    payload = json.loads(out.getvalue())

    assert payload["ok"] is True
    assert payload["event_count"] == 1
    event = payload["events"][0]
    assert event["record_id"] == "req-json"
    assert event["decision"] == "approval_required"
    assert event["payload_hash"].startswith("sha256:")
    assert "debug" not in event


def test_events_show_debug_includes_bounded_debug_fields(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record("req-debug"))

    out = io.StringIO()
    show_events(home=home, last=1, debug=True, output_json=True, out=out)
    event = json.loads(out.getvalue())["events"][0]

    assert "debug" in event
    assert "record_hash" in event["debug"]


def test_events_show_session_filter(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record("req-a", session_id="keep-me"))
        store.write_pending(_record("req-b", session_id="other"))

    payload = build_events_show_payload(
        evidence_path=paths.proxy_dir / "evidence.sqlite",
        config_path=paths.config_path,
        session_id="keep-me",
        last=10,
    )

    assert payload["event_count"] == 1
    assert payload["events"][0]["record_id"] == "req-a"


def test_events_show_hard_block_and_sandbox_reason() -> None:
    # claim-check: allow blocked/hard_blocked fixture literals; this test verifies bounded event display, not a coverage claim.
    record = _record(
        "req-blocked",  # claim-check: allow blocked fixture id for bounded renderer test.
        status="blocked",  # claim-check: allow blocked status enum fixture.
        error_class="path_outside_workspace",
    )
    entry = build_event_show_entry(record)

    assert entry["valid"] is True
    assert entry["decision"] == "hard_blocked"
    assert "sandbox" in entry["reason_summary"].lower()


def test_events_show_verify_not_available_without_chain() -> None:
    record = _record("req-no-chain")
    result = verify_local_evidence_chain([record])
    assert result["status"] == "not_available"


def test_events_show_verify_intact_for_chained_records() -> None:
    first = _record("req-1", prev_event_hash=GENESIS_PREV_EVENT_HASH)
    first_hash = record_hash(first)
    second = _record("req-2", prev_event_hash=first_hash)
    result = verify_local_evidence_chain([first, second])
    assert result["status"] == "intact"
    assert result["chain_root_hash"] == record_hash(second)


def test_events_show_verify_failed_on_mismatch() -> None:
    first = _record("req-1", prev_event_hash=GENESIS_PREV_EVENT_HASH)
    second = _record("req-2", prev_event_hash="sha256:" + "f" * 64)
    result = verify_local_evidence_chain([first, second])
    assert result["status"] == "failed"


def test_events_show_skips_unparseable_metadata_without_crashing() -> None:
    record = _record("req-bad", metadata_jcs="{not-json")
    entry = build_event_show_entry(record)
    assert entry["valid"] is False


def test_events_show_last_must_be_positive(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    with pytest.raises(ProxyCliError, match="positive"):
        show_events(home=home, last=0)


def test_cli_events_show_last_integration(tmp_path: Path, capsys) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record("req-cli"))

    rc = main(["events", "show", "--last", "--home", str(home)])
    assert rc == 0
    text = capsys.readouterr().out
    assert "decision=approval_required" in text
    assert "write_file" in text


def test_cli_events_show_bare_last_includes_pending_write_same_second(
    tmp_path: Path,
    capsys,
) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    same_second = 1_700_000_000
    read_metadata = json.dumps({"target_reached": True}, separators=(",", ":"))
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record(
            "aaa-write-pending",
            status="pending",
            created_at=same_second,
        ))
        for request_id, tool_name, action_class in (
            ("read-list", "list_workspace", "read"),
            ("read-file", "read_file", "read"),
            ("zzz-tail-read", "instruction_surface_status", "read"),
        ):
            store.write_pending(_record(
                request_id,
                status="pending",
                tool_name=tool_name,
                risk_class="read",
                action_class=action_class,
                metadata_jcs=read_metadata,
                created_at=same_second,
            ))
            store.transition(
                request_id,
                "executed",
                result_hash="sha256:" + "f" * 64,
            )

    rc = main(["events", "show", "--last", "--home", str(home)])
    assert rc == 0
    text = capsys.readouterr().out
    assert "decision=approval_required" in text
    assert "tool=write_file" in text
    assert "tool=instruction_surface_status" in text


def test_events_show_read_only_allowed() -> None:
    metadata = json.dumps({
        "action_family": "read",
        "target_reached": True,
    }, separators=(",", ":"))
    record = _record(
        "req-read",
        status="executed",
        tool_name="read_file",
        risk_class="read",
        action_class="read",
        metadata_jcs=metadata,
    )
    entry = build_event_show_entry(record)
    assert entry["decision"] == "target_reached"


def test_events_show_read_only_allowed_without_target_flag() -> None:
    record = _record(
        "req-read-plain",
        status="executed",
        tool_name="read_file",
        risk_class="read",
        action_class="read",
    )
    entry = build_event_show_entry(record)
    assert entry["decision"] == "allowed"


def test_events_show_approved_event() -> None:
    record = _record("req-approved", status="approved")
    entry = build_event_show_entry(record)
    assert entry["decision"] == "approved"
    assert "Approved by user" in entry["reason_summary"]


def test_events_show_target_reached_after_execution() -> None:
    metadata = json.dumps({
        "action_family": "write",
        "target_reached": True,
        "execution_status": "executed",
    }, separators=(",", ":"))
    record = _record(
        "req-reached",
        status="executed",
        metadata_jcs=metadata,
        granted_by="req-parent",
    )
    entry = build_event_show_entry(record)
    assert entry["decision"] == "target_reached"
    assert entry["target_reached"] is True
    assert entry["reason_summary"] == "Action reached target."
    assert "Approval required before execution" not in entry["reason_summary"]
    assert LOCAL_PROOF_MCP_TOOL_NAME in entry["next_step"]


def test_events_show_why_copy_matches_decision_semantics() -> None:
    pending = build_event_show_entry(_record("req-pending", status="pending"))
    approved = build_event_show_entry(_record("req-approved", status="approved"))
    reached_metadata = json.dumps({"target_reached": True}, separators=(",", ":"))
    reached = build_event_show_entry(_record(
        "req-reached",
        status="executed",
        metadata_jcs=reached_metadata,
        granted_by="req-parent",
    ))
    denied = build_event_show_entry(_record("req-denied", status="denied"))

    assert pending["decision"] == "approval_required"
    assert "Approval required before execution" in pending["reason_summary"]
    assert "Retry the same MCP tool call after approval" in pending["reason_summary"]

    assert approved["decision"] == "approved"
    assert "Approved by user" in approved["reason_summary"]
    assert "Retry the same MCP tool call" in approved["reason_summary"]
    assert approved["reason_summary"] != pending["reason_summary"]

    assert reached["decision"] == "target_reached"
    assert reached["reason_summary"] == "Action reached target."
    assert "Approval required before execution" not in reached["reason_summary"]
    assert reached["reason_summary"] != pending["reason_summary"]
    assert reached["reason_summary"] != approved["reason_summary"]

    assert denied["decision"] == "hard_blocked"
    assert denied["reason_summary"] == "Denied by user."
    assert denied["reason_summary"] != pending["reason_summary"]


def test_events_show_execution_not_reached() -> None:
    metadata = json.dumps({
        "action_family": "write",
        "target_reached": False,
        "execution_status": "not_reached",
    }, separators=(",", ":"))
    record = _record("req-not-reached", status="executed", metadata_jcs=metadata)
    entry = build_event_show_entry(record)
    assert entry["decision"] == "execution_not_reached"
    assert entry["target_reached"] is False
    assert entry.get("next_step")


def test_events_show_human_output_includes_local_proof_discoverability(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    metadata = json.dumps({"target_reached": True}, separators=(",", ":"))
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record(
            "req-proof",
            status="pending",
            metadata_jcs=metadata,
        ))
        store.transition("req-proof", "executed", result_hash="sha256:" + "f" * 64)

    out = io.StringIO()
    show_events(home=home, last=1, out=out)
    text = out.getvalue()

    assert "Local proof shows what was requested, decided, executed" in text
    assert "events show --last --verify" in text
    assert "/proof" not in text


def test_events_show_redirect_original_includes_next_step() -> None:
    # claim-check: allow blocked fixture literal; redirect renderer behavior is asserted below.
    metadata = json.dumps({
        "redirect_role": "original",
        "redirect_playbook_id": "use_read_only_tool",
        "original_request_id": "req-redirect",
        "target_reached": False,
    }, separators=(",", ":"))
    record = _record(
        "req-redirect",
        status="blocked",  # claim-check: allow blocked status enum fixture.
        metadata_jcs=metadata,
    )
    entry = build_event_show_entry(record)
    assert entry["decision"] == "redirected"
    assert entry.get("redirect_playbook_id") == "use_read_only_tool"
    assert "redirect" in entry["next_step"].lower()


def test_events_show_user_denied_vs_policy_hard_block() -> None:
    # claim-check: allow blocked/hard_blocked fixture literals; this is an enum/status rendering test.
    denied = build_event_show_entry(_record("req-denied", status="denied"))
    blocked = build_event_show_entry(_record(  # claim-check: allow blocked variable in enum rendering test.
        "req-blocked",  # claim-check: allow blocked fixture id.
        status="blocked",  # claim-check: allow blocked status enum fixture.
        error_class="local_policy_block",
    ))
    assert denied["decision"] == "hard_blocked"
    assert blocked["decision"] == "hard_blocked"  # claim-check: allow hard_blocked decision enum assertion.
    assert denied["reason_summary"] == "Denied by user."
    assert blocked["reason_summary"] == "local policy block"  # claim-check: allow policy block fixture text.


def test_local_proof_mcp_payload_is_bounded_and_includes_verify(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    metadata = json.dumps({
        "action_family": "write",
        "policy_decision": "approval",
        "approval_status": "pending",
        "execution_status": "not_reached",
        "target_reached": False,
    }, separators=(",", ":"))
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record("req-write", metadata_jcs=metadata))

    payload = build_local_proof_mcp_payload(
        evidence_path=paths.proxy_dir / "evidence.sqlite",
        config_path=paths.config_path,
        last=5,
        verify=True,
    )

    assert payload["status"] == "ok"
    assert payload["proof"]["events"]
    event = payload["proof"]["events"][-1]
    assert event["decision"] == "approval_required"
    assert event["tool"] == "write_file"
    assert event["why"]
    assert LOCAL_PROOF_MCP_TOOL_NAME in event["next_step"]
    assert payload["proof"]["verify"]["status"] in {"not_available", "intact", "failed"}
    serialized = json.dumps(payload)
    assert "/Users/" not in serialized
    assert "sha256:" not in serialized


def test_local_proof_agent_hint_prefers_mcp_tool() -> None:
    assert LOCAL_PROOF_MCP_TOOL_NAME in LOCAL_PROOF_AGENT_INSPECTION_HINT
    assert "events show --last --verify" in LOCAL_PROOF_AGENT_INSPECTION_HINT


def test_local_proof_human_summary_prefers_write_outcome_chain(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    pending_meta = json.dumps({
        "action_family": "write",
        "policy_decision": "approval",
        "approval_status": "pending",
        "execution_status": "not_reached",
        "target_reached": False,
    }, separators=(",", ":"))
    approved_meta = json.dumps({
        "action_family": "write",
        "policy_decision": "approval",
        "approval_status": "approved",
        "execution_status": "not_reached",
        "target_reached": False,
    }, separators=(",", ":"))
    reached_meta = json.dumps({
        "action_family": "write",
        "policy_decision": "approval",
        "approval_status": "approved",
        "execution_status": "executed",
        "target_reached": True,
    }, separators=(",", ":"))
    read_meta = json.dumps({"target_reached": True}, separators=(",", ":"))
    write_resource = "sha256:" + "c" * 64
    write_payload = "sha256:" + "d" * 64
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record(
            "req-read",
            tool_name="list_workspace",
            risk_class="read",
            action_class="read",
            metadata_jcs=read_meta,
            created_at=1_700_000_000,
        ))
        store.transition("req-read", "executed", result_hash="sha256:" + "f" * 64)
        store.write_pending(_record(
            "req-pending",
            metadata_jcs=pending_meta,
            resource_hash=write_resource,
            payload_hash=write_payload,
            created_at=1_700_000_100,
        ))
        store.write_pending(_record(
            "req-approved",
            metadata_jcs=approved_meta,
            granted_by="req-pending",
            resource_hash=write_resource,
            payload_hash=write_payload,
            created_at=1_700_000_200,
        ))
        store.transition(
            "req-approved",
            "approved",
            approval_token_hash="sha256:" + "e" * 64,
            approval_decided_by="local-user",
            approval_scope="once",
            user_decision_timestamp=1_700_000_200,
        )
        store.write_pending(_record(
            "req-reached",
            metadata_jcs=reached_meta,
            granted_by="req-approved",
            resource_hash=write_resource,
            payload_hash=write_payload,
            created_at=1_700_000_300,
        ))
        store.transition("req-reached", "executed", result_hash="sha256:" + "1" * 64)

    show_payload = build_events_show_payload(
        evidence_path=paths.proxy_dir / "evidence.sqlite",
        config_path=paths.config_path,
        last=10,
        verify=True,
    )
    text = format_local_proof_human_summary(show_payload)

    assert text.startswith("AgentVeil proof")
    assert "Result: write approved and completed" in text
    assert "Verification:" in text
    assert "Tool: write_file" in text
    assert "approval required -> approved -> target reached" in text
    assert "Target: resource:" in text
    assert "Target: resource:cccccccccccc" in text
    assert "Observed read-only actions:" in text
    assert "- list_workspace allowed" in text
    assert "next_step" not in text
    assert '"status"' not in text
    assert "Use debug/json for full bounded evidence." in text


def test_local_proof_json_mode_remains_bounded(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    metadata = json.dumps({
        "action_family": "write",
        "target_reached": False,
    }, separators=(",", ":"))
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record("req-write", metadata_jcs=metadata))

    text = render_local_proof_mcp_content(
        evidence_path=paths.proxy_dir / "evidence.sqlite",
        config_path=paths.config_path,
        last=5,
        verify=True,
        output_format="json",
    )
    payload = json.loads(text)

    assert payload["status"] == "ok"
    assert payload["proof"]["events"]
    assert payload["proof"]["verify"]["status"] in {"not_available", "intact", "failed"}


def test_local_proof_human_summary_does_not_mix_unrelated_write_chains(tmp_path: Path) -> None:
    home = tmp_path / "home"
    init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
    paths = proxy_paths(home)
    pending_meta = json.dumps({
        "action_family": "write",
        "approval_status": "pending",
        "target_reached": False,
    }, separators=(",", ":"))
    approved_meta = json.dumps({
        "action_family": "write",
        "approval_status": "approved",
        "target_reached": False,
    }, separators=(",", ":"))
    reached_meta = json.dumps({
        "action_family": "write",
        "approval_status": "approved",
        "execution_status": "executed",
        "target_reached": True,
    }, separators=(",", ":"))
    resource_a = "sha256:" + "a" * 64
    payload_a = "sha256:" + "1" * 64
    resource_b = "sha256:" + "c" * 64
    payload_b = "sha256:" + "2" * 64
    with ApprovalEvidenceStore(paths.proxy_dir / "evidence.sqlite") as store:
        store.write_pending(_record(
            "req-a-pending",
            metadata_jcs=pending_meta,
            resource_hash=resource_a,
            payload_hash=payload_a,
            created_at=1_700_000_100,
        ))
        store.write_pending(_record(
            "req-b-pending",
            metadata_jcs=pending_meta,
            resource_hash=resource_b,
            payload_hash=payload_b,
            created_at=1_700_000_200,
        ))
        store.write_pending(_record(
            "req-b-approved",
            metadata_jcs=approved_meta,
            granted_by="req-b-pending",
            resource_hash=resource_b,
            payload_hash=payload_b,
            created_at=1_700_000_300,
        ))
        store.transition(
            "req-b-approved",
            "approved",
            approval_token_hash="sha256:" + "e" * 64,
            approval_decided_by="local-user",
            approval_scope="once",
            user_decision_timestamp=1_700_000_300,
        )
        store.write_pending(_record(
            "req-b-reached",
            metadata_jcs=reached_meta,
            granted_by="req-b-approved",
            resource_hash=resource_b,
            payload_hash=payload_b,
            created_at=1_700_000_400,
        ))
        store.transition("req-b-reached", "executed", result_hash="sha256:" + "f" * 64)

    show_payload = build_events_show_payload(
        evidence_path=paths.proxy_dir / "evidence.sqlite",
        config_path=paths.config_path,
        last=10,
        verify=True,
    )
    text = format_local_proof_human_summary(show_payload)

    assert "Target: resource:cccccccccccc" in text
    assert "resource:aaaaaaaaaaaa" not in text
    assert "approval required -> approved -> target reached" in text
    assert "Result: write approved and completed" in text
