"""Tests for SessionsResource."""

from __future__ import annotations

from typing import Any
from unittest.mock import AsyncMock
from uuid import uuid4

import pytest

from mcpgateway_sdk.models.session import Session
from mcpgateway_sdk.resources.sessions import SessionsResource


@pytest.fixture
def http() -> AsyncMock:
    """Mock HTTPClient."""
    return AsyncMock()


@pytest.fixture
def sessions(http: AsyncMock) -> SessionsResource:
    """SessionsResource with mocked HTTP."""
    return SessionsResource(http)


def _session_data(**overrides: Any) -> dict[str, Any]:
    data: dict[str, Any] = {
        "id": str(uuid4()),
        "server_id": None,
        "bundle_id": None,
        "process_state": {},
        "external_sessions": {},
        "last_accessed_at": "2026-03-29T12:00:00Z",
        "expires_at": "2026-03-30T12:00:00Z",
        "is_active": True,
        "is_expired": False,
        "is_idle": False,
        "created_at": "2026-03-29T12:00:00Z",
        "allowed_tool_names": None,
        "denied_tool_names": None,
    }
    data.update(overrides)
    return data


# ---------------------------------------------------------------------------
# _endpoints dict
# ---------------------------------------------------------------------------


class TestEndpointsDict:
    """Tests for _endpoints class variable."""

    def test_endpoints_has_create(self) -> None:
        assert SessionsResource._endpoints["create"] == ("POST", "/api/v1/sessions")

    def test_endpoints_has_list(self) -> None:
        assert SessionsResource._endpoints["list"] == ("GET", "/api/v1/sessions")

    def test_endpoints_has_get(self) -> None:
        assert SessionsResource._endpoints["get"] == (
            "GET",
            "/api/v1/sessions/{session_id}",
        )

    def test_endpoints_has_update(self) -> None:
        assert SessionsResource._endpoints["update"] == (
            "PATCH",
            "/api/v1/sessions/{session_id}",
        )

    def test_endpoints_has_delete(self) -> None:
        assert SessionsResource._endpoints["delete"] == (
            "DELETE",
            "/api/v1/sessions/{session_id}",
        )

    def test_endpoints_has_touch(self) -> None:
        assert SessionsResource._endpoints["touch"] == (
            "POST",
            "/api/v1/sessions/{session_id}/touch",
        )

    def test_endpoints_has_cleanup(self) -> None:
        assert SessionsResource._endpoints["cleanup"] == (
            "POST",
            "/api/v1/sessions/cleanup",
        )

    def test_endpoints_has_stats(self) -> None:
        assert SessionsResource._endpoints["stats"] == (
            "GET",
            "/api/v1/sessions/stats",
        )

    def test_endpoints_has_exactly_eight_entries(self) -> None:
        assert len(SessionsResource._endpoints) == 8


# ---------------------------------------------------------------------------
# create
# ---------------------------------------------------------------------------


class TestCreate:
    """Tests for SessionsResource.create."""

    async def test_sends_post_with_empty_body(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _session_data()

        # Act
        await sessions.create()

        # Assert
        http.request.assert_awaited_once_with("POST", "/api/v1/sessions", json={})

    async def test_sends_post_with_bundle_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        bundle_id = uuid4()
        http.request.return_value = _session_data()

        # Act
        await sessions.create(bundle_id=bundle_id)

        # Assert
        call_kwargs = http.request.call_args
        body = call_kwargs.kwargs["json"]
        assert body["bundle_id"] == str(bundle_id)

    async def test_sends_post_with_allowed_tool_names(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        tools = ["PREFIX__tool_a", "PREFIX__tool_b"]
        http.request.return_value = _session_data(allowed_tool_names=tools)

        # Act
        await sessions.create(allowed_tool_names=tools)

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["allowed_tool_names"] == tools

    async def test_sends_post_with_server_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        server_id = uuid4()
        http.request.return_value = _session_data()

        # Act
        await sessions.create(server_id=server_id)

        # Assert
        call_kwargs = http.request.call_args
        body = call_kwargs.kwargs["json"]
        assert body["server_id"] == str(server_id)

    async def test_returns_session_model(self, sessions: SessionsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _session_data(is_expired=True)

        # Act
        result = await sessions.create()

        # Assert
        assert isinstance(result, Session)
        assert result.is_expired is True

    async def test_sends_post_with_denied_tool_names(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        tools = ["PREFIX__tool_x", "PREFIX__tool_y"]
        http.request.return_value = _session_data(denied_tool_names=tools)

        # Act
        await sessions.create(denied_tool_names=tools)

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["denied_tool_names"] == tools

    async def test_raises_if_both_server_and_bundle(self, sessions: SessionsResource) -> None:
        # Act / Assert
        with pytest.raises(ValueError, match="mutually exclusive"):
            await sessions.create(server_id=uuid4(), bundle_id=uuid4())


# ---------------------------------------------------------------------------
# list
# ---------------------------------------------------------------------------


class TestList:
    """Tests for SessionsResource.list."""

    async def test_sends_get_with_default_params(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await sessions.list()

        # Assert
        http.request.assert_awaited_once_with(
            "GET",
            "/api/v1/sessions",
            params={},
        )

    async def test_sends_get_with_server_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        server_id = uuid4()
        http.request.return_value = {"items": []}

        # Act
        await sessions.list(server_id=server_id)

        # Assert
        params = http.request.call_args.kwargs["params"]
        assert params["server_id"] == str(server_id)

    async def test_sends_get_with_bundle_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        bundle_id = uuid4()
        http.request.return_value = {"items": []}

        # Act
        await sessions.list(bundle_id=bundle_id)

        # Assert
        params = http.request.call_args.kwargs["params"]
        assert params["bundle_id"] == str(bundle_id)

    async def test_sends_get_with_include_inactive(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await sessions.list(include_inactive=True)

        # Assert
        params = http.request.call_args.kwargs["params"]
        assert params["include_inactive"] is True

    async def test_include_inactive_false_not_sent(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await sessions.list(include_inactive=False)

        # Assert
        params = http.request.call_args.kwargs["params"]
        assert "include_inactive" not in params

    async def test_returns_list_of_sessions(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "items": [
                _session_data(is_idle=False),
                _session_data(is_idle=True),
            ]
        }

        # Act
        result = await sessions.list()

        # Assert
        assert len(result) == 2
        assert all(isinstance(s, Session) for s in result)
        assert result[0].is_idle is False
        assert result[1].is_idle is True

    async def test_empty_list_returns_empty(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        result = await sessions.list()

        # Assert
        assert result == []


# ---------------------------------------------------------------------------
# get
# ---------------------------------------------------------------------------


class TestGet:
    """Tests for SessionsResource.get."""

    async def test_sends_get_with_session_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        http.request.return_value = _session_data()

        # Act
        await sessions.get(session_id)

        # Assert
        http.request.assert_awaited_once_with("GET", f"/api/v1/sessions/{session_id}")

    async def test_accepts_string_session_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = str(uuid4())
        http.request.return_value = _session_data()

        # Act
        await sessions.get(session_id)

        # Assert
        http.request.assert_awaited_once_with("GET", f"/api/v1/sessions/{session_id}")

    async def test_returns_session_model(self, sessions: SessionsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _session_data(is_active=False)

        # Act
        result = await sessions.get(uuid4())

        # Assert
        assert isinstance(result, Session)
        assert result.is_active is False


# ---------------------------------------------------------------------------
# update
# ---------------------------------------------------------------------------


class TestUpdate:
    """Tests for SessionsResource.update."""

    async def test_sends_patch_with_allowed_tool_names(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        tools = ["PREFIX__tool_x"]
        http.request.return_value = _session_data(allowed_tool_names=tools)

        # Act
        await sessions.update(session_id, allowed_tool_names=tools)

        # Assert
        http.request.assert_awaited_once_with(
            "PATCH",
            f"/api/v1/sessions/{session_id}",
            json={"allowed_tool_names": tools},
        )

    async def test_sends_patch_with_denied_tool_names(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        tools = ["PREFIX__dangerous_tool"]
        http.request.return_value = _session_data(denied_tool_names=tools)

        # Act
        await sessions.update(session_id, denied_tool_names=tools)

        # Assert
        http.request.assert_awaited_once_with(
            "PATCH",
            f"/api/v1/sessions/{session_id}",
            json={"denied_tool_names": tools},
        )

    async def test_sends_patch_with_both_fields(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        allowed = ["PREFIX__tool_a"]
        denied = ["PREFIX__tool_b"]
        http.request.return_value = _session_data(
            allowed_tool_names=allowed, denied_tool_names=denied
        )

        # Act
        await sessions.update(session_id, allowed_tool_names=allowed, denied_tool_names=denied)

        # Assert
        http.request.assert_awaited_once_with(
            "PATCH",
            f"/api/v1/sessions/{session_id}",
            json={"allowed_tool_names": allowed, "denied_tool_names": denied},
        )

    async def test_omitted_fields_not_sent(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange — call update without any tool-scope args
        session_id = uuid4()
        http.request.return_value = _session_data()

        # Act
        await sessions.update(session_id)

        # Assert — body should be empty (no keys sent)
        http.request.assert_awaited_once_with(
            "PATCH",
            f"/api/v1/sessions/{session_id}",
            json={},
        )

    async def test_sends_patch_with_null_to_clear(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        http.request.return_value = _session_data(allowed_tool_names=None)

        # Act
        await sessions.update(session_id, allowed_tool_names=None)

        # Assert
        http.request.assert_awaited_once_with(
            "PATCH",
            f"/api/v1/sessions/{session_id}",
            json={"allowed_tool_names": None},
        )

    async def test_sends_patch_with_null_denied_to_clear(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        http.request.return_value = _session_data(denied_tool_names=None)

        # Act
        await sessions.update(session_id, denied_tool_names=None)

        # Assert
        http.request.assert_awaited_once_with(
            "PATCH",
            f"/api/v1/sessions/{session_id}",
            json={"denied_tool_names": None},
        )

    async def test_returns_session_model(self, sessions: SessionsResource, http: AsyncMock) -> None:
        # Arrange
        tools = ["PREFIX__a", "PREFIX__b", "PREFIX__c"]
        http.request.return_value = _session_data(allowed_tool_names=tools)

        # Act
        result = await sessions.update(uuid4(), allowed_tool_names=tools)

        # Assert
        assert isinstance(result, Session)
        assert result.allowed_tool_names == tools


# ---------------------------------------------------------------------------
# delete
# ---------------------------------------------------------------------------


class TestDelete:
    """Tests for SessionsResource.delete."""

    async def test_sends_delete_with_session_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        http.request.return_value = None

        # Act
        await sessions.delete(session_id)

        # Assert
        http.request.assert_awaited_once_with(
            "DELETE", f"/api/v1/sessions/{session_id}", params=None
        )

    async def test_sends_delete_with_hard_delete(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        http.request.return_value = None

        # Act
        await sessions.delete(session_id, hard_delete=True)

        # Assert
        http.request.assert_awaited_once_with(
            "DELETE",
            f"/api/v1/sessions/{session_id}",
            params={"hard_delete": "true"},
        )

    async def test_returns_none(self, sessions: SessionsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = None

        # Act
        result = await sessions.delete(uuid4())

        # Assert
        assert result is None


# ---------------------------------------------------------------------------
# touch
# ---------------------------------------------------------------------------


class TestTouch:
    """Tests for SessionsResource.touch."""

    async def test_sends_post_with_session_id(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        session_id = uuid4()
        http.request.return_value = _session_data()

        # Act
        await sessions.touch(session_id)

        # Assert
        http.request.assert_awaited_once_with("POST", f"/api/v1/sessions/{session_id}/touch")

    async def test_returns_session_model(self, sessions: SessionsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = _session_data(is_active=True)

        # Act
        result = await sessions.touch(uuid4())

        # Assert
        assert isinstance(result, Session)
        assert result.is_active is True


# ---------------------------------------------------------------------------
# cleanup
# ---------------------------------------------------------------------------


class TestCleanup:
    """Tests for SessionsResource.cleanup."""

    async def test_sends_post_to_cleanup(self, sessions: SessionsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"cleaned_count": 5}

        # Act
        await sessions.cleanup()

        # Assert
        http.request.assert_awaited_once_with("POST", "/api/v1/sessions/cleanup")

    async def test_returns_dict_with_cleaned_count(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"cleaned_count": 3}

        # Act
        result = await sessions.cleanup()

        # Assert
        assert isinstance(result, dict)
        assert result["cleaned_count"] == 3


# ---------------------------------------------------------------------------
# stats
# ---------------------------------------------------------------------------


class TestStats:
    """Tests for SessionsResource.stats (global admin endpoint)."""

    async def test_sends_get_to_global_stats(
        self, sessions: SessionsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "total_active": 10,
            "server_sessions": 6,
            "bundle_sessions": 4,
            "expired_not_cleaned": 2,
            "idle_sessions": 1,
        }

        # Act
        await sessions.stats()

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/sessions/stats")

    async def test_returns_dict(self, sessions: SessionsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {
            "total_active": 10,
            "server_sessions": 6,
            "bundle_sessions": 4,
            "expired_not_cleaned": 2,
            "idle_sessions": 1,
        }

        # Act
        result = await sessions.stats()

        # Assert
        assert isinstance(result, dict)
        assert result["total_active"] == 10
        assert result["server_sessions"] == 6
