"""Authentication endpoint tests."""
from datetime import datetime, timedelta, timezone
from uuid import uuid4

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

from src.apps.auth.models.refresh_token import RefreshToken


@pytest.mark.asyncio
async def test_login_success(client: AsyncClient, test_admin_user, test_account):
    response = await client.post(
        "/api/v1/auth/login",
        json={"email": "admin@testcemetery.com", "password": "TestPassword123"},
        headers={"X-Tenant-ID": str(test_account.id)},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["success"] is True
    assert "access_token" in data["data"]
    assert "refresh_token" in data["data"]
    assert data["data"]["user"]["role"] == "administrator"


@pytest.mark.asyncio
async def test_login_wrong_password(client: AsyncClient, test_admin_user, test_account):
    response = await client.post(
        "/api/v1/auth/login",
        json={"email": "admin@testcemetery.com", "password": "WrongPassword"},
        headers={"X-Tenant-ID": str(test_account.id)},
    )
    assert response.status_code == 401
    assert response.json()["success"] is False


@pytest.mark.asyncio
async def test_get_me(client: AsyncClient, auth_headers):
    response = await client.get("/api/v1/auth/me", headers=auth_headers)
    assert response.status_code == 200
    data = response.json()
    assert data["success"] is True
    assert data["data"]["email"] == "admin@testcemetery.com"


@pytest.mark.asyncio
async def test_get_me_unauthenticated(client: AsyncClient):
    response = await client.get("/api/v1/auth/me")
    assert response.status_code == 401


@pytest.mark.asyncio
async def test_refresh_token(client: AsyncClient, test_admin_user, test_account):
    # Login first
    login_resp = await client.post(
        "/api/v1/auth/login",
        json={"email": "admin@testcemetery.com", "password": "TestPassword123"},
        headers={"X-Tenant-ID": str(test_account.id)},
    )
    refresh_token = login_resp.json()["data"]["refresh_token"]

    # Refresh
    response = await client.post(
        "/api/v1/auth/refresh",
        json={"refresh_token": refresh_token},
    )
    assert response.status_code == 200
    assert "access_token" in response.json()["data"]


@pytest.mark.asyncio
async def test_logout(client: AsyncClient, test_admin_user, test_account):
    login_resp = await client.post(
        "/api/v1/auth/login",
        json={"email": "admin@testcemetery.com", "password": "TestPassword123"},
        headers={"X-Tenant-ID": str(test_account.id)},
    )
    refresh_token = login_resp.json()["data"]["refresh_token"]

    response = await client.post(
        "/api/v1/auth/logout",
        json={"refresh_token": refresh_token},
    )
    assert response.status_code == 204


# ─────────────────────────────────────────────────────────────────────────────
# POST /api/v1/auth/change-password (INDL-55)
# ─────────────────────────────────────────────────────────────────────────────

CHANGE_PASSWORD_URL = "/api/v1/auth/change-password"


@pytest.fixture(autouse=True)
def _clear_change_password_rate_limit():
    """Clear the Redis rate-limit counter for this bucket namespace before each
    test to prevent bleed between runs (mirrors the pattern used in
    tests/apps/public/test_plot_inquiries.py)."""
    import redis
    from src.core.config import settings

    r = redis.from_url(settings.REDIS_URL, decode_responses=True)
    for key in r.keys("rate:change-password:*"):
        r.delete(key)
    yield
    r.close()


@pytest_asyncio.fixture
async def compliant_password_user(db_session: AsyncSession, test_admin_user):
    """`test_admin_user`'s fixture password ("TestPassword123") has no symbol,
    so it fails `validate_password_strength` and can't be used as a
    `new_password` (e.g. for the reuse test, which needs current == new).
    Re-hash it to a policy-compliant password and return the plaintext."""
    from src.core.security import hash_password

    plaintext = "Str0ng!Pass1"
    test_admin_user.password_hash = hash_password(plaintext)
    await db_session.flush()
    return plaintext


@pytest.mark.asyncio
async def test_change_password_success(
    client: AsyncClient,
    db_session: AsyncSession,
    test_admin_user,
    auth_headers,
):
    """Correct current password + valid new password -> 200; password_hash
    updated; password_changed_at stamped; all active refresh tokens for the
    user are revoked."""
    token_record = RefreshToken(
        user_id=test_admin_user.id,
        token_hash="fake-hash-for-success-test",
        expires_at=datetime.now(timezone.utc) + timedelta(days=7),
    )
    db_session.add(token_record)
    await db_session.flush()

    old_hash = test_admin_user.password_hash

    response = await client.post(
        CHANGE_PASSWORD_URL,
        json={
            "current_password": "TestPassword123",
            "new_password": "NewStr0ng!Pass1",
            "confirm_password": "NewStr0ng!Pass1",
        },
        headers=auth_headers,
    )

    assert response.status_code == 200
    assert response.json()["success"] is True

    assert test_admin_user.password_hash != old_hash
    assert test_admin_user.password_changed_at is not None

    result = await db_session.execute(
        select(RefreshToken).where(RefreshToken.user_id == test_admin_user.id)
    )
    tokens = result.scalars().all()
    assert len(tokens) >= 1
    assert all(t.revoked_at is not None for t in tokens)


@pytest.mark.asyncio
async def test_change_password_wrong_current_password(
    client: AsyncClient, test_admin_user, auth_headers
):
    """Wrong current password -> 401, generic message, no DB write."""
    old_hash = test_admin_user.password_hash

    response = await client.post(
        CHANGE_PASSWORD_URL,
        json={
            "current_password": "TotallyWrongPassword1!",
            "new_password": "NewStr0ng!Pass1",
            "confirm_password": "NewStr0ng!Pass1",
        },
        headers=auth_headers,
    )

    assert response.status_code == 401
    assert response.json()["message"] == "Current password is incorrect"
    assert test_admin_user.password_hash == old_hash


@pytest.mark.asyncio
async def test_change_password_weak_new_password(
    client: AsyncClient, test_admin_user, auth_headers
):
    """New password fails policy -> 422, no DB write."""
    old_hash = test_admin_user.password_hash

    response = await client.post(
        CHANGE_PASSWORD_URL,
        json={
            "current_password": "TestPassword123",
            "new_password": "weak",
            "confirm_password": "weak",
        },
        headers=auth_headers,
    )

    assert response.status_code == 422
    assert test_admin_user.password_hash == old_hash


@pytest.mark.asyncio
async def test_change_password_mismatch(
    client: AsyncClient, test_admin_user, auth_headers
):
    """new_password != confirm_password -> 422 'Passwords do not match', no DB write."""
    old_hash = test_admin_user.password_hash

    response = await client.post(
        CHANGE_PASSWORD_URL,
        json={
            "current_password": "TestPassword123",
            "new_password": "Str0ng!Pass1",
            "confirm_password": "Str0ng!Pass2",
        },
        headers=auth_headers,
    )

    assert response.status_code == 422
    messages = [e["message"] for e in response.json()["errors"]]
    assert any("Passwords do not match" in m for m in messages)
    assert test_admin_user.password_hash == old_hash


@pytest.mark.asyncio
async def test_change_password_reuse(
    client: AsyncClient,
    test_admin_user,
    auth_headers,
    compliant_password_user,
):
    """new_password == current_password -> 422 reuse message, no DB write."""
    old_hash = test_admin_user.password_hash

    response = await client.post(
        CHANGE_PASSWORD_URL,
        json={
            "current_password": compliant_password_user,
            "new_password": compliant_password_user,
            "confirm_password": compliant_password_user,
        },
        headers=auth_headers,
    )

    assert response.status_code == 422
    assert (
        response.json()["message"]
        == "New password must be different from your current password"
    )
    assert test_admin_user.password_hash == old_hash


@pytest.mark.asyncio
async def test_change_password_rate_limit(
    client: AsyncClient, test_admin_user, auth_headers
):
    """6 rapid wrong-current-password attempts for the same user within the
    window -> the 6th returns 429."""
    payload = {
        "current_password": "TotallyWrongPassword1!",
        "new_password": "Str0ng!Pass9",
        "confirm_password": "Str0ng!Pass9",
    }

    for _ in range(5):
        response = await client.post(
            CHANGE_PASSWORD_URL, json=payload, headers=auth_headers
        )
        assert response.status_code == 401

    response = await client.post(
        CHANGE_PASSWORD_URL, json=payload, headers=auth_headers
    )
    assert response.status_code == 429


@pytest.mark.asyncio
async def test_change_password_rejects_injected_user_id(
    client: AsyncClient, test_admin_user, auth_headers
):
    """Injected `user_id` field in the request body -> 422 (extra='forbid'),
    confirming AC-11 (target is always the JWT-resolved caller)."""
    old_hash = test_admin_user.password_hash

    response = await client.post(
        CHANGE_PASSWORD_URL,
        json={
            "current_password": "TestPassword123",
            "new_password": "NewStr0ng!Pass1",
            "confirm_password": "NewStr0ng!Pass1",
            "user_id": str(uuid4()),
        },
        headers=auth_headers,
    )

    assert response.status_code == 422
    assert test_admin_user.password_hash == old_hash


@pytest.mark.asyncio
async def test_change_password_revokes_refresh_token_for_future_use(
    client: AsyncClient, test_admin_user, test_account, auth_headers
):
    """T-12: a refresh token issued before the change is rejected (401) by
    POST /api/v1/auth/refresh after a successful password change."""
    login_resp = await client.post(
        "/api/v1/auth/login",
        json={"email": "admin@testcemetery.com", "password": "TestPassword123"},
        headers={"X-Tenant-ID": str(test_account.id)},
    )
    assert login_resp.status_code == 200
    pre_change_refresh_token = login_resp.json()["data"]["refresh_token"]

    change_resp = await client.post(
        CHANGE_PASSWORD_URL,
        json={
            "current_password": "TestPassword123",
            "new_password": "NewStr0ng!Pass1",
            "confirm_password": "NewStr0ng!Pass1",
        },
        headers=auth_headers,
    )
    assert change_resp.status_code == 200

    refresh_resp = await client.post(
        "/api/v1/auth/refresh",
        json={"refresh_token": pre_change_refresh_token},
    )
    assert refresh_resp.status_code == 401
