# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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
#
#   http://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.
from __future__ import annotations

import json
import os
from unittest import mock
from urllib.parse import urlencode

import pytest

from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
from airflow.api_fastapi.auth.managers.models.resource_details import (
    AccessView,
    ConnectionDetails,
    DagDetails,
    PoolDetails,
    TeamDetails,
    VariableDetails,
)
from airflow.api_fastapi.auth.managers.simple.simple_auth_manager import SimpleAuthManager
from airflow.api_fastapi.auth.managers.simple.user import SimpleAuthManagerUser
from airflow.api_fastapi.common.types import MenuItem

from tests_common.test_utils.config import conf_vars


class TestSimpleAuthManager:
    @conf_vars(
        {
            ("core", "multi_team"): "true",
            ("core", "simple_auth_manager_users"): "test1:viewer,test2:viewer,test3:viewer:test|marketing",
        }
    )
    def test_get_users(self, auth_manager):
        users = auth_manager.get_users()
        assert users == [
            SimpleAuthManagerUser(username="test1", role="viewer", teams=None),
            SimpleAuthManagerUser(username="test2", role="viewer", teams=None),
            SimpleAuthManagerUser(username="test3", role="viewer", teams=["test", "marketing"]),
        ]

    @pytest.mark.parametrize(
        ("file_content", "expected"),
        [
            ("{}", {}),
            ("", {}),
            ('{"test1": "test1"}', {"test1": "test1"}),
            ('{"test1": "test1", "test2": "test2"}', {"test1": "test1", "test2": "test2"}),
        ],
    )
    def test_get_passwords(self, auth_manager, file_content, expected):
        with conf_vars(
            {
                ("core", "simple_auth_manager_users"): "test1:viewer,test2:viewer",
            }
        ):
            with open(auth_manager.get_generated_password_file(), "w") as file:
                file.write(file_content)
            passwords = auth_manager.get_passwords()
            assert passwords == expected

    def test_init_with_default_user(self, auth_manager):
        auth_manager.init()
        with open(auth_manager.get_generated_password_file()) as file:
            passwords_str = file.read().strip()
            user_passwords_from_file = json.loads(passwords_str)

            assert len(user_passwords_from_file) == 1

    def test_init_with_users(self, auth_manager):
        with conf_vars(
            {
                ("core", "simple_auth_manager_users"): "test1:viewer,test2:viewer",
            }
        ):
            auth_manager.init()
            with open(auth_manager.get_generated_password_file()) as file:
                passwords_str = file.read().strip()
                user_passwords_from_file = json.loads(passwords_str)

                assert len(user_passwords_from_file) == 2

    @pytest.mark.parametrize(
        ("file_content", "expected"),
        [
            ({"test1": "test1"}, {"test1": "test1"}),
            ({"test2": "test2", "test3": "test3"}, {"test1": mock.ANY, "test2": "test2", "test3": "test3"}),
        ],
    )
    def test_init_with_users_with_password(self, auth_manager, file_content, expected):
        with conf_vars(
            {
                ("core", "simple_auth_manager_users"): "test1:viewer",
            }
        ):
            with open(auth_manager.get_generated_password_file(), "w") as file:
                file.write(json.dumps(file_content) + "\n")
            auth_manager.init()
            with open(auth_manager.get_generated_password_file()) as file:
                passwords_str = file.read().strip()
                user_passwords_from_file = json.loads(passwords_str)

                assert user_passwords_from_file == expected

    def test_init_with_all_admins(self, auth_manager):
        with conf_vars({("core", "simple_auth_manager_all_admins"): "true"}):
            auth_manager.init()
            assert not os.path.exists(auth_manager.get_generated_password_file())

    @pytest.mark.parametrize(
        ("kwargs", "expected"),
        [
            pytest.param(
                {
                    "sql_conn": "sqlite:////tmp/airflow.db",
                    "api_host": "localhost",
                    "executor": "LocalExecutor",
                },
                False,
                id="all-dev",
            ),
            pytest.param(
                {
                    "sql_conn": "postgresql+psycopg2://airflow@db/airflow",
                    "api_host": "localhost",
                    "executor": "LocalExecutor",
                },
                True,
                id="postgres-backend-is-prod-shape",
            ),
            pytest.param(
                {
                    "sql_conn": "mysql://airflow@db/airflow",
                    "api_host": "localhost",
                    "executor": "LocalExecutor",
                },
                True,
                id="mysql-backend-is-prod-shape",
            ),
            pytest.param(
                {
                    "sql_conn": "sqlite:////tmp/airflow.db",
                    "api_host": "0.0.0.0",
                    "executor": "LocalExecutor",
                },
                True,
                id="non-local-bind-is-prod-shape",
            ),
            pytest.param(
                {
                    "sql_conn": "sqlite:////tmp/airflow.db",
                    "api_host": "127.0.0.1",
                    "executor": "LocalExecutor",
                },
                False,
                id="loopback-bind-is-dev",
            ),
            pytest.param(
                {
                    "sql_conn": "sqlite:////tmp/airflow.db",
                    "api_host": "localhost",
                    "executor": "CeleryExecutor",
                },
                True,
                id="celery-executor-is-prod-shape",
            ),
            pytest.param(
                {
                    "sql_conn": "sqlite:////tmp/airflow.db",
                    "api_host": "localhost",
                    "executor": "airflow.executors.local_executor.LocalExecutor",
                },
                False,
                id="fully-qualified-local-executor-is-dev",
            ),
            pytest.param(
                {
                    "sql_conn": "sqlite:////tmp/airflow.db",
                    "api_host": "  localhost  ",
                    "executor": "LocalExecutor",
                },
                False,
                id="api-host-is-stripped",
            ),
        ],
    )
    def test_looks_like_production(self, kwargs, expected):
        # The function takes each axis as a keyword argument so the test fully
        # controls the inputs and does not rely on conf state. Any axis left
        # as None would fall through to conf.get(); we pass all three explicitly.
        assert SimpleAuthManager._looks_like_production(**kwargs) is expected

    @mock.patch.object(SimpleAuthManager, "_looks_like_production", return_value=True)
    @mock.patch("airflow.api_fastapi.auth.managers.simple.simple_auth_manager.log")
    def test_init_warns_when_production_shaped(self, mock_log, mock_check, auth_manager):
        """SimpleAuthManager.init() emits a loud warning in a production-shaped deployment."""
        with conf_vars({("core", "simple_auth_manager_users"): "alice:admin"}):
            auth_manager.init()
        mock_check.assert_called_once_with()
        mock_log.warning.assert_called_once()

    @mock.patch.object(SimpleAuthManager, "_looks_like_production", return_value=False)
    @mock.patch("airflow.api_fastapi.auth.managers.simple.simple_auth_manager.log")
    def test_init_does_not_warn_for_dev_shape(self, mock_log, mock_check, auth_manager):
        """No production warning when the deployment shape looks like local dev."""
        with conf_vars({("core", "simple_auth_manager_users"): "alice:admin"}):
            auth_manager.init()
        mock_check.assert_called_once_with()
        mock_log.warning.assert_not_called()

    def test_get_url_login(self, auth_manager):
        result = auth_manager.get_url_login()
        assert result == AUTH_MANAGER_FASTAPI_APP_PREFIX + "/login"

    def test_get_url_login_with_all_admins(self, auth_manager):
        with conf_vars({("core", "simple_auth_manager_all_admins"): "true"}):
            result = auth_manager.get_url_login()
            assert result == AUTH_MANAGER_FASTAPI_APP_PREFIX + "/token/login"

    def test_get_url_login_with_next_url(self, auth_manager):
        next_url = "http://localhost:8080/dags/example_dag/runs/manual__2026-05-20/tasks/example_task"
        result = auth_manager.get_url_login(next_url=next_url)
        assert result == f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login?{urlencode({'next': next_url})}"

    def test_get_url_login_with_next_url_all_admins(self, auth_manager):
        with conf_vars({("core", "simple_auth_manager_all_admins"): "true"}):
            next_url = "http://localhost:8080/dags/example_dag/runs/manual__2026-05-20/tasks/example_task"
            result = auth_manager.get_url_login(next_url=next_url)
            assert result == f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/token/login?{urlencode({'next': next_url})}"

    def test_deserialize_user(self, auth_manager):
        result = auth_manager.deserialize_user({"sub": "test", "role": "admin"})
        assert result.username == "test"
        assert result.role == "admin"
        assert result.teams == []

    def test_serialize_user(self, auth_manager):
        user = SimpleAuthManagerUser(username="test", role="admin")
        result = auth_manager.serialize_user(user)
        assert result == {"sub": "test", "role": "admin", "teams": []}

    @pytest.mark.parametrize(
        "api",
        [
            "is_authorized_configuration",
            "is_authorized_connection",
            "is_authorized_dag",
            "is_authorized_asset",
            "is_authorized_asset_alias",
            "is_authorized_pool",
            "is_authorized_variable",
        ],
    )
    @pytest.mark.parametrize(
        ("role", "method", "result"),
        [
            ("ADMIN", "GET", True),
            ("ADMIN", "DELETE", True),
            ("VIEWER", "POST", False),
            ("VIEWER", "PUT", False),
            ("VIEWER", "DELETE", False),
        ],
    )
    def test_is_authorized_methods(self, auth_manager, api, role, method, result):
        assert (
            getattr(auth_manager, api)(method=method, user=SimpleAuthManagerUser(username="test", role=role))
            is result
        )

    @pytest.mark.parametrize(
        ("api", "details"),
        [
            ("is_authorized_connection", ConnectionDetails(team_name="test")),
            ("is_authorized_connection", None),
            ("is_authorized_dag", DagDetails(team_name="test")),
            ("is_authorized_dag", None),
            ("is_authorized_pool", PoolDetails(team_name="test")),
            ("is_authorized_pool", None),
            ("is_authorized_variable", VariableDetails(team_name="test")),
            ("is_authorized_variable", None),
        ],
    )
    @pytest.mark.parametrize(
        ("role", "method", "user_teams", "result"),
        [
            ("ADMIN", "GET", ["test", "marketing"], True),
            ("ADMIN", "GET", [], True),
            ("OP", "GET", [], False),
            ("OP", "GET", ["marketing"], False),
            ("OP", "GET", ["test"], True),
            ("OP", "GET", ["test", "marketing"], True),
        ],
    )
    def test_is_authorized_methods_with_teams(
        self, auth_manager, api, details, role, method, user_teams, result
    ):
        assert (
            getattr(auth_manager, api)(
                method=method,
                user=SimpleAuthManagerUser(username="test", role=role, teams=user_teams),
                details=details,
            )
            is result
            or not details
        )

    @pytest.mark.parametrize(
        ("api", "kwargs"),
        [
            ("is_authorized_view", {"access_view": AccessView.CLUSTER_ACTIVITY}),
            (
                "is_authorized_custom_view",
                {
                    "method": "GET",
                    "resource_name": "test",
                },
            ),
        ],
    )
    @pytest.mark.parametrize(
        ("role", "result"),
        [
            ("ADMIN", True),
            ("VIEWER", True),
            ("USER", True),
            ("OP", True),
        ],
    )
    def test_is_authorized_view_methods(self, auth_manager, api, kwargs, role, result):
        assert (
            getattr(auth_manager, api)(**kwargs, user=SimpleAuthManagerUser(username="test", role=role))
            is result
        )

    @pytest.mark.parametrize(
        "api",
        [
            "is_authorized_configuration",
            "is_authorized_connection",
            "is_authorized_asset",
            "is_authorized_asset_alias",
            "is_authorized_pool",
            "is_authorized_variable",
        ],
    )
    @pytest.mark.parametrize(
        ("role", "method", "result"),
        [
            ("ADMIN", "GET", True),
            ("OP", "DELETE", True),
            ("USER", "DELETE", False),
            ("VIEWER", "PUT", False),
        ],
    )
    def test_is_authorized_methods_op_role_required(self, auth_manager, api, role, method, result):
        assert (
            getattr(auth_manager, api)(method=method, user=SimpleAuthManagerUser(username="test", role=role))
            is result
        )

    @pytest.mark.parametrize(
        "api",
        ["is_authorized_dag"],
    )
    @pytest.mark.parametrize(
        ("role", "method", "result"),
        [
            ("ADMIN", "GET", True),
            ("OP", "DELETE", True),
            ("USER", "GET", True),
            ("USER", "DELETE", True),
            ("VIEWER", "PUT", False),
        ],
    )
    def test_is_authorized_methods_user_role_required(self, auth_manager, api, role, method, result):
        assert (
            getattr(auth_manager, api)(method=method, user=SimpleAuthManagerUser(username="test", role=role))
            is result
        )

    @pytest.mark.parametrize(
        "api",
        [
            "is_authorized_dag",
            "is_authorized_asset",
            "is_authorized_asset_alias",
            "is_authorized_pool",
        ],
    )
    @pytest.mark.parametrize(
        ("role", "method", "result"),
        [
            ("ADMIN", "GET", True),
            ("VIEWER", "GET", True),
            ("OP", "GET", True),
            ("USER", "GET", True),
            ("VIEWER", "POST", False),
        ],
    )
    def test_is_authorized_methods_viewer_role_required_for_get(
        self, auth_manager, api, role, method, result
    ):
        assert (
            getattr(auth_manager, api)(method=method, user=SimpleAuthManagerUser(username="test", role=role))
            is result
        )

    @pytest.mark.parametrize(
        ("user_teams", "team", "role", "expected"),
        [
            (None, None, None, False),
            (["test"], "marketing", None, False),
            (["test"], "test", None, True),
            (["test", "marketing"], "test", None, True),
            # Admin role can access all teams regardless of user.teams
            ([], "team_a", "ADMIN", True),
            (None, "team_b", "ADMIN", True),
        ],
    )
    def test_is_authorized_team(self, auth_manager, user_teams, team, role, expected):
        result = auth_manager.is_authorized_team(
            method="GET",
            user=SimpleAuthManagerUser(username="test", role=role, teams=user_teams),
            details=TeamDetails(name=team),
        )
        assert expected is result

    def test_filter_authorized_menu_items(self, auth_manager):
        items = [MenuItem.ASSETS]
        results = auth_manager.filter_authorized_menu_items(
            items, user=SimpleAuthManagerUser(username="test", role=None)
        )
        assert results == items

    @pytest.mark.parametrize(
        ("all_admins", "user_id", "assigned_users", "expected"),
        [
            # When simple_auth_manager_all_admins=True, any user should be allowed
            (True, "user1", {"user2"}, True),
            (True, "user2", {"user2"}, True),
            (True, "admin", {"test_user"}, True),
            # When simple_auth_manager_all_admins=False, user must be in assigned_users
            (False, "user1", {"user1"}, True),
            (False, "user2", {"user1"}, False),
            (False, "admin", {"test_user"}, False),
            # When no assigned_users, allow access
            (False, "user1", set(), True),
        ],
    )
    def test_is_authorized_hitl_task(self, auth_manager, all_admins, user_id, assigned_users, expected):
        """Test is_authorized_hitl_task method with different configurations."""
        with conf_vars({("core", "simple_auth_manager_all_admins"): str(all_admins)}):
            user = SimpleAuthManagerUser(username=user_id, role="user")
            result = auth_manager.is_authorized_hitl_task(assigned_users=assigned_users, user=user)
            assert result == expected

    @conf_vars(
        {
            ("core", "multi_team"): "true",
            (
                "core",
                "simple_auth_manager_users",
            ): "test1:viewer,test2:viewer:test,test3:viewer:test|marketing",
        }
    )
    def test_get_teams(self, auth_manager):
        teams = auth_manager._get_teams()
        assert teams == {"test", "marketing"}

    @conf_vars({("core", "simple_auth_manager_all_admins"): "false"})
    def test_get_fastapi_middlewares_disabled(self, auth_manager):
        assert auth_manager.get_fastapi_middlewares() == []

    @conf_vars({("core", "simple_auth_manager_all_admins"): "true"})
    def test_get_fastapi_middlewares_enabled(self, auth_manager):
        from airflow.api_fastapi.auth.managers.simple.middleware import SimpleAllAdminMiddleware

        assert auth_manager.get_fastapi_middlewares() == [(SimpleAllAdminMiddleware, {})]

    def test_generate_password_uses_expected_alphabet_and_length(self):
        alphabet = set("abcdefghkmnpqrstuvwxyzABCDEFGHKMNPQRSTUVWXYZ23456789")
        for _ in range(50):
            password = SimpleAuthManager._generate_password()
            assert len(password) == 16
            assert set(password).issubset(alphabet)
