"""Unit tests for the connectr Python SDK.

All tests use mocked HTTP servers and no real gRPC connection.
"""

from __future__ import annotations

import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any
from urllib.parse import urlparse

import pytest

from service_bridge.http_catalog import extract_trace_from_headers, _new_hex_id, HttpSpan
from service_bridge.servicebridge import EventContext, Options, ServiceBridge


# ─── Helpers ──────────────────────────────────────────────────────────────────

class _MockAdminHandler(BaseHTTPRequestHandler):
    """A minimal HTTP server that records POST /api/http/register calls."""

    calls: list[dict] = []
    responses: dict[str, tuple[int, str]] = {}

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        parsed = {}
        if body:
            try:
                parsed = json.loads(body)
            except json.JSONDecodeError:
                pass
        _MockAdminHandler.calls.append({"path": self.path, "body": parsed, "headers": dict(self.headers)})

        status, response_body = _MockAdminHandler.responses.get(self.path, (200, "{}"))
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(response_body.encode())

    def log_message(self, format, *args):  # noqa: A002
        pass  # suppress output


def make_mock_server() -> tuple[HTTPServer, str]:
    """Start a mock HTTP server on a random port and return (server, base_url)."""
    _MockAdminHandler.calls = []
    _MockAdminHandler.responses = {}
    server = HTTPServer(("127.0.0.1", 0), _MockAdminHandler)
    port = server.server_address[1]
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    return server, f"http://127.0.0.1:{port}"


# ─── ServiceBridge construction ───────────────────────────────────────────────

class TestServiceBridgeInit:
    def test_default_creation(self):
        svc = ServiceBridge("localhost:14445", "key", "mysvc")
        assert svc is not None
        assert svc._service_name == "mysvc"
        assert svc._service_key == "key"

    def test_with_options(self):
        opts = Options(admin_url="http://localhost:9000", heartbeat_interval_ms=5000)
        svc = ServiceBridge("localhost:14445", "key", "svc", opts)
        assert svc._opts.heartbeat_interval_ms == 5000
        assert svc._opts.admin_url == "http://localhost:9000"

    def test_instance_id_is_unique(self):
        a = ServiceBridge("localhost:14445", "key", "svc")
        b = ServiceBridge("localhost:14445", "key", "svc")
        assert a._instance_id != b._instance_id


# ─── handle_rpc decorator ─────────────────────────────────────────────────────

class TestHandleRpc:
    def test_registers_handler(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")

        @svc.handle_rpc("my.fn")
        async def handler(payload):
            return {"ok": True}

        assert "my.fn" in svc._rpc_handlers
        assert svc._rpc_handlers["my.fn"] is handler

    def test_returns_original_function(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")

        @svc.handle_rpc("fn1")
        def sync_handler(payload):
            return {}

        assert svc._rpc_handlers["fn1"] is sync_handler

    def test_multiple_handlers(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")

        @svc.handle_rpc("fn.a")
        async def h_a(payload): return {}

        @svc.handle_rpc("fn.b")
        async def h_b(payload): return {}

        assert "fn.a" in svc._rpc_handlers
        assert "fn.b" in svc._rpc_handlers
        assert len(svc._rpc_handlers) == 2

    def test_overwrite_handler(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")

        @svc.handle_rpc("fn")
        async def h1(payload): return {"v": 1}

        @svc.handle_rpc("fn")
        async def h2(payload): return {"v": 2}

        assert svc._rpc_handlers["fn"] is h2


# ─── handle_event decorator ───────────────────────────────────────────────────

class TestHandleEvent:
    def test_registers_with_default_group(self):
        svc = ServiceBridge("localhost:14445", "key", "myservice")

        @svc.handle_event("order.created")
        async def handler(payload, ec): pass

        assert "myservice.order.created" in svc._event_handlers

    def test_registers_with_custom_group(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")

        @svc.handle_event("order.created", group_name="payments.grp")
        async def handler(payload, ec): pass

        assert "payments.grp" in svc._event_handlers

    def test_stores_topic(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")

        @svc.handle_event("my.topic")
        async def handler(payload, ec): pass

        key = "svc.my.topic"
        assert svc._event_handlers[key]["topic"] == "my.topic"

    def test_stores_retry_policy(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")
        policy = '{"maxAttempts": 3}'

        @svc.handle_event("t", retry_policy_json=policy)
        async def handler(payload, ec): pass

        key = "svc.t"
        assert svc._event_handlers[key]["retry_policy_json"] == policy

    def test_returns_original_function(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")

        @svc.handle_event("t")
        def sync_handler(payload, ec): pass

        assert svc._event_handlers["svc.t"]["handler"] is sync_handler


# ─── EventContext ─────────────────────────────────────────────────────────────

class TestEventContext:
    def _make_ec(self):
        retry_calls = []
        reject_calls = []
        ec = EventContext(
            trace_id="trace1",
            span_id="span1",
            topic="t",
            group_name="g",
            message_id="msg1",
            attempt=1,
            headers={},
            _retry_fn=lambda ms: retry_calls.append(ms),
            _reject_fn=lambda r: reject_calls.append(r),
        )
        return ec, retry_calls, reject_calls

    def test_retry_stores_delay(self):
        ec, retry_calls, _ = self._make_ec()
        ec.retry(500)
        assert retry_calls == [500]

    def test_retry_default_delay(self):
        ec, retry_calls, _ = self._make_ec()
        ec.retry()
        assert retry_calls == [1000]

    def test_reject_stores_reason(self):
        ec, _, reject_calls = self._make_ec()
        ec.reject("bad_payload")
        assert reject_calls == ["bad_payload"]

    def test_reject_default_reason(self):
        ec, _, reject_calls = self._make_ec()
        ec.reject()
        assert reject_calls == ["rejected_by_consumer"]


# ─── extract_trace_from_headers ───────────────────────────────────────────────

class TestExtractTrace:
    def test_w3c_traceparent(self):
        headers = {"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}
        trace_id, parent_span_id = extract_trace_from_headers(headers)
        assert trace_id == "4bf92f3577b34da6a3ce929d0e0e4736"
        assert parent_span_id == "00f067aa0ba902b7"

    def test_x_trace_id(self):
        headers = {"x-trace-id": "my-trace-123"}
        trace_id, parent_span_id = extract_trace_from_headers(headers)
        assert trace_id == "my-trace-123"
        assert parent_span_id == ""

    def test_no_headers_generates_id(self):
        trace_id, parent_span_id = extract_trace_from_headers({})
        assert trace_id != ""
        assert len(trace_id) == 32  # 16 bytes hex
        assert parent_span_id == ""

    def test_invalid_traceparent_falls_back(self):
        headers = {"traceparent": "bad-value"}
        trace_id, _ = extract_trace_from_headers(headers)
        assert trace_id != ""

    def test_case_insensitive_headers(self):
        headers = {"X-Trace-Id": "upper-case-header"}
        trace_id, _ = extract_trace_from_headers(headers)
        assert trace_id == "upper-case-header"

    def test_traceparent_takes_priority_over_x_trace_id(self):
        headers = {
            "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
            "x-trace-id": "should-be-ignored",
        }
        trace_id, _ = extract_trace_from_headers(headers)
        assert trace_id == "4bf92f3577b34da6a3ce929d0e0e4736"


# ─── _new_hex_id ──────────────────────────────────────────────────────────────

class TestNewHexId:
    def test_default_length(self):
        result = _new_hex_id()
        assert len(result) == 32  # 16 bytes * 2

    def test_custom_length(self):
        result = _new_hex_id(8)
        assert len(result) == 16

    def test_unique(self):
        ids = {_new_hex_id() for _ in range(100)}
        assert len(ids) == 100


# ─── HttpSpan ─────────────────────────────────────────────────────────────────

class TestHttpSpan:
    def _make_mock_client(self):
        """Return a minimal object that has _report_call."""
        calls = []

        class MockClient:
            def _report_call(self, *args, **kwargs):
                calls.append(args)

        return MockClient(), calls

    def test_start_http_span_returns_span(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")
        span = svc.start_http_span("GET", "/users")
        assert span is not None
        assert span.trace_id != ""
        assert span.span_id != ""

    def test_start_http_span_propagates_trace_id(self):
        svc = ServiceBridge("localhost:14445", "key", "svc")
        span = svc.start_http_span("GET", "/users", trace_id="fixed-trace-id")
        assert span.trace_id == "fixed-trace-id"

    def test_span_end_calls_report(self):
        mock_client, calls = self._make_mock_client()
        span = HttpSpan(
            trace_id="t1",
            span_id="s1",
            _fn="http:GET:/test",
            _started_at=0,
            _client=mock_client,
            _input_buf=b"{}",
        )
        span.end(status_code=200)
        assert len(calls) == 1

    def test_span_end_success_on_2xx(self):
        mock_client, calls = self._make_mock_client()
        span = HttpSpan(
            trace_id="t", span_id="s", _fn="f", _started_at=0,
            _client=mock_client, _input_buf=b"{}",
        )
        span.end(status_code=201)
        assert calls[0][5] is True  # success=True

    def test_span_end_failure_on_5xx(self):
        mock_client, calls = self._make_mock_client()
        span = HttpSpan(
            trace_id="t", span_id="s", _fn="f", _started_at=0,
            _client=mock_client, _input_buf=b"{}",
        )
        span.end(status_code=500)
        assert calls[0][5] is False  # success=False

    def test_span_end_explicit_success_override(self):
        mock_client, calls = self._make_mock_client()
        span = HttpSpan(
            trace_id="t", span_id="s", _fn="f", _started_at=0,
            _client=mock_client, _input_buf=b"{}",
        )
        span.end(status_code=500, success=True)
        assert calls[0][5] is True


# ─── register_http_endpoint ───────────────────────────────────────────────────

class TestRegisterHttpEndpoint:
    def test_posts_to_correct_endpoint(self):
        server, base_url = make_mock_server()
        try:
            svc = ServiceBridge(
                "localhost:14445", "testkey", "testsvc",
                Options(admin_url=base_url)
            )
            svc.register_http_endpoint("GET", "/users")

            assert len(_MockAdminHandler.calls) == 1
            call = _MockAdminHandler.calls[0]
            assert call["path"] == "/api/http/register"
            assert call["body"]["method"] == "GET"
            assert call["body"]["route_pattern"] == "/users"
            assert call["body"]["service_name"] == "testsvc"
        finally:
            server.shutdown()

    def test_sends_service_key_header(self):
        server, base_url = make_mock_server()
        try:
            svc = ServiceBridge(
                "localhost:14445", "secretkey", "svc",
                Options(admin_url=base_url)
            )
            svc.register_http_endpoint("POST", "/orders")

            call = _MockAdminHandler.calls[0]
            # Headers may be stored with mixed case by BaseHTTPRequestHandler
            headers_lower = {k.lower(): v for k, v in call["headers"].items()}
            assert headers_lower.get("x-service-key") == "secretkey"
        finally:
            server.shutdown()

    def test_raises_on_server_error(self):
        server, base_url = make_mock_server()
        _MockAdminHandler.responses["/api/http/register"] = (401, "unauthorized")
        try:
            svc = ServiceBridge(
                "localhost:14445", "key", "svc",
                Options(admin_url=base_url)
            )
            with pytest.raises(RuntimeError, match="401"):
                svc.register_http_endpoint("GET", "/protected")
        finally:
            server.shutdown()

    def test_sends_allowed_callers(self):
        server, base_url = make_mock_server()
        try:
            svc = ServiceBridge(
                "localhost:14445", "key", "svc",
                Options(admin_url=base_url)
            )
            svc.register_http_endpoint("GET", "/admin", allowed_callers=["admin-service"])

            call = _MockAdminHandler.calls[0]
            assert call["body"]["allowed_callers"] == ["admin-service"]
        finally:
            server.shutdown()


# ─── admin_base derivation ────────────────────────────────────────────────────

class TestAdminBase:
    def test_derived_from_grpc_url(self):
        svc = ServiceBridge("myhost:14445", "key", "svc")
        assert svc._admin_base() == "http://myhost:14444"

    def test_explicit_admin_url(self):
        svc = ServiceBridge("host:14445", "key", "svc", Options(admin_url="http://custom:9999"))
        assert svc._admin_base() == "http://custom:9999"
