"""Sign-in, end to end (spec admin-auth, design D6, D7, D14, D15, D16).

Real database, real cookies, real Argon2. What matters here is mostly what the responses do
*not* distinguish: an unknown username and a wrong password have to be one answer, or the
form becomes a way to enumerate who administers the system.
"""

import uuid
from collections.abc import AsyncIterator
from typing import Any

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import get_settings
from app.services import auth, passwords

from .conftest import requires_db

pytestmark = [requires_db, pytest.mark.asyncio]

GOOD_PASSWORD = "mango-tree-42-sunset"
OTHER_PASSWORD = "lychee-river-77-dawn"


@pytest_asyncio.fixture(autouse=True)
async def _fresh_rate_limit_window(session: AsyncSession) -> AsyncIterator[None]:
    """Clear the per-IP sign-in buckets around every test in this module.

    Every ASGI test shares one source address, so the twenty-attempts-per-quarter-hour
    ceiling is reached partway through the file and the rest fail with 429 — a fact about
    the fixture, not the code. Cleared rather than raised: the limit is the thing under
    test in `test_repeated_failures_lock_the_account`, and a limit only exercised at an
    inflated value is not the one production runs.
    """
    buckets = ("password_signin", "admin_signup")
    await session.execute(
        text("DELETE FROM rate_limit WHERE bucket = ANY(:b)"), {"b": list(buckets)}
    )
    await session.commit()
    yield
    await session.execute(
        text("DELETE FROM rate_limit WHERE bucket = ANY(:b)"), {"b": list(buckets)}
    )
    await session.commit()


@pytest_asyncio.fixture
async def client(app_engine_per_loop: None) -> AsyncIterator[AsyncClient]:
    from app.main import app

    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
        yield c


async def _password_account(
    session: AsyncSession,
    *,
    password: str = GOOD_PASSWORD,
    temporary: bool = False,
    status: str = "active",
) -> dict[str, Any]:
    admin_id = uuid.uuid4()
    username = f"user{admin_id.hex[:8]}"
    await session.execute(
        text(
            "INSERT INTO admin_user (id, email, name, username, password_hash, role, status, "
            "                        auth_method, must_change_password, failed_login_count, "
            "                        password_set_at) "
            "VALUES (:id, :email, 'Password Admin', :username, :hash, 'host', :status, "
            "        'password', :temp, 0, now())"
        ),
        {
            "id": admin_id,
            "email": f"{username}@example.com",
            "username": username,
            "hash": passwords.hash_password(password),
            "status": status,
            "temp": temporary,
        },
    )
    await session.commit()
    return {"id": admin_id, "username": username, "email": f"{username}@example.com"}


async def _cleanup(session: AsyncSession, admin_id: uuid.UUID) -> None:
    await session.execute(text("DELETE FROM audit_log WHERE admin_user_id = :id"), {"id": admin_id})
    await session.execute(text("DELETE FROM admin_user WHERE id = :id"), {"id": admin_id})
    await session.commit()


# ---------------------------------------------------------------- password sign-in


async def test_a_correct_password_issues_a_session_cookie(
    client: AsyncClient, session: AsyncSession
) -> None:
    account = await _password_account(session)
    response = await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    assert response.status_code == 200, response.text
    assert response.json()["auth_method"] == "password"
    assert response.json()["username"] == account["username"]

    cookie = response.headers.get("set-cookie", "")
    assert auth.SESSION_COOKIE in cookie
    assert "HttpOnly" in cookie
    await _cleanup(session, account["id"])


async def test_a_wrong_password_and_an_unknown_username_are_the_same_answer(
    client: AsyncClient, session: AsyncSession
) -> None:
    """The difference is what an attacker working through a list is trying to measure."""
    account = await _password_account(session)

    wrong = await client.post(
        "/api/auth/password", json={"username": account["username"], "password": "wrong-one-1234"}
    )
    unknown = await client.post(
        "/api/auth/password", json={"username": "nobody-at-all", "password": "wrong-one-1234"}
    )

    assert wrong.status_code == unknown.status_code == 401
    assert wrong.json() == unknown.json()
    assert wrong.json()["detail"] == auth.GENERIC_SIGNIN_ERROR
    await _cleanup(session, account["id"])


async def test_repeated_failures_lock_the_account_even_against_the_right_password(
    client: AsyncClient, session: AsyncSession
) -> None:
    account = await _password_account(session)
    settings = get_settings()

    for _ in range(settings.password_max_attempts):
        await client.post(
            "/api/auth/password",
            json={"username": account["username"], "password": "wrong-one-1234"},
        )

    locked = await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    assert locked.status_code == 401

    # And the lockout is not itself an oracle: it reads like an unknown username.
    unknown = await client.post(
        "/api/auth/password", json={"username": "nobody-at-all", "password": GOOD_PASSWORD}
    )
    assert locked.json() == unknown.json()
    await _cleanup(session, account["id"])


async def test_a_successful_sign_in_clears_the_failure_count(
    client: AsyncClient, session: AsyncSession
) -> None:
    account = await _password_account(session)
    await client.post(
        "/api/auth/password", json={"username": account["username"], "password": "wrong-one-1234"}
    )
    await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    remaining = await session.scalar(
        text("SELECT failed_login_count FROM admin_user WHERE id = :id").bindparams(
            id=account["id"]
        )
    )
    assert remaining == 0
    await _cleanup(session, account["id"])


async def test_a_withdrawn_account_is_told_so_once_the_password_is_right(
    client: AsyncClient, session: AsyncSession
) -> None:
    """They proved who they are, so they may know the state of their own account."""
    account = await _password_account(session, status="withdrawn")
    response = await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    assert response.status_code == 403
    assert "withdrawn" in response.json()["detail"].lower()
    await _cleanup(session, account["id"])


async def test_a_google_account_cannot_be_reached_through_the_password_form(
    client: AsyncClient, session: AsyncSession
) -> None:
    """Kind is fixed at creation, and the two paths never cross (design D16)."""
    admin_id = uuid.uuid4()
    email = f"google-only-{admin_id.hex[:8]}@example.com"
    await session.execute(
        text(
            "INSERT INTO admin_user (id, email, name, username, role, status, auth_method, "
            "                        must_change_password, failed_login_count) "
            "VALUES (:id, :email, 'Google Only', :username, 'host', 'active', 'google', "
            "        false, 0)"
        ),
        {"id": admin_id, "email": email, "username": f"goog{admin_id.hex[:8]}"},
    )
    await session.commit()

    response = await client.post(
        "/api/auth/password",
        json={"username": f"goog{admin_id.hex[:8]}", "password": GOOD_PASSWORD},
    )
    assert response.status_code == 401
    assert response.json()["detail"] == auth.GENERIC_SIGNIN_ERROR
    await _cleanup(session, admin_id)


# ------------------------------------------------------------- temporary passwords


async def test_a_temporary_password_signs_in_but_is_confined(
    client: AsyncClient, session: AsyncSession
) -> None:
    account = await _password_account(session, temporary=True)

    signin = await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    assert signin.status_code == 200
    assert signin.json()["must_change_password"] is True

    blocked = await client.get("/api/admin/stats")
    assert blocked.status_code == 403
    assert blocked.json()["detail"] == auth.PASSWORD_CHANGE_REQUIRED
    await _cleanup(session, account["id"])


async def test_changing_the_password_lifts_the_confinement_without_signing_in_again(
    client: AsyncClient, session: AsyncSession
) -> None:
    account = await _password_account(session, temporary=True)
    await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )

    changed = await client.post(
        "/api/auth/password/change",
        json={"current_password": GOOD_PASSWORD, "new_password": OTHER_PASSWORD},
    )
    assert changed.status_code == 200, changed.text
    assert changed.json()["must_change_password"] is False

    # The same client, no second sign-in: the cookie was re-issued by the change itself.
    allowed = await client.get("/api/admin/stats")
    assert allowed.status_code == 200
    await _cleanup(session, account["id"])


async def test_the_new_password_cannot_be_the_temporary_one(
    client: AsyncClient, session: AsyncSession
) -> None:
    """Otherwise a credential two people know stays live and the flow reports success."""
    account = await _password_account(session, temporary=True)
    await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    response = await client.post(
        "/api/auth/password/change",
        json={"current_password": GOOD_PASSWORD, "new_password": GOOD_PASSWORD},
    )
    assert response.status_code == 400
    await _cleanup(session, account["id"])


async def test_a_weak_new_password_is_refused(client: AsyncClient, session: AsyncSession) -> None:
    account = await _password_account(session, temporary=True)
    await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    response = await client.post(
        "/api/auth/password/change",
        json={"current_password": GOOD_PASSWORD, "new_password": "short"},
    )
    assert response.status_code == 400
    await _cleanup(session, account["id"])


async def test_the_wrong_current_password_leaves_the_stored_one_alone(
    client: AsyncClient, session: AsyncSession
) -> None:
    account = await _password_account(session)
    await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    response = await client.post(
        "/api/auth/password/change",
        json={"current_password": "not-the-one-1234", "new_password": OTHER_PASSWORD},
    )
    assert response.status_code == 400

    still_works = await client.post(
        "/api/auth/password", json={"username": account["username"], "password": GOOD_PASSWORD}
    )
    assert still_works.status_code == 200
    await _cleanup(session, account["id"])


async def test_a_password_change_invalidates_a_session_held_elsewhere(
    client: AsyncClient, session: AsyncSession
) -> None:
    """The one revocation the per-request row lookup cannot express (design D17)."""
    from app.main import app

    account = await _password_account(session)

    async with AsyncClient(
        transport=ASGITransport(app=app), base_url="http://test"
    ) as other_device:
        signed_in = await other_device.post(
            "/api/auth/password",
            json={"username": account["username"], "password": GOOD_PASSWORD},
        )
        assert signed_in.status_code == 200
        assert (await other_device.get("/api/auth/me")).status_code == 200

        await client.post(
            "/api/auth/password",
            json={"username": account["username"], "password": GOOD_PASSWORD},
        )
        await client.post(
            "/api/auth/password/change",
            json={"current_password": GOOD_PASSWORD, "new_password": OTHER_PASSWORD},
        )

        # The other device's token is unexpired and its account is still active, and it is
        # refused anyway — because it was issued before the credential changed.
        assert (await other_device.get("/api/auth/me")).status_code == 401

    # The device that made the change keeps working.
    assert (await client.get("/api/auth/me")).status_code == 200
    await _cleanup(session, account["id"])


async def test_a_google_account_has_no_password_to_change(
    client: AsyncClient, session: AsyncSession, fixture_event: dict[str, Any]
) -> None:
    from app.main import app
    from app.models.enums import AdminRole, AuthMethod
    from app.services.auth import CurrentAdmin, get_current_admin

    owner_id = fixture_event["owner_admin_id"]
    app.dependency_overrides[get_current_admin] = lambda: CurrentAdmin(
        id=owner_id,
        email="owner@example.test",
        name="Owner",
        role=AdminRole.SUPER_ADMIN,
        auth_method=AuthMethod.GOOGLE,
    )
    try:
        response = await client.post(
            "/api/auth/password/change",
            json={"current_password": GOOD_PASSWORD, "new_password": OTHER_PASSWORD},
        )
        assert response.status_code == 400
        assert "Google" in response.json()["detail"]
    finally:
        app.dependency_overrides.pop(get_current_admin, None)


# ------------------------------------------------------------------ the dev bypass


async def test_the_dev_bypass_never_provisions_an_unknown_email(
    client: AsyncClient, session: AsyncSession
) -> None:
    """It is a shortcut past authentication for an existing admin, not a way in."""
    settings = get_settings()
    if settings.is_prod or not settings.auth_dev_bypass:
        pytest.skip("dev bypass is disabled in this configuration")

    email = f"never-seen-{uuid.uuid4().hex[:8]}@example.com"
    response = await client.post("/api/auth/dev", json={"email": email})
    assert response.status_code == 401

    created = await session.scalar(
        text("SELECT count(*) FROM admin_user WHERE email = :e").bindparams(e=email)
    )
    assert created == 0


async def test_the_dev_bypass_does_not_lift_the_confinement(
    client: AsyncClient, session: AsyncSession
) -> None:
    """Confinement is read from the row, not from how the caller arrived (design D9)."""
    settings = get_settings()
    if settings.is_prod or not settings.auth_dev_bypass:
        pytest.skip("dev bypass is disabled in this configuration")

    account = await _password_account(session, temporary=True)
    signin = await client.post("/api/auth/dev", json={"email": account["email"]})
    assert signin.status_code == 200

    blocked = await client.get("/api/admin/stats")
    assert blocked.status_code == 403
    assert blocked.json()["detail"] == auth.PASSWORD_CHANGE_REQUIRED
    await _cleanup(session, account["id"])
