"""Tests for the Flowlines session management module."""

from __future__ import annotations

import json
from io import BytesIO
from unittest.mock import MagicMock, patch
from urllib.error import HTTPError, URLError

import pytest

from flowlines._session import (
    FlowlinesSessionError,
    aend_session,
    end_session,
)


class TestEndSession:
    """Tests for end_session()."""

    @patch("flowlines._session.urlopen")
    def test_success(self, mock_urlopen: MagicMock) -> None:
        """Completes without error on any 2xx response."""
        mock_urlopen.return_value = MagicMock()

        end_session(
            api_key="test-key",
            api_endpoint="https://api.flowlines.ai",
            user_id="u1",
            session_id="s1",
        )

    @patch("flowlines._session.urlopen")
    def test_sends_correct_request(self, mock_urlopen: MagicMock) -> None:
        """Sends POST with correct URL, headers, and body."""
        mock_urlopen.return_value = MagicMock()

        end_session(
            api_key="my-secret-key",
            api_endpoint="https://api.flowlines.ai",
            user_id="u1",
            session_id="s1",
        )

        request = mock_urlopen.call_args[0][0]
        assert request.full_url == "https://api.flowlines.ai/v1/mark-session-as-ended"
        assert request.get_header("X-flowlines-api-key") == "my-secret-key"
        assert request.get_header("Content-type") == "application/json"
        assert request.get_method() == "POST"
        body = json.loads(request.data)
        assert body == {"session_id": "s1", "user_id": "u1"}

    @patch("flowlines._session.urlopen")
    def test_trailing_slash_stripped(self, mock_urlopen: MagicMock) -> None:
        """Trailing slash on the endpoint is stripped."""
        mock_urlopen.return_value = MagicMock()

        end_session(
            api_key="key",
            api_endpoint="https://api.flowlines.ai/",
            user_id="u1",
            session_id="s1",
        )

        request = mock_urlopen.call_args[0][0]
        assert request.full_url == "https://api.flowlines.ai/v1/mark-session-as-ended"

    @patch("flowlines._session.urlopen")
    def test_http_error_json_body(self, mock_urlopen: MagicMock) -> None:
        """Raises FlowlinesSessionError with status_code on HTTP error (JSON body)."""
        error_body = json.dumps({"error": "missing or invalid session_id"}).encode()
        mock_urlopen.side_effect = HTTPError(
            url="https://api.flowlines.ai/mark-session-as-ended",
            code=400,
            msg="Bad Request",
            hdrs=MagicMock(),
            fp=BytesIO(error_body),
        )

        with pytest.raises(FlowlinesSessionError, match="HTTP 400") as exc_info:
            end_session(
                api_key="key",
                api_endpoint="https://api.flowlines.ai",
                user_id="u1",
                session_id="",
            )

        assert exc_info.value.status_code == 400
        assert "missing or invalid session_id" in str(exc_info.value)

    @patch("flowlines._session.urlopen")
    def test_http_error_plain_text_body(self, mock_urlopen: MagicMock) -> None:
        """Raises FlowlinesSessionError with plain text body on HTTP error."""
        mock_urlopen.side_effect = HTTPError(
            url="https://api.flowlines.ai/mark-session-as-ended",
            code=500,
            msg="Internal Server Error",
            hdrs=MagicMock(),
            fp=BytesIO(b"Something went wrong"),
        )

        with pytest.raises(FlowlinesSessionError, match="HTTP 500") as exc_info:
            end_session(
                api_key="key",
                api_endpoint="https://api.flowlines.ai",
                user_id="u1",
                session_id="s1",
            )

        assert exc_info.value.status_code == 500

    @patch("flowlines._session.urlopen")
    def test_http_error_404_ignored(self, mock_urlopen: MagicMock) -> None:
        """Returns without raising on HTTP 404."""
        mock_urlopen.side_effect = HTTPError(
            url="https://api.flowlines.ai/mark-session-as-ended",
            code=404,
            msg="Not Found",
            hdrs=MagicMock(),
            fp=BytesIO(b'{"error": "session not found"}'),
        )

        end_session(
            api_key="key",
            api_endpoint="https://api.flowlines.ai",
            user_id="u1",
            session_id="s1",
        )

    @patch("flowlines._session.urlopen")
    def test_network_error(self, mock_urlopen: MagicMock) -> None:
        """Raises FlowlinesSessionError with no status_code on network error."""
        mock_urlopen.side_effect = URLError("Connection refused")

        with pytest.raises(
            FlowlinesSessionError, match="Connection refused"
        ) as exc_info:
            end_session(
                api_key="key",
                api_endpoint="https://api.flowlines.ai",
                user_id="u1",
                session_id="s1",
            )

        assert exc_info.value.status_code is None


class TestAendSession:
    """Tests for aend_session()."""

    @patch("flowlines._session.end_session")
    async def test_delegates_to_end_session(self, mock_end: MagicMock) -> None:
        """aend_session() delegates to end_session via asyncio.to_thread."""
        await aend_session(
            api_key="key",
            api_endpoint="https://api.flowlines.ai",
            user_id="u1",
            session_id="s1",
        )

        mock_end.assert_called_once_with(
            api_key="key",
            api_endpoint="https://api.flowlines.ai",
            user_id="u1",
            session_id="s1",
        )

    @patch("flowlines._session.end_session")
    async def test_propagates_error(self, mock_end: MagicMock) -> None:
        """aend_session() propagates FlowlinesSessionError from end_session."""
        mock_end.side_effect = FlowlinesSessionError("test error", status_code=500)

        with pytest.raises(FlowlinesSessionError, match="test error"):
            await aend_session(
                api_key="key",
                api_endpoint="https://api.flowlines.ai",
                user_id="u1",
                session_id="s1",
            )


class TestFlowlinesSessionError:
    """Tests for FlowlinesSessionError."""

    def test_with_status_code(self) -> None:
        """FlowlinesSessionError stores status_code."""
        err = FlowlinesSessionError("error", status_code=401)
        assert str(err) == "error"
        assert err.status_code == 401

    def test_without_status_code(self) -> None:
        """FlowlinesSessionError defaults status_code to None."""
        err = FlowlinesSessionError("error")
        assert err.status_code is None

    def test_is_exception(self) -> None:
        """FlowlinesSessionError is an Exception."""
        assert issubclass(FlowlinesSessionError, Exception)
