"""Tests for Tier 2 features: debug logging, outbound request ID, per-operation timeout."""

from __future__ import annotations

import logging
import uuid

import pytest
from unittest.mock import MagicMock, patch

from optropic.client import Optropic


# ═══════════════════════════════════════════════════════════════════════════
# HELPERS
# ═══════════════════════════════════════════════════════════════════════════

def _make_client(**kwargs):
    return Optropic(api_key="optr_test_aaaabbbbccccddddeeee", **kwargs)


def _ok_response(body=None):
    mock = MagicMock()
    mock.ok = True
    mock.status_code = 200
    mock.json.return_value = body or {"id": "a1"}
    mock.headers = {"x-request-id": "server-req-123"}
    return mock


# ═══════════════════════════════════════════════════════════════════════════
# DEBUG LOGGING
# ═══════════════════════════════════════════════════════════════════════════

class TestDebugLogging:
    def test_debug_false_no_log(self, caplog):
        client = _make_client(debug=False)
        client._session.request = MagicMock(return_value=_ok_response())
        with caplog.at_level(logging.DEBUG, logger="optropic"):
            client._request("GET", "/assets/1")
        # No optropic log entries when debug=False
        optropic_records = [r for r in caplog.records if r.name == "optropic"]
        assert len(optropic_records) == 0

    def test_debug_true_logs_request_and_response(self, caplog):
        client = _make_client(debug=True)
        client._session.request = MagicMock(return_value=_ok_response())
        with caplog.at_level(logging.DEBUG, logger="optropic"):
            client._request("GET", "/assets/1")
        optropic_records = [r for r in caplog.records if r.name == "optropic"]
        assert len(optropic_records) >= 2  # at least request + response
        messages = " ".join(r.message for r in optropic_records)
        assert "GET" in messages
        assert "/assets/1" in messages
        assert "200" in messages
        assert "req=" in messages

    def test_debug_redacts_api_key(self, caplog):
        client = _make_client(debug=True)
        # Force a URL that contains the API key (shouldn't normally, but test redaction)
        client._session.request = MagicMock(return_value=_ok_response())
        with caplog.at_level(logging.DEBUG, logger="optropic"):
            client._request("GET", "/assets/1")
        messages = " ".join(r.message for r in caplog.records if r.name == "optropic")
        # The API key should not appear in plain text
        assert "aaaabbbbccccddddeeee" not in messages

    def test_debug_logs_retry(self, caplog):
        client = _make_client(debug=True, max_retries=1, base_delay=0.01)
        mock_503 = MagicMock()
        mock_503.ok = False
        mock_503.status_code = 503
        mock_503.json.return_value = {"code": "SERVICE_UNAVAILABLE", "message": "Down"}
        mock_503.text = "Down"
        mock_503.headers = {}

        client._session.request = MagicMock(
            side_effect=[mock_503, _ok_response()]
        )
        with patch("optropic.client.time.sleep"):
            with caplog.at_level(logging.DEBUG, logger="optropic"):
                client._request("GET", "/health")
        messages = " ".join(r.message for r in caplog.records if r.name == "optropic")
        assert "retry" in messages.lower()

    def test_redact_helper(self):
        text = "key=optr_live_abcdefghijklmnop1234 token=optr_test_xyz123456789abcdef"
        redacted = Optropic._redact(text)
        assert "optr_live_****" in redacted
        assert "optr_test_****" in redacted
        assert "abcdefghijklmnop1234" not in redacted


# ═══════════════════════════════════════════════════════════════════════════
# OUTBOUND REQUEST ID
# ═══════════════════════════════════════════════════════════════════════════

class TestOutboundRequestID:
    def test_sends_x_request_id_header(self):
        client = _make_client()
        client._session.request = MagicMock(return_value=_ok_response())
        client._request("GET", "/assets/1")

        call_kwargs = client._session.request.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers", {})
        assert "X-Request-ID" in headers
        # Should be a valid UUID
        uuid.UUID(headers["X-Request-ID"])  # raises if invalid

    def test_request_id_is_unique_per_call(self):
        client = _make_client()
        client._session.request = MagicMock(return_value=_ok_response())

        client._request("GET", "/assets/1")
        id1 = client._session.request.call_args.kwargs.get("headers", {}).get("X-Request-ID")

        client._request("GET", "/assets/2")
        id2 = client._session.request.call_args.kwargs.get("headers", {}).get("X-Request-ID")

        assert id1 != id2

    def test_raw_request_also_sends_request_id(self):
        client = _make_client()
        mock_resp = MagicMock()
        mock_resp.ok = True
        mock_resp.status_code = 200
        mock_resp.content = b"\x00" * 100
        mock_resp.headers = {}
        client._session.request = MagicMock(return_value=mock_resp)

        client._request_raw("GET", "/sync/revocation-filter")
        call_kwargs = client._session.request.call_args
        headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers", {})
        assert "X-Request-ID" in headers


# ═══════════════════════════════════════════════════════════════════════════
# PER-OPERATION TIMEOUT
# ═══════════════════════════════════════════════════════════════════════════

class TestPerOperationTimeout:
    def test_default_timeout_used(self):
        client = _make_client(timeout=15.0)
        client._session.request = MagicMock(return_value=_ok_response())
        client._request("GET", "/assets/1")

        call_kwargs = client._session.request.call_args
        assert call_kwargs.kwargs.get("timeout") == 15.0

    def test_per_operation_timeout_overrides(self):
        client = _make_client(timeout=30.0)
        client._session.request = MagicMock(return_value=_ok_response())
        client._request("GET", "/assets/1", timeout=5.0)

        call_kwargs = client._session.request.call_args
        assert call_kwargs.kwargs.get("timeout") == 5.0

    def test_raw_request_per_operation_timeout(self):
        client = _make_client(timeout=30.0)
        mock_resp = MagicMock()
        mock_resp.ok = True
        mock_resp.status_code = 200
        mock_resp.content = b"\x00"
        mock_resp.headers = {}
        client._session.request = MagicMock(return_value=mock_resp)

        client._request_raw("GET", "/sync/revocation-filter", timeout=120.0)
        call_kwargs = client._session.request.call_args
        assert call_kwargs.kwargs.get("timeout") == 120.0
