# 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 BasicAuthMixin — auth logic extracted from AuthMiddleware."""

import base64

import pytest

from genro_asgi.exceptions import HTTPException
from genro_asgi.middleware.authentication import AuthMiddleware
from genro_asgi.server.auth_mixin import BasicAuthMixin


class TestableAuthMixin(BasicAuthMixin):
    """Standalone testable auth mixin without server dependencies."""

    __slots__ = ("_auth_config",)


class FakeServer(BasicAuthMixin):
    """Simulates AsgiServer with BasicAuthMixin for integration tests."""

    __slots__ = ("_auth_config",)


class FakeDispatcher:
    """Simulates Dispatcher with a .server attribute. Callable as ASGI app."""

    __slots__ = ("server", "captured_scope")

    def __init__(self, server: FakeServer) -> None:
        self.server = server
        self.captured_scope: dict = {}

    async def __call__(self, scope, receive, send) -> None:
        self.captured_scope.update(scope)


class TestBasicAuthMixinConfiguration:
    """Tests for BasicAuthMixin credential configuration."""

    def test_configure_bearer_single_token(self) -> None:
        """Single bearer token is stored with O(1) lookup by token value."""
        mixin = TestableAuthMixin(
            bearer={"api_key": {"token": "tk_abc123", "tags": "read"}},
        )
        assert "tk_abc123" in mixin._auth_config["bearer"]
        entry = mixin._auth_config["bearer"]["tk_abc123"]
        assert entry["identity"] == "api_key"
        assert entry["tags"] == ["read"]

    def test_configure_bearer_multiple_tokens(self) -> None:
        """Multiple bearer tokens are all stored."""
        mixin = TestableAuthMixin(
            bearer={
                "api": {"token": "tk_api", "tags": "api"},
                "admin": {"token": "tk_admin", "tags": "admin,write"},
            },
        )
        assert "tk_api" in mixin._auth_config["bearer"]
        assert "tk_admin" in mixin._auth_config["bearer"]
        assert mixin._auth_config["bearer"]["tk_admin"]["tags"] == ["admin", "write"]

    def test_configure_bearer_missing_token_raises(self) -> None:
        """Bearer entry without 'token' value raises ValueError."""
        with pytest.raises(ValueError, match="missing 'token' value"):
            TestableAuthMixin(bearer={"bad": {"tags": "read"}})

    def test_configure_basic_single_user(self) -> None:
        """Single basic auth user stored as base64 key for O(1) lookup."""
        mixin = TestableAuthMixin(
            basic={"admin": {"password": "secret", "tags": "admin"}},
        )
        b64_key = base64.b64encode(b"admin:secret").decode()
        assert b64_key in mixin._auth_config["basic"]
        entry = mixin._auth_config["basic"][b64_key]
        assert entry["identity"] == "admin"
        assert entry["tags"] == ["admin"]

    def test_configure_basic_missing_password_raises(self) -> None:
        """Basic auth entry without 'password' raises ValueError."""
        with pytest.raises(ValueError, match="missing 'password'"):
            TestableAuthMixin(basic={"bad": {"tags": "read"}})

    def test_configure_unknown_type_ignored(self) -> None:
        """Unknown auth type is silently ignored."""
        mixin = TestableAuthMixin(custom={"foo": "bar"})
        assert len(mixin._auth_config["custom"]) == 0


class TestBasicAuthMixinAuthenticate:
    """Tests for BasicAuthMixin.authenticate() method."""

    def test_authenticate_bearer_valid(self) -> None:
        """Valid bearer token returns auth dict."""
        mixin = TestableAuthMixin(
            bearer={"api_key": {"token": "tk_test", "tags": "read"}},
        )
        scope = {"_headers": {"authorization": "Bearer tk_test"}}
        result = mixin.authenticate(scope)

        assert result is not None
        assert result["identity"] == "api_key"
        assert result["tags"] == ["read"]
        assert result["backend"] == "bearer"

    def test_authenticate_basic_valid(self) -> None:
        """Valid basic credentials return auth dict."""
        mixin = TestableAuthMixin(
            basic={"mrossi": {"password": "secret123", "tags": "read,write"}},
        )
        credentials = base64.b64encode(b"mrossi:secret123").decode()
        scope = {"_headers": {"authorization": f"Basic {credentials}"}}
        result = mixin.authenticate(scope)

        assert result is not None
        assert result["identity"] == "mrossi"
        assert result["tags"] == ["read", "write"]
        assert result["backend"] == "basic"

    def test_authenticate_no_header_returns_none(self) -> None:
        """No Authorization header returns None."""
        mixin = TestableAuthMixin(
            bearer={"api_key": {"token": "tk_test", "tags": "read"}},
        )
        scope = {"_headers": {}}
        result = mixin.authenticate(scope)

        assert result is None

    def test_authenticate_invalid_credentials_raises_401(self) -> None:
        """Invalid credentials raise HTTPException(401)."""
        mixin = TestableAuthMixin(
            bearer={"api_key": {"token": "tk_test", "tags": "read"}},
        )
        scope = {"_headers": {"authorization": "Bearer invalid_token"}}

        with pytest.raises(HTTPException) as exc_info:
            mixin.authenticate(scope)

        assert exc_info.value.status_code == 401

    def test_authenticate_unknown_scheme_raises_401(self) -> None:
        """Unknown auth scheme raises HTTPException(401)."""
        mixin = TestableAuthMixin(
            bearer={"api_key": {"token": "tk_test", "tags": "read"}},
        )
        scope = {"_headers": {"authorization": "Digest somecredentials"}}

        with pytest.raises(HTTPException) as exc_info:
            mixin.authenticate(scope)

        assert exc_info.value.status_code == 401


class TestBasicAuthMixinVerifyCredentials:
    """Tests for BasicAuthMixin.verify_credentials() method."""

    def test_verify_credentials_valid(self) -> None:
        """Valid username/password returns auth dict."""
        mixin = TestableAuthMixin(
            basic={"admin": {"password": "secret", "tags": "admin,write"}},
        )
        result = mixin.verify_credentials("admin", "secret")

        assert result is not None
        assert result["identity"] == "admin"
        assert result["tags"] == ["admin", "write"]

    def test_verify_credentials_invalid(self) -> None:
        """Wrong password returns None."""
        mixin = TestableAuthMixin(
            basic={"admin": {"password": "secret", "tags": "admin"}},
        )
        result = mixin.verify_credentials("admin", "wrong")

        assert result is None


class TestMiddlewareServerDelegation:
    """Tests for AuthMiddleware delegation to server.authenticate()."""

    @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_middleware_delegates_to_server(self) -> None:
        """AuthMiddleware delegates to server.authenticate() when server is in chain."""
        server = FakeServer(
            bearer={"api_key": {"token": "tk_server", "tags": "server_tag"}},
        )
        dispatcher = FakeDispatcher(server)
        middleware = AuthMiddleware(
            dispatcher,
            bearer={"api_key": {"token": "tk_middleware", "tags": "mw_tag"}},
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Bearer tk_server")],
        }
        await middleware(scope, None, None)

        assert dispatcher.captured_scope["auth"]["tags"] == ["server_tag"]
        assert dispatcher.captured_scope["auth"]["identity"] == "api_key"

    @pytest.mark.asyncio
    async def test_middleware_standalone_fallback(self, dummy_app, captured_scope) -> None:
        """AuthMiddleware uses internal _authenticate() when no server in chain."""
        middleware = AuthMiddleware(
            dummy_app,
            bearer={"api_key": {"token": "tk_standalone", "tags": "standalone_tag"}},
        )
        scope = {
            "type": "http",
            "headers": [(b"authorization", b"Bearer tk_standalone")],
        }
        await middleware(scope, None, None)

        assert captured_scope["auth"]["tags"] == ["standalone_tag"]
        assert captured_scope["auth"]["identity"] == "api_key"
