"""Tests for claude_code.sender — synchronous HTTP span sender."""
from __future__ import annotations

from unittest.mock import MagicMock, patch

import httpx
import pytest

from staso.claude_code.sender import ClaudeCodeSender, sender_from_env


class TestSenderFromEnv:
    def test_returns_none_without_api_key(self, monkeypatch):
        monkeypatch.delenv("STASO_API_KEY", raising=False)
        assert sender_from_env() is None

    def test_returns_sender_with_api_key(self, monkeypatch):
        monkeypatch.setenv("STASO_API_KEY", "ak_test")
        sender = sender_from_env()
        assert sender is not None
        assert isinstance(sender, ClaudeCodeSender)

    def test_reads_all_env_vars(self, monkeypatch):
        monkeypatch.setenv("STASO_API_KEY", "ak_test")
        monkeypatch.setenv("STASO_BASE_URL", "https://custom.host")
        monkeypatch.setenv("STASO_ENVIRONMENT", "staging")
        monkeypatch.setenv("STASO_WORKSPACE_SLUG", "my-ws")
        sender = sender_from_env()
        assert sender._url == "https://custom.host/v1/traces/ingest"
        assert sender._environment == "staging"
        assert sender._workspace_slug == "my-ws"

    def test_defaults(self, monkeypatch):
        monkeypatch.setenv("STASO_API_KEY", "ak_test")
        monkeypatch.delenv("STASO_BASE_URL", raising=False)
        monkeypatch.delenv("STASO_ENVIRONMENT", raising=False)
        monkeypatch.delenv("STASO_WORKSPACE_SLUG", raising=False)
        sender = sender_from_env()
        assert sender._url == "https://api.staso.ai/v1/traces/ingest"
        assert sender._environment == "default"
        assert sender._workspace_slug == ""


class TestBuildPayload:
    def _sender(self) -> ClaudeCodeSender:
        return ClaudeCodeSender(
            base_url="https://api.test",
            api_key="ak_test",
            environment="prod",
            workspace_slug="ws",
        )

    def test_groups_spans_by_trace_id(self):
        sender = self._sender()
        spans = [
            {"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z", "kind": "llm"},
            {"trace_id": "t1", "span_id": "s2", "timestamp": "2026-01-01T00:00:01Z", "kind": "tool"},
            {"trace_id": "t2", "span_id": "s3", "timestamp": "2026-01-01T00:00:02Z", "kind": "agent"},
        ]
        payload = sender._build_payload(spans)
        assert len(payload["traces"]) == 2
        trace_ids = {t["trace_id"] for t in payload["traces"]}
        assert trace_ids == {"t1", "t2"}

    def test_earliest_timestamp_per_trace(self):
        sender = self._sender()
        spans = [
            {"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:05Z"},
            {"trace_id": "t1", "span_id": "s2", "timestamp": "2026-01-01T00:00:01Z"},
        ]
        payload = sender._build_payload(spans)
        trace = payload["traces"][0]
        assert trace["timestamp"] == "2026-01-01T00:00:01Z"

    def test_includes_environment_and_workspace(self):
        sender = self._sender()
        spans = [{"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z"}]
        payload = sender._build_payload(spans)
        trace = payload["traces"][0]
        assert trace["environment"] == "prod"
        assert trace["workspace_slug"] == "ws"

    def test_span_fields_have_defaults(self):
        sender = self._sender()
        # Minimal span — missing most optional fields
        spans = [{"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z"}]
        payload = sender._build_payload(spans)
        span = payload["traces"][0]["spans"][0]
        assert span["kind"] == "agent"
        assert span["status"] == "ok"
        assert span["input_tokens"] == 0
        assert span["output_tokens"] == 0
        assert span["model"] == ""


class TestSend:
    def test_empty_spans_is_noop(self):
        sender = ClaudeCodeSender(base_url="https://api.test", api_key="ak_test")
        with patch.object(sender, "_send_with_retry") as mock:
            sender.send([])
            mock.assert_not_called()

    def test_success_on_first_attempt(self):
        sender = ClaudeCodeSender(base_url="https://api.test", api_key="ak_test")
        mock_response = MagicMock()
        mock_response.status_code = 200

        with patch("staso.claude_code.sender.httpx.Client") as mock_client_cls:
            mock_client = MagicMock()
            mock_client.post.return_value = mock_response
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock(return_value=False)
            mock_client_cls.return_value = mock_client

            sender.send([{"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z"}])
            mock_client.post.assert_called_once()

    def test_no_retry_on_4xx(self):
        sender = ClaudeCodeSender(base_url="https://api.test", api_key="ak_test")
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.text = "Bad Request"

        with patch("staso.claude_code.sender.httpx.Client") as mock_client_cls:
            mock_client = MagicMock()
            mock_client.post.return_value = mock_response
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock(return_value=False)
            mock_client_cls.return_value = mock_client

            sender.send([{"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z"}])
            assert mock_client.post.call_count == 1  # no retry

    def test_retries_on_5xx(self):
        sender = ClaudeCodeSender(base_url="https://api.test", api_key="ak_test")
        error_response = MagicMock()
        error_response.status_code = 503
        ok_response = MagicMock()
        ok_response.status_code = 200

        with patch("staso.claude_code.sender.httpx.Client") as mock_client_cls, \
             patch("staso.claude_code.sender.time.sleep"):
            mock_client = MagicMock()
            mock_client.post.side_effect = [error_response, ok_response]
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock(return_value=False)
            mock_client_cls.return_value = mock_client

            sender.send([{"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z"}])
            assert mock_client.post.call_count == 2

    def test_retries_on_connection_error(self):
        sender = ClaudeCodeSender(base_url="https://api.test", api_key="ak_test")
        ok_response = MagicMock()
        ok_response.status_code = 200

        with patch("staso.claude_code.sender.httpx.Client") as mock_client_cls, \
             patch("staso.claude_code.sender.time.sleep"):
            mock_client = MagicMock()
            mock_client.post.side_effect = [httpx.ConnectError("refused"), ok_response]
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock(return_value=False)
            mock_client_cls.return_value = mock_client

            sender.send([{"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z"}])
            assert mock_client.post.call_count == 2

    def test_sends_correct_headers(self):
        sender = ClaudeCodeSender(base_url="https://api.test", api_key="ak_secret")
        mock_response = MagicMock()
        mock_response.status_code = 200

        with patch("staso.claude_code.sender.httpx.Client") as mock_client_cls:
            mock_client = MagicMock()
            mock_client.post.return_value = mock_response
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock(return_value=False)
            mock_client_cls.return_value = mock_client

            sender.send([{"trace_id": "t1", "span_id": "s1", "timestamp": "2026-01-01T00:00:00Z"}])
            call_kwargs = mock_client.post.call_args
            headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers")
            assert headers["X-API-Key"] == "ak_secret"
            assert headers["Content-Type"] == "application/json"

    def test_url_strips_trailing_slash(self):
        sender = ClaudeCodeSender(base_url="https://api.test/", api_key="ak_test")
        assert sender._url == "https://api.test/v1/traces/ingest"
