"""Unit tests for the session-aware HTTP client."""

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

import pytest

from byteb4rb1e.homeostat.util.http import HttpResponse, HttpSession


# -- Helpers -----------------------------------------------------------------


class FakeUrllibResponse(io.BytesIO):
    """Stand-in for urllib response objects."""

    def __init__(self, body, status=200, headers=None):
        super().__init__(
            body.encode() if isinstance(body, str) else body
        )
        self._status = status
        self._headers = list((headers or {}).items())

    def getcode(self):
        return self._status

    def getheaders(self):
        return self._headers

    def __enter__(self):
        return self

    def __exit__(self, *args):
        pass


@pytest.fixture
def session():
    return HttpSession(
        default_headers={"User-Agent": "test-agent"},
        timeout=10,
    )


# -- HttpResponse ------------------------------------------------------------


class TestHttpResponse:
    def test_text_decodes_body(self):
        resp = HttpResponse(
            status_code=200, headers={}, body=b"hello"
        )
        assert resp.text == "hello"

    def test_text_handles_invalid_utf8(self):
        resp = HttpResponse(
            status_code=200, headers={}, body=b"\xff\xfe"
        )
        assert isinstance(resp.text, str)


# -- HttpSession GET ---------------------------------------------------------


class TestHttpSessionGet:
    def test_get_returns_response(self, session):
        fake = FakeUrllibResponse("ok", status=200)
        with patch.object(
            session._opener, "open", return_value=fake
        ):
            resp = session.get("https://example.com")

        assert resp.status_code == 200
        assert resp.text == "ok"

    def test_get_appends_params(self, session):
        fake = FakeUrllibResponse("ok")
        with patch.object(
            session._opener, "open", return_value=fake
        ) as mock_open:
            session.get(
                "https://example.com",
                params={"key": "val"},
            )

        req = mock_open.call_args[0][0]
        assert "key=val" in req.full_url

    def test_get_includes_default_headers(self, session):
        fake = FakeUrllibResponse("ok")
        with patch.object(
            session._opener, "open", return_value=fake
        ) as mock_open:
            session.get("https://example.com")

        req = mock_open.call_args[0][0]
        assert req.get_header("User-agent") == "test-agent"

    def test_get_extra_headers_override_defaults(self, session):
        fake = FakeUrllibResponse("ok")
        with patch.object(
            session._opener, "open", return_value=fake
        ) as mock_open:
            session.get(
                "https://example.com",
                headers={"User-Agent": "override"},
            )

        req = mock_open.call_args[0][0]
        assert req.get_header("User-agent") == "override"


# -- HttpSession POST --------------------------------------------------------


class TestHttpSessionPost:
    def test_post_sends_form_data(self, session):
        fake = FakeUrllibResponse("ok")
        with patch.object(
            session._opener, "open", return_value=fake
        ) as mock_open:
            session.post(
                "https://example.com",
                data={"user": "test", "pass": "secret"},
            )

        req = mock_open.call_args[0][0]
        body = req.data.decode()
        assert "user=test" in body
        assert "pass=secret" in body

    def test_post_sets_content_type(self, session):
        fake = FakeUrllibResponse("ok")
        with patch.object(
            session._opener, "open", return_value=fake
        ) as mock_open:
            session.post(
                "https://example.com",
                data={"key": "val"},
            )

        req = mock_open.call_args[0][0]
        assert req.get_header("Content-type") == (
            "application/x-www-form-urlencoded"
        )

    def test_post_without_data_sends_no_body(self, session):
        fake = FakeUrllibResponse("ok")
        with patch.object(
            session._opener, "open", return_value=fake
        ) as mock_open:
            session.post("https://example.com")

        req = mock_open.call_args[0][0]
        assert req.data is None


# -- Error handling ----------------------------------------------------------


class TestErrorHandling:
    def test_http_error_returns_response(self, session):
        error = urllib.error.HTTPError(
            url="https://example.com",
            code=404,
            msg="Not Found",
            hdrs={"Content-Type": "text/html"},
            fp=io.BytesIO(b"not found"),
        )
        with patch.object(
            session._opener, "open", side_effect=error
        ):
            resp = session.get("https://example.com")

        assert resp.status_code == 404
        assert resp.text == "not found"
