"""Integration tests for the local login gate + autologin (#1589).

Covers the four contracts described on the GitHub issue:
1. POST /auth/login rejects local credentials when local_login_enabled=false
   AND the BAMBUDDY_LOCAL_LOGIN env var is not set.
2. The BAMBUDDY_LOCAL_LOGIN=true env var bypasses the gate (recovery path).
3. POST /auth/forgot-password is gated by the same flag (with the same bypass).
4. GET /auth/advanced-auth/status surfaces both new fields so the LoginPage
   can render the right UI in a single query.
"""

from __future__ import annotations

from unittest.mock import patch

import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from backend.app.models.settings import Settings
from backend.app.services.ldap_service import LDAPUserInfo


async def _set_setting(db: AsyncSession, key: str, value: str) -> None:
    result = await db.execute(select(Settings).where(Settings.key == key))
    row = result.scalar_one_or_none()
    if row is None:
        db.add(Settings(key=key, value=value))
    else:
        row.value = value
    await db.commit()


async def _enable_auth(async_client: AsyncClient, username: str = "gateadm") -> None:
    """Set up an auth-enabled install with a known admin so /auth/login is reachable."""
    await async_client.post(
        "/api/v1/auth/setup",
        json={
            "auth_enabled": True,
            "admin_username": username,
            "admin_password": "GatePass1!",
        },
    )


class TestLocalLoginGate:
    """The `local_login_enabled` setting blocks /auth/login + /auth/forgot-password
    when the env-var recovery bypass is not in play."""

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_login_default_allows_local_credentials(self, async_client: AsyncClient, db_session: AsyncSession):
        """Default install (setting absent) keeps the pre-#1589 behaviour."""
        await _enable_auth(async_client, "gatedefault")
        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": "gatedefault", "password": "GatePass1!"},
        )
        assert response.status_code == 200, response.text

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_login_rejected_when_local_disabled(
        self, async_client: AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
    ):
        """With local_login_enabled=false and no env bypass, valid creds are
        rejected with the same generic 401 as bad creds (no UI-stating leak)."""
        await _enable_auth(async_client, "gatedeny")
        await _set_setting(db_session, "local_login_enabled", "false")
        monkeypatch.delenv("BAMBUDDY_LOCAL_LOGIN", raising=False)

        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": "gatedeny", "password": "GatePass1!"},
        )
        assert response.status_code == 401
        # Same wording as wrong-password 401 — never leaks whether local
        # login is disabled (would help credential stuffing prioritise).
        assert "Incorrect username or password" in response.json()["detail"]

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_env_var_bypasses_local_disabled_gate(
        self, async_client: AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
    ):
        """BAMBUDDY_LOCAL_LOGIN=true opens the recovery path even when the
        DB setting forbids local login (SSO-broken admin recovery)."""
        await _enable_auth(async_client, "gatebypass")
        await _set_setting(db_session, "local_login_enabled", "false")
        monkeypatch.setenv("BAMBUDDY_LOCAL_LOGIN", "true")

        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": "gatebypass", "password": "GatePass1!"},
        )
        assert response.status_code == 200, response.text

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_unrecognized_env_value_does_not_500_the_login_path(
        self, async_client: AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
    ):
        """The recovery bypass reads BAMBUDDY_LOCAL_LOGIN on the request path, so
        an unrecognized value (BAMBUDDY_LOCAL_LOGIN=on) must fall back to "off",
        never raise -- env_bool is strict for the startup OIDC reader but lenient
        here. A raise would 500 the very endpoint the bypass exists to keep open."""
        await _enable_auth(async_client, "gateonval")
        await _set_setting(db_session, "local_login_enabled", "false")
        monkeypatch.setenv("BAMBUDDY_LOCAL_LOGIN", "on")

        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": "gateonval", "password": "GatePass1!"},
        )
        # Bypass stays off (same 401 as no env var), and crucially not a 500.
        assert response.status_code == 401, response.text

    def test_the_bypass_var_is_registered_in_the_typo_guard(self):
        """config.py logs "possible typo" for any unregistered BAMBUDDY_* var.
        Unregistered, this one tells an operator who is locked out and following
        the documented recovery that the variable they just set is not real --
        while the same line lists every BAMBUDDY_OIDC_* var as legitimate."""
        from backend.app.core.config import _INTENTIONAL_UNSETTINGS

        assert "BAMBUDDY_LOCAL_LOGIN" in _INTENTIONAL_UNSETTINGS

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_forgot_password_rejected_when_local_disabled(
        self, async_client: AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
    ):
        """Forgot-password is a local-credentials flow — useless when local
        login is off (the reset wouldn't grant access anyway)."""
        await _enable_auth(async_client, "gatefp")
        await _set_setting(db_session, "local_login_enabled", "false")
        monkeypatch.delenv("BAMBUDDY_LOCAL_LOGIN", raising=False)

        response = await async_client.post(
            "/api/v1/auth/forgot-password",
            json={"email": "x@example.com"},
        )
        assert response.status_code == 403
        assert "Local login is disabled" in response.json()["detail"]


class TestLdapLoginNotAffectedByGate:
    """LDAP keeps its own ldap_enabled switch and bypasses local_login_enabled
    entirely. This is the regression suite for the refactor in #1589 — without
    these tests, an LDAP user could fail to log in when local login is
    disabled even though the gate is supposed to leave LDAP alone."""

    async def _enable_ldap(self, db: AsyncSession) -> None:
        for key, value in {
            "ldap_enabled": "true",
            "ldap_server_url": "ldaps://ldap.test",
            "ldap_bind_dn": "cn=svc,dc=test,dc=com",
            "ldap_bind_password": "x",
            "ldap_search_base": "dc=test,dc=com",
            "ldap_user_filter": "(uid={username})",
            "ldap_security": "ldaps",
            "ldap_group_mapping": "{}",
            "ldap_auto_provision": "true",
            "ldap_default_group": "",
        }.items():
            await _set_setting(db, key, value)

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_ldap_login_succeeds_when_local_disabled(
        self, async_client: AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
    ):
        """LDAP-authenticated login must still issue a JWT even when the
        local-login gate is off and no env-var bypass is set. The original
        cut of #1589 wiped the LDAP-bound `user` variable in this branch."""
        await _enable_auth(async_client, "ldapseed")
        await self._enable_ldap(db_session)
        await _set_setting(db_session, "local_login_enabled", "false")
        monkeypatch.delenv("BAMBUDDY_LOCAL_LOGIN", raising=False)

        fake_ldap = LDAPUserInfo(
            username="ldapuser",
            email="ldapuser@test.com",
            display_name="LDAP User",
            groups=[],
        )
        with patch(
            "backend.app.services.ldap_service.authenticate_ldap_user",
            return_value=fake_ldap,
        ):
            response = await async_client.post(
                "/api/v1/auth/login",
                json={"username": "ldapuser", "password": "anything"},
            )

        assert response.status_code == 200, response.text
        assert "access_token" in response.json()
        assert response.json()["user"]["username"] == "ldapuser"


class TestAdvancedAuthStatusSurfacesGate:
    """The /auth/advanced-auth/status endpoint feeds the LoginPage's render
    decisions in a single query — it must surface both new #1589 fields."""

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_status_includes_local_login_and_autologin(
        self, async_client: AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
    ):
        monkeypatch.delenv("BAMBUDDY_LOCAL_LOGIN", raising=False)
        response = await async_client.get("/api/v1/auth/advanced-auth/status")
        assert response.status_code == 200
        result = response.json()
        assert "local_login_enabled" in result
        assert "autologin_provider_id" in result
        # Default install: local on, no autologin provider.
        assert result["local_login_enabled"] is True
        assert result["autologin_provider_id"] is None

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_env_var_bypass_flips_status_back_to_true(
        self, async_client: AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
    ):
        """When the DB setting is false but the env-var bypass is set, the
        status reports local_login_enabled=true so the LoginPage shows the
        credentials form (matching what the route will actually accept)."""
        await _set_setting(db_session, "local_login_enabled", "false")
        monkeypatch.setenv("BAMBUDDY_LOCAL_LOGIN", "true")

        response = await async_client.get("/api/v1/auth/advanced-auth/status")
        assert response.status_code == 200
        assert response.json()["local_login_enabled"] is True
