# Copyright 2025 Softwell S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for AuthMiddleware."""

import base64

import pytest

from genro_asgi.exceptions import HTTPException
from genro_asgi.middleware.authentication import AuthMiddleware


class TestAuthMiddleware:
    """Tests for AuthMiddleware."""

    @pytest.fixture
    def captured_scope(self) -> dict:
        """Fixture to capture scope passed to app."""
        return {}

    @pytest.fixture
    def dummy_app(self, captured_scope: dict):
        """Dummy ASGI app that captures scope."""

        async def app(scope, receive, send):
            captured_scope.update(scope)

        return app

    @pytest.mark.asyncio
    async def test_bearer_auth(self, dummy_app, captured_scope) -> None:
        """Middleware authenticates bearer token."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Bearer tk_test")],
        }
        await middleware(scope, None, None)

        assert "auth" in captured_scope
        assert captured_scope["auth"]["identity"] == "test_token"
        assert captured_scope["auth"]["tags"] == ["read"]
        assert captured_scope["auth"]["backend"] == "bearer"

    @pytest.mark.asyncio
    async def test_basic_auth(self, dummy_app, captured_scope) -> None:
        """Middleware authenticates basic credentials."""
        credentials = base64.b64encode(b"mrossi:secret123").decode()
        middleware = AuthMiddleware(
            dummy_app,
            basic={
                "mrossi": {"password": "secret123", "tags": "read,write"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", f"Basic {credentials}".encode())],
        }
        await middleware(scope, None, None)

        assert "auth" in captured_scope
        assert captured_scope["auth"]["identity"] == "mrossi"
        assert captured_scope["auth"]["tags"] == ["read", "write"]
        assert captured_scope["auth"]["backend"] == "basic"

    @pytest.mark.asyncio
    async def test_no_auth_header(self, dummy_app, captured_scope) -> None:
        """Middleware sets auth to None when no header present."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {"type": "http", "headers": []}
        await middleware(scope, None, None)

        assert "auth" in captured_scope
        assert captured_scope["auth"] is None

    @pytest.mark.asyncio
    async def test_invalid_token(self, dummy_app, captured_scope) -> None:
        """Middleware raises 401 when token is invalid."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Bearer invalid_token")],
        }
        with pytest.raises(HTTPException) as exc_info:
            await middleware(scope, None, None)

        assert exc_info.value.status_code == 401
        assert "Invalid or expired credentials" in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_multiple_entries(self, dummy_app, captured_scope) -> None:
        """Middleware tries all entries until one matches."""
        credentials = base64.b64encode(b"admin:supersecret").decode()
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "api_token": {"token": "tk_api", "tags": "api"},
            },
            basic={
                "admin": {"password": "supersecret", "tags": "admin"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", f"Basic {credentials}".encode())],
        }
        await middleware(scope, None, None)

        assert captured_scope["auth"]["identity"] == "admin"
        assert captured_scope["auth"]["tags"] == ["admin"]
        assert captured_scope["auth"]["backend"] == "basic"

    @pytest.mark.asyncio
    async def test_bearer_over_basic(self, dummy_app, captured_scope) -> None:
        """Bearer token is matched when header uses Bearer scheme."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "api_token": {"token": "tk_api", "tags": "api"},
            },
            basic={
                "admin": {"password": "supersecret", "tags": "admin"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Bearer tk_api")],
        }
        await middleware(scope, None, None)

        assert captured_scope["auth"]["identity"] == "api_token"
        assert captured_scope["auth"]["backend"] == "bearer"

    @pytest.mark.asyncio
    async def test_non_http_passthrough(self, dummy_app, captured_scope) -> None:
        """Non-HTTP/non-WebSocket scopes pass through without auth."""
        middleware = AuthMiddleware(
            dummy_app,
            tokens={
                "type": "bearer",
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {"type": "lifespan", "headers": []}
        await middleware(scope, None, None)

        assert "auth" not in captured_scope

    @pytest.mark.asyncio
    async def test_websocket_auth_bearer(self, dummy_app, captured_scope) -> None:
        """WebSocket scope with valid bearer token sets scope['auth']."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "ws_token": {"token": "tk_ws", "tags": "realtime"},
            },
        )
        scope = {
            "type": "websocket",
            "headers": [(b"authorization", b"Bearer tk_ws")],
        }
        await middleware(scope, None, None)

        assert captured_scope["auth"]["identity"] == "ws_token"
        assert captured_scope["auth"]["tags"] == ["realtime"]
        assert captured_scope["_filters"]["auth_tags"] == ["realtime"]

    @pytest.mark.asyncio
    async def test_websocket_auth_no_header(self, dummy_app, captured_scope) -> None:
        """WebSocket scope without auth header sets scope['auth'] to None."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "ws_token": {"token": "tk_ws", "tags": "realtime"},
            },
        )
        scope = {"type": "websocket", "headers": []}
        await middleware(scope, None, None)

        assert captured_scope["auth"] is None
        assert captured_scope["_filters"]["auth_tags"] == []

    @pytest.mark.asyncio
    async def test_websocket_auth_invalid(self, dummy_app, captured_scope) -> None:
        """WebSocket scope with invalid token raises 401."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "ws_token": {"token": "tk_ws", "tags": "realtime"},
            },
        )
        scope = {
            "type": "websocket",
            "headers": [(b"authorization", b"Bearer invalid_token")],
        }
        with pytest.raises(HTTPException) as exc_info:
            await middleware(scope, None, None)

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_no_entries(self, dummy_app, captured_scope) -> None:
        """Middleware with no entries raises 401 when credentials provided."""
        middleware = AuthMiddleware(dummy_app)
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Bearer tk_test")],
        }
        with pytest.raises(HTTPException) as exc_info:
            await middleware(scope, None, None)

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_unknown_scheme(self, dummy_app, captured_scope) -> None:
        """Unknown auth scheme raises 401."""
        middleware = AuthMiddleware(
            dummy_app,
            tokens={
                "type": "bearer",
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Digest somecredentials")],
        }
        with pytest.raises(HTTPException) as exc_info:
            await middleware(scope, None, None)

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_auth_tags_set_on_bearer_auth(self, dummy_app, captured_scope) -> None:
        """scope["auth_tags"] contains tags after bearer authentication."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Bearer tk_test")],
        }
        await middleware(scope, None, None)

        assert captured_scope["_filters"]["auth_tags"] == ["read"]

    @pytest.mark.asyncio
    async def test_auth_tags_set_on_basic_auth(self, dummy_app, captured_scope) -> None:
        """scope["auth_tags"] contains tags after basic authentication."""
        credentials = base64.b64encode(b"mrossi:secret123").decode()
        middleware = AuthMiddleware(
            dummy_app,
            basic={
                "mrossi": {"password": "secret123", "tags": "read,write"},
            },
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", f"Basic {credentials}".encode())],
        }
        await middleware(scope, None, None)

        assert captured_scope["_filters"]["auth_tags"] == ["read", "write"]

    @pytest.mark.asyncio
    async def test_auth_tags_empty_when_no_header(self, dummy_app, captured_scope) -> None:
        """scope["auth_tags"] is empty list when no auth header."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {"type": "http", "headers": []}
        await middleware(scope, None, None)

        assert captured_scope["_filters"]["auth_tags"] == []

    @pytest.mark.asyncio
    async def test_auth_tags_not_set_for_lifespan(self, dummy_app, captured_scope) -> None:
        """scope["auth_tags"] not set for non-HTTP/non-WebSocket scopes."""
        middleware = AuthMiddleware(
            dummy_app,
            tokens={
                "type": "bearer",
                "test_token": {"token": "tk_test", "tags": "read"},
            },
        )
        scope = {"type": "lifespan", "headers": []}
        await middleware(scope, None, None)

        assert "_filters" not in captured_scope
