import json
import asyncio
from unittest.mock import patch, AsyncMock, MagicMock

from payfence.middleware import PayFenceMiddleware


def run(coro):
    return asyncio.get_event_loop().run_until_complete(coro)


def make_scope(path="/v1/weather", method="GET", token="tok_test_123", headers=None):
    hdrs = headers if headers is not None else [
        (b"authorization", f"Bearer {token}".encode()),
    ]
    return {"type": "http", "path": path, "method": method, "headers": hdrs}


def capture_response():
    """Returns (send_fn, messages_list) to capture ASGI send() calls."""
    messages = []

    async def send(msg):
        messages.append(msg)

    return send, messages


class TestPayFenceMiddleware:
    @patch("payfence.middleware.authorize")
    def test_allow_passes_to_app(self, mock_auth):
        mock_auth.return_value = {"decision": "allow", "units": 1}
        app = AsyncMock()

        mw = PayFenceMiddleware(app, api_key="sk_test")
        scope = make_scope()
        run(mw(scope, AsyncMock(), AsyncMock()))

        app.assert_called_once()
        assert scope["payfence"]["decision"] == "allow"

    @patch("payfence.middleware.authorize")
    def test_deny_returns_402(self, mock_auth):
        mock_auth.return_value = {"decision": "deny", "reason": "payment_required"}
        app = AsyncMock()

        mw = PayFenceMiddleware(app, api_key="sk_test")
        send, messages = capture_response()
        run(mw(make_scope(), AsyncMock(), send))

        app.assert_not_called()
        assert messages[0]["status"] == 402
        body = json.loads(messages[1]["body"])
        assert body["decision"] == "deny"

    @patch("payfence.middleware.authorize")
    def test_missing_token_returns_402(self, mock_auth):
        app = AsyncMock()
        mw = PayFenceMiddleware(app, api_key="sk_test")
        scope = make_scope(headers=[])  # no auth header
        send, messages = capture_response()

        run(mw(scope, AsyncMock(), send))

        mock_auth.assert_not_called()
        assert messages[0]["status"] == 402
        body = json.loads(messages[1]["body"])
        assert body["reason"] == "payment_required"

    @patch("payfence.middleware.authorize")
    def test_excluded_path_skips_auth(self, mock_auth):
        app = AsyncMock()
        mw = PayFenceMiddleware(app, api_key="sk_test", exclude_paths=["/health"])
        scope = make_scope(path="/health")

        run(mw(scope, AsyncMock(), AsyncMock()))

        mock_auth.assert_not_called()
        app.assert_called_once()

    @patch("payfence.middleware.authorize")
    def test_network_error_fails_open(self, mock_auth):
        mock_auth.side_effect = ConnectionError("ECONNREFUSED")
        app = AsyncMock()

        mw = PayFenceMiddleware(app, api_key="sk_test")
        run(mw(make_scope(), AsyncMock(), AsyncMock()))

        app.assert_called_once()

    @patch("payfence.middleware.authorize")
    def test_on_deny_callback(self, mock_auth):
        mock_auth.return_value = {"decision": "deny", "reason": "quota_exceeded"}
        app = AsyncMock()
        on_deny = AsyncMock()

        mw = PayFenceMiddleware(app, api_key="sk_test", on_deny=on_deny)
        run(mw(make_scope(), AsyncMock(), AsyncMock()))

        on_deny.assert_called_once()
        assert on_deny.call_args[0][3]["reason"] == "quota_exceeded"
        app.assert_not_called()

    def test_non_http_scope_passes_through(self):
        app = AsyncMock()
        mw = PayFenceMiddleware(app, api_key="sk_test")
        scope = {"type": "websocket", "path": "/ws"}

        run(mw(scope, AsyncMock(), AsyncMock()))

        app.assert_called_once()

    @patch("payfence.middleware.authorize")
    def test_custom_base_url_forwarded(self, mock_auth):
        mock_auth.return_value = {"decision": "allow"}
        app = AsyncMock()

        mw = PayFenceMiddleware(app, api_key="sk_test", base_url="https://custom.io")
        run(mw(make_scope(), AsyncMock(), AsyncMock()))

        _, kwargs = mock_auth.call_args
        assert kwargs["base_url"] == "https://custom.io"
