"""Tests for FlashMessage and FlashMessageMiddleware."""

from unittest.mock import AsyncMock, Mock

import pytest
from starlette.requests import Request
from starlette.responses import Response
from starlette.testclient import TestClient

from fastpluggy.core.flash import FlashMessage, FlashMessageMiddleware


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

def _make_request(flash_messages=None, session=None):
    """Create a mock Request with configurable flash/session state."""
    request = Mock(spec=Request)
    request.state = Mock()
    if flash_messages is not None:
        request.state.flash_messages = flash_messages
    else:
        # Simulate no flash_messages attribute yet
        del request.state.flash_messages
    request.session = session if session is not None else {}
    return request


# ---------------------------------------------------------------------------
# FlashMessage.add()
# ---------------------------------------------------------------------------

class TestFlashMessageAdd:
    def test_add_appends_to_request_state(self):
        request = _make_request(flash_messages=[])
        FlashMessage.add(request, "Hello")
        assert len(request.state.flash_messages) == 1
        assert request.state.flash_messages[0]["message"] == "Hello"
        assert request.state.flash_messages[0]["category"] == "info"

    def test_add_initializes_list_when_missing(self):
        request = _make_request()  # no flash_messages attr
        FlashMessage.add(request, "Created", "success")
        assert request.state.flash_messages[0]["message"] == "Created"
        assert request.state.flash_messages[0]["category"] == "success"

    def test_add_returns_flash_instance(self):
        request = _make_request(flash_messages=[])
        result = FlashMessage.add(request, "msg", "warning")
        assert isinstance(result, FlashMessage)
        assert result.message == "msg"
        assert result.category == "warning"

    def test_add_with_exception(self):
        request = _make_request(flash_messages=[])
        exc = ValueError("bad value")
        FlashMessage.add(request, "Error occurred", "error", exception=exc)
        stored = request.state.flash_messages[0]
        assert stored["message"] == "Error occurred"
        assert stored["category"] == "error"
        assert stored["exception"] is not None


# ---------------------------------------------------------------------------
# FlashMessage.get_flash_messages()
# ---------------------------------------------------------------------------

class TestGetFlashMessages:
    def test_returns_messages_and_clears(self):
        msgs = [{"message": "hi", "category": "info", "exception": None}]
        request = _make_request(flash_messages=list(msgs), session={"flash_messages": list(msgs)})
        result = FlashMessage.get_flash_messages(request)
        assert result == msgs
        # State and session should be cleared
        assert request.state.flash_messages == []
        assert "flash_messages" not in request.session

    def test_returns_empty_when_no_attr(self):
        request = _make_request()  # no flash_messages attr
        request.session = {}
        result = FlashMessage.get_flash_messages(request)
        assert result == []

    def test_clears_session_key(self):
        request = _make_request(flash_messages=[], session={"flash_messages": [{"message": "old"}]})
        FlashMessage.get_flash_messages(request)
        assert "flash_messages" not in request.session


# ---------------------------------------------------------------------------
# FlashMessageMiddleware (pure ASGI)
# ---------------------------------------------------------------------------

class TestFlashMessageMiddleware:

    def _build_app(self, session, inner_checks=None):
        """Build a minimal ASGI app with session middleware + FlashMessageMiddleware."""
        from starlette.middleware.sessions import SessionMiddleware

        async def inner_app(scope, receive, send):
            request = Request(scope)
            if inner_checks:
                inner_checks(request)
            response = Response(status_code=200)
            await response(scope, receive, send)

        flash_mw = FlashMessageMiddleware(app=inner_app)
        # Use SessionMiddleware to provide a real session
        app = SessionMiddleware(flash_mw, secret_key="test-secret")

        # We'll inject session data via a wrapper that pre-seeds scope
        original_app = app

        async def seeded_app(scope, receive, send):
            # For simplicity, we test without pre-seeding session — use inner_checks
            await original_app(scope, receive, send)

        return seeded_app, session

    @pytest.mark.anyio
    async def test_dispatch_initializes_from_session(self):
        """Middleware loads flash_messages from session into request.state."""
        session_msgs = [{"message": "from session", "category": "info", "exception": None}]
        captured = {}

        # Test with mock objects directly via __call__
        session = {"flash_messages": list(session_msgs)}

        async def inner_app(scope, receive, send):
            request = Request(scope)
            # Middleware should have initialized flash_messages
            captured["flash"] = request.state.flash_messages
            response = Response(status_code=200)
            await response(scope, receive, send)

        middleware = FlashMessageMiddleware(app=inner_app)
        scope = {"type": "http", "method": "GET", "path": "/", "headers": [], "query_string": b"", "session": session}

        async def receive():
            return {"type": "http.request", "body": b""}

        sent = []
        async def send(msg):
            sent.append(msg)

        await middleware(scope, receive, send)
        assert captured["flash"] == session_msgs

    @pytest.mark.anyio
    async def test_dispatch_persists_new_messages_to_session(self):
        """Messages added during request get saved to session."""
        session = {}

        async def inner_app(scope, receive, send):
            request = Request(scope)
            # Simulate a route adding a flash message
            request.state.flash_messages = [{"message": "new", "category": "success", "exception": None}]
            response = Response(status_code=200)
            await response(scope, receive, send)

        middleware = FlashMessageMiddleware(app=inner_app)
        scope = {"type": "http", "method": "GET", "path": "/", "headers": [], "query_string": b"", "session": session}

        async def receive():
            return {"type": "http.request", "body": b""}

        sent = []
        async def send(msg):
            sent.append(msg)

        await middleware(scope, receive, send)
        assert session["flash_messages"] == [{"message": "new", "category": "success", "exception": None}]

    @pytest.mark.anyio
    async def test_dispatch_no_messages_leaves_session_clean(self):
        """When no messages are added, session is not modified."""
        session = {}

        async def inner_app(scope, receive, send):
            response = Response(status_code=200)
            await response(scope, receive, send)

        middleware = FlashMessageMiddleware(app=inner_app)
        scope = {"type": "http", "method": "GET", "path": "/", "headers": [], "query_string": b"", "session": session}

        async def receive():
            return {"type": "http.request", "body": b""}

        sent = []
        async def send(msg):
            sent.append(msg)

        await middleware(scope, receive, send)
        assert "flash_messages" not in session


# ---------------------------------------------------------------------------
# Message categories
# ---------------------------------------------------------------------------

class TestCategories:
    @pytest.mark.parametrize("category", ["info", "success", "warning", "error"])
    def test_category_preserved(self, category):
        request = _make_request(flash_messages=[])
        FlashMessage.add(request, "test", category)
        assert request.state.flash_messages[0]["category"] == category

    def test_default_category_is_info(self):
        request = _make_request(flash_messages=[])
        FlashMessage.add(request, "test")
        assert request.state.flash_messages[0]["category"] == "info"


# ---------------------------------------------------------------------------
# Multiple messages in a single request
# ---------------------------------------------------------------------------

class TestMultipleMessages:
    def test_multiple_add_calls(self):
        request = _make_request(flash_messages=[])
        FlashMessage.add(request, "first", "info")
        FlashMessage.add(request, "second", "success")
        FlashMessage.add(request, "third", "error")
        assert len(request.state.flash_messages) == 3
        assert [m["message"] for m in request.state.flash_messages] == ["first", "second", "third"]

    def test_get_returns_all_then_clears(self):
        msgs = [
            {"message": "a", "category": "info", "exception": None},
            {"message": "b", "category": "warning", "exception": None},
        ]
        request = _make_request(flash_messages=list(msgs), session={"flash_messages": list(msgs)})
        result = FlashMessage.get_flash_messages(request)
        assert len(result) == 2
        assert request.state.flash_messages == []


# ---------------------------------------------------------------------------
# to_dict() serialization
# ---------------------------------------------------------------------------

class TestToDict:
    def test_basic_serialization(self):
        flash = FlashMessage("hello", "success")
        d = flash.to_dict()
        assert d == {"message": "hello", "category": "success", "exception": None}

    def test_exception_serialization(self):
        exc = ValueError("bad")
        exc.detail = "extra info"
        flash = FlashMessage("error", "error", exception=exc)
        d = flash.to_dict()
        assert d["exception"] is not None
        assert "detail" in d["exception"]
        assert d["exception"]["detail"] == "extra info"

    def test_non_exception_value_ignored(self):
        """If exception is not an Exception instance, to_dict returns None."""
        flash = FlashMessage("msg", "info", exception="not an exception")
        d = flash.to_dict()
        assert d["exception"] is None

    def test_exception_with_no_extra_attrs(self):
        """Exception with no custom attributes serializes to empty-ish dict."""
        exc = RuntimeError("boom")
        flash = FlashMessage("msg", "error", exception=exc)
        d = flash.to_dict()
        assert isinstance(d["exception"], dict)


# ---------------------------------------------------------------------------
# Edge case: session not initialized / empty
# ---------------------------------------------------------------------------

class TestEdgeCases:
    @pytest.mark.anyio
    async def test_middleware_with_empty_session(self):
        """Middleware handles empty session gracefully."""
        captured = {}

        async def inner_app(scope, receive, send):
            request = Request(scope)
            captured["flash"] = request.state.flash_messages
            response = Response(status_code=200)
            await response(scope, receive, send)

        middleware = FlashMessageMiddleware(app=inner_app)
        scope = {"type": "http", "method": "GET", "path": "/", "headers": [], "query_string": b"", "session": {}}

        async def receive():
            return {"type": "http.request", "body": b""}

        sent = []
        async def send(msg):
            sent.append(msg)

        await middleware(scope, receive, send)
        assert captured["flash"] == []

    def test_get_flash_messages_when_session_has_no_key(self):
        request = _make_request(flash_messages=[], session={})
        result = FlashMessage.get_flash_messages(request)
        assert result == []
        assert "flash_messages" not in request.session

    def test_add_when_state_has_no_flash_messages(self):
        """add() initializes list when request.state has no flash_messages."""
        request = _make_request()  # no flash_messages attr
        flash = FlashMessage.add(request, "init", "info")
        assert flash.message == "init"
        assert len(request.state.flash_messages) == 1
