"""Tests for Shield server telemetry (fire-and-forget).

After the local Shield scan completes in download(), the SDK POSTs
metadata to /v1/shield/analyze in the background to feed the server's
cross-user reputation aggregation. This test verifies:

  - Telemetry fires after a scan
  - Telemetry runs in a daemon thread (non-blocking)
  - File content never leaks in the payload
  - Server failures never raise to the caller
"""

import json
import time
from unittest.mock import patch, MagicMock

import pytest

from agentdrop.client import AgentDrop
from agentdrop.shield.models import ScanResult, ThreatLevel


def _make_scan_result() -> ScanResult:
    return ScanResult(
        threat_level=ThreatLevel.CLEAN,
        malware_clean=True,
        injection_findings=[],
        warnings=[],
        scan_time_ms=5,
        intent_score=0.0,
        semantic_score=0.0,
        sender_reputation=0.0,
        details={
            "resource_guard": {"passed": True, "reason": None},
            "format_validation": {
                "detected_mime": "text/plain",
                "declared_extension": ".txt",
                "type_mismatch": False,
                "is_polyglot": False,
            },
            "text_extraction": {
                "sources": 1,
                "total_chars": 5,
                "anomaly_count": 0,
            },
            "injection_detection": {
                "total_findings": 0,
                "max_confidence": 0.0,
                "max_threat_level": "CLEAN",
                "patterns_matched": [],
            },
            "intent_classification": {
                "intent_score": 0.0,
                "is_instructive": False,
                "signals": [],
            },
            "semantic_analysis": {
                "semantic_score": 0.0,
                "action_verb_score": 0.0,
                "sensitive_target_score": 0.0,
                "authority_framing_score": 0.0,
                "instruction_density_score": 0.0,
                "context_mismatch_score": 0.0,
                "signals": [],
            },
        },
    )


def _make_client() -> AgentDrop:
    return AgentDrop(
        api_key="agd_test_telemetry",
        api_base="https://test.api.agentdrop.io",
        agent_id="test-agent",
        shield=True,
    )


class TestShieldTelemetry:
    """Fire-and-forget telemetry from download() to /v1/shield/analyze."""

    def test_posts_to_shield_analyze_with_top_level_fields(self):
        client = _make_client()

        with patch("agentdrop.client.requests.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=200, json=lambda: {})

            client._report_scan_to_server(
                _make_scan_result(),
                filename="report.pdf",
                file_size=12345,
                sender_id="sender-agent-id",
                sender_account_id="sender-account-uuid",
            )

            # Let the daemon thread run
            time.sleep(0.1)

            assert mock_post.call_count == 1
            call = mock_post.call_args
            url = call[0][0] if call[0] else call[1]["url"]
            assert url == "https://test.api.agentdrop.io/v1/shield/analyze"

            payload = call[1]["json"]
            # Top-level fields the server schema expects
            assert "fileMetadata" in payload
            assert "layerResults" in payload
            assert "localVerdict" in payload
            assert payload["senderId"] == "sender-agent-id"
            assert payload["senderAccountId"] == "sender-account-uuid"

            headers = call[1]["headers"]
            assert headers["Authorization"] == "Bearer agd_test_telemetry"

    def test_silently_swallows_server_failures(self):
        client = _make_client()

        with patch("agentdrop.client.requests.post") as mock_post:
            mock_post.side_effect = Exception("Server is down")

            # Must not raise
            client._report_scan_to_server(
                _make_scan_result(),
                filename="x.txt",
                file_size=100,
            )

            time.sleep(0.1)
            assert mock_post.call_count == 1

    def test_omits_sender_ids_when_not_provided(self):
        client = _make_client()

        with patch("agentdrop.client.requests.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=200, json=lambda: {})

            client._report_scan_to_server(
                _make_scan_result(),
                filename="anon.txt",
                file_size=42,
            )

            time.sleep(0.1)
            payload = mock_post.call_args[1]["json"]
            assert "senderId" not in payload
            assert "senderAccountId" not in payload

    def test_returns_immediately_does_not_block(self):
        """The POST must happen in a background thread, not synchronously."""
        client = _make_client()

        def slow_post(*args, **kwargs):
            time.sleep(0.3)
            return MagicMock(status_code=200, json=lambda: {})

        with patch("agentdrop.client.requests.post", side_effect=slow_post):
            start = time.time()
            client._report_scan_to_server(
                _make_scan_result(),
                filename="nonblock.txt",
                file_size=7,
            )
            elapsed = time.time() - start

            # Must return in well under the 300ms server delay
            assert elapsed < 0.1, f"Telemetry blocked for {elapsed*1000:.0f}ms"

    def test_does_not_leak_raw_file_content(self):
        client = _make_client()

        with patch("agentdrop.client.requests.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=200, json=lambda: {})

            client._report_scan_to_server(
                _make_scan_result(),
                filename="secret.txt",
                file_size=1024,
            )

            time.sleep(0.1)
            payload = mock_post.call_args[1]["json"]
            body_str = json.dumps(payload)

            # Payload must not contain file name content or path
            assert "secret.txt" not in body_str or body_str.count("secret.txt") <= 0
            # No raw file bytes, no matched_text, no text content
            assert "IGNORE ALL PREVIOUS" not in body_str

    def test_includes_transfer_context_when_provided(self):
        client = _make_client()

        with patch("agentdrop.client.requests.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=200, json=lambda: {})

            ctx = {
                "transferId": "tr_abc123",
                "recipientAgentId": "my-other-agent",
                "recipientAccountId": "550e8400-e29b-41d4-a716-446655440000",
                "mode": "agent-to-agent",
                "fileHash": "a" * 64,
                "filenameHash": "b" * 64,
                "messageHash": "c" * 64,
                "messageLength": 17,
                "channelId": "chan_xyz",
                "createdAt": "2026-04-17T22:00:00.000Z",
            }

            client._report_scan_to_server(
                _make_scan_result(),
                filename="x.txt",
                file_size=100,
                sender_id="sender-id",
                sender_account_id="sender-acct",
                transfer_context=ctx,
            )

            time.sleep(0.1)
            payload = mock_post.call_args[1]["json"]
            assert payload["transferContext"] == ctx

    def test_strips_none_and_empty_values_from_transfer_context(self):
        client = _make_client()

        with patch("agentdrop.client.requests.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=200, json=lambda: {})

            client._report_scan_to_server(
                _make_scan_result(),
                filename="x.txt",
                file_size=100,
                transfer_context={
                    "transferId": "tr_abc",
                    "recipientAgentId": None,  # drop
                    "mode": "agent-to-agent",
                    "channelId": "",           # drop
                },
            )

            time.sleep(0.1)
            payload = mock_post.call_args[1]["json"]
            assert payload["transferContext"] == {
                "transferId": "tr_abc",
                "mode": "agent-to-agent",
            }

    def test_omits_transfer_context_when_not_provided(self):
        """Backwards-compatible wire format: old SDKs send no transferContext."""
        client = _make_client()

        with patch("agentdrop.client.requests.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=200, json=lambda: {})

            client._report_scan_to_server(
                _make_scan_result(),
                filename="x.txt",
                file_size=100,
            )

            time.sleep(0.1)
            payload = mock_post.call_args[1]["json"]
            assert "transferContext" not in payload
