import io
import json
import unittest
import urllib.error
import urllib.request
from unittest.mock import patch

# Adjust to your real paths
from src.aiel_sdk.client import AielClient
from src.aiel_sdk.transport import UrllibTransport, RetryPolicy, normalize_join, encode_query
from src.aiel_sdk.errors import ApiError


class _FakeResponse:
    def __init__(self, body: bytes, headers=None):
        self._body = body
        self.headers = headers or {}

    def read(self) -> bytes:
        return self._body

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False


class _CaptureUrlopen:
    """
    urlopen replacement that captures the last request.
    Can be configured with either a single response or a list of side effects.
    """
    def __init__(self, side_effects):
        self.side_effects = list(side_effects) if isinstance(side_effects, list) else [side_effects]
        self.calls = []
        self.last_request = None

    def __call__(self, req, timeout=None):
        self.calls.append({"req": req, "timeout": timeout})
        self.last_request = req
        eff = self.side_effects.pop(0)
        if isinstance(eff, Exception):
            raise eff
        return eff


def _http_error(url: str, code: int, reason: str, body: bytes, headers=None):
    fp = io.BytesIO(body)
    err = urllib.error.HTTPError(url=url, code=code, msg=reason, hdrs=headers, fp=fp)
    return err


class TransportUnitTests(unittest.TestCase):
    def test_normalize_join(self):
        self.assertEqual(normalize_join("https://api.example.com/", "/v1/x"), "https://api.example.com/v1/x")
        self.assertEqual(normalize_join("https://api.example.com", "v1/x"), "https://api.example.com/v1/x")

    def test_encode_query(self):
        qs = encode_query({"a": 1, "b": None, "c": "x y"})
        # b is None -> excluded; space encoded
        self.assertIn("a=1", qs)
        self.assertIn("c=x+y", qs)
        self.assertNotIn("b=", qs)

    @patch("aiel_sdk.transport.new_request_id", return_value="req_test_123")
    def test_transport_builds_request_url_headers_and_body(self, _rid):
        transport = UrllibTransport()
        cap = _CaptureUrlopen(
            _FakeResponse(json.dumps({"ok": True}).encode("utf-8"))
        )

        with patch("urllib.request.urlopen", new=cap):
            out = transport.request(
                service="memory",
                method="POST",
                base_url="https://api.example.com",
                path="/v1/test",
                headers={"Authorization": "Bearer t", "User-Agent": "ua"},
                params={"limit": 50},
                json_body={"hello": "world"},
                timeout=12.3,
                retry=RetryPolicy(max_attempts=1),
            )

        self.assertEqual(out["ok"], True)
        req = cap.last_request
        self.assertIsNotNone(req)

        # URL
        self.assertEqual(req.full_url, "https://api.example.com/v1/test?limit=50")

        # Method
        self.assertEqual(req.get_method(), "POST")

        # Headers
        self.assertEqual(req.headers.get("Authorization"), "Bearer t")
        self.assertEqual(req.get_header("User-agent"), "ua")
        self.assertEqual(req.headers.get("X-Request-Id"), "req_test_123")
        self.assertEqual(req.headers.get("Content-Type"), "application/json")

        # Body
        self.assertEqual(json.loads(req.data.decode("utf-8")), {"hello": "world"})

        # Timeout passed to urlopen
        self.assertEqual(cap.calls[0]["timeout"], 12.3)

    @patch("src.aiel_sdk.transport.new_request_id", return_value="req_env_ok")
    def test_transport_unwraps_envelope_ok(self, _rid):
        transport = UrllibTransport()
        body = json.dumps({"ok": True, "result": {"x": 1}, "error": None}).encode("utf-8")
        cap = _CaptureUrlopen(_FakeResponse(body))

        with patch("urllib.request.urlopen", new=cap):
            out = transport.request(
                service="memory",
                method="GET",
                base_url="https://api.example.com",
                path="/v1/test",
                headers={},
                retry=RetryPolicy(max_attempts=1),
            )

        self.assertEqual(out, {"x": 1})

    @patch("src.aiel_sdk.transport.new_request_id", return_value="req_env_fail")
    def test_transport_raises_on_envelope_ok_false_even_if_http_200(self, _rid):
        transport = UrllibTransport()
        body = json.dumps(
            {
                "ok": False,
                "http_status": 400,
                "result": None,
                "error": {"code": "VALIDATION_ERROR", "message": "bad"},
            }
        ).encode("utf-8")
        cap = _CaptureUrlopen(_FakeResponse(body))

        with patch("urllib.request.urlopen", new=cap):
            with self.assertRaises(ApiError) as ctx:
                transport.request(
                    service="memory",
                    method="GET",
                    base_url="https://api.example.com",
                    path="/v1/test",
                    headers={},
                    retry=RetryPolicy(max_attempts=1),
                )

        self.assertEqual(ctx.exception.status, 400)
        self.assertEqual(ctx.exception.code, "VALIDATION_ERROR")
        self.assertIn("bad", str(ctx.exception))
        self.assertEqual(ctx.exception.request_id, "req_env_fail")

    @patch("src.aiel_sdk.transport.new_request_id", return_value="req_http_err")
    def test_transport_parses_http_error_detail_shape(self, _rid):
        transport = UrllibTransport()

        err = _http_error(
            url="https://api.example.com/v1/test",
            code=400,
            reason="Bad Request",
            body=b'{"detail":{"code":"VALIDATION_ERROR","message":"nope"}}',
        )
        cap = _CaptureUrlopen(err)

        with patch("urllib.request.urlopen", new=cap):
            with self.assertRaises(ApiError) as ctx:
                transport.request(
                    service="memory",
                    method="GET",
                    base_url="https://api.example.com",
                    path="/v1/test",
                    headers={},
                    retry=RetryPolicy(max_attempts=1),
                )

        self.assertEqual(ctx.exception.status, 400)
        self.assertEqual(ctx.exception.code, "VALIDATION_ERROR")
        self.assertIn("nope", str(ctx.exception))
        self.assertIn("url=https://api.example.com/v1/test", str(ctx.exception))

    @patch("src.aiel_sdk.transport.new_request_id", return_value="req_retry_429")
    def test_transport_retries_on_429_and_respects_retry_after(self, _rid):
        transport = UrllibTransport()

        # First call: HTTP 429 with Retry-After: 0.0
        headers = {"Retry-After": "0"}
        err = _http_error(
            url="https://api.example.com/v1/test",
            code=429,
            reason="Too Many Requests",
            body=b'{"detail":{"code":"RATE_LIMIT","message":"slow down"}}',
            headers=headers,
        )
        # Second call: success
        ok = _FakeResponse(b'{"ok": true}')

        cap = _CaptureUrlopen([err, ok])

        with patch("urllib.request.urlopen", new=cap), patch("time.sleep") as sleep_mock:
            out = transport.request(
                service="integrations",
                method="GET",
                base_url="https://api.example.com",
                path="/v1/test",
                headers={},
                retry=RetryPolicy(max_attempts=2),
            )

        self.assertEqual(out["ok"], True)
        self.assertEqual(len(cap.calls), 2)
        sleep_mock.assert_called()  # should have slept at least once

    @patch("src.aiel_sdk.transport.new_request_id", return_value="req_retry_urLError")
    def test_transport_retries_on_network_error(self, _rid):
        transport = UrllibTransport()
        net_err = urllib.error.URLError("dns failed")
        ok = _FakeResponse(b'{"ok": true}')
        cap = _CaptureUrlopen([net_err, ok])

        with patch("urllib.request.urlopen", new=cap), patch("time.sleep") as _:
            out = transport.request(
                service="memory",
                method="GET",
                base_url="https://api.example.com",
                path="/v1/test",
                headers={},
                retry=RetryPolicy(max_attempts=2),
            )

        self.assertEqual(out["ok"], True)
        self.assertEqual(len(cap.calls), 2)


class ClientIntegrationTests(unittest.TestCase):
    @patch.dict("os.environ", {"AIEL_TOKEN": "env_token"}, clear=True)
    def test_client_build_headers_authorization_and_user_agent(self):
        client = AielClient(base_url="https://api.example.com", user_agent="ua-test")
        hdrs = client._build_headers()
        self.assertEqual(hdrs["Authorization"], "Bearer env_token")
        self.assertEqual(hdrs["User-Agent"], "ua-test")
        self.assertEqual(hdrs["Accept"], "application/json")

    @patch.dict("os.environ", {"AIEL_TOKEN": "env_token"}, clear=True)
    def test_client_x_api_token_fallback(self):
        client = AielClient(
            base_url="https://api.example.com",
            use_x_api_token_fallback=True,
        )
        hdrs = client._build_headers()
        self.assertEqual(hdrs["Authorization"], "Bearer env_token")
        self.assertEqual(hdrs["X-API-Token"], "env_token")

    def test_client_extra_headers_are_preserved(self):
        client = AielClient(
            base_url="https://api.example.com",
            api_key="k",
            extra_headers={"X-Test": "1"},
        )
        hdrs = client._build_headers()
        self.assertEqual(hdrs["X-Test"], "1")

    def test_client_request_calls_transport_with_expected_args(self):
        class _FakeTransport:
            def __init__(self):
                self.calls = []
            def request(self, **kw):
                self.calls.append(kw)
                return {"ok": True}

        t = _FakeTransport()
        client = AielClient(base_url="https://api.example.com", api_key="k", transport=t)
        out = client._request("memory", "GET", "/v1/x", params={"a": 1}, json_body=None, timeout=9.0, request_id="r1")
        self.assertEqual(out["ok"], True)

        call = t.calls[0]
        self.assertEqual(call["service"], "memory")
        self.assertEqual(call["method"], "GET")
        self.assertEqual(call["base_url"], "https://api.example.com")
        self.assertEqual(call["path"], "/v1/x")
        self.assertEqual(call["params"], {"a": 1})
        self.assertEqual(call["timeout"], 9.0)
        self.assertEqual(call["request_id"], "r1")


if __name__ == "__main__":
    unittest.main(verbosity=2)