import asyncio
import time
import uuid
from datetime import UTC, datetime, timedelta

from app.config import get_settings
from app.main import app
from app.models import DeviceSession, MfaRecoveryCode, PasswordResetToken, User, WorkspaceMembership
from app.security import _totp_value, generate_token, hash_token
from fastapi.testclient import TestClient
from sqlalchemy import select, update


def owner_payload():
    return {
        "email": "owner@example.com",
        "password": "correct horse battery staple",
        "display_name": "Owner",
        "workspace_name": "Personal",
        "timezone": "Asia/Dubai",
    }


def test_owner_setup_is_one_time_and_sets_protected_session(auth_client):
    assert auth_client.get("/api/v1/setup/status").json() == {"setup_required": True}
    with TestClient(app) as client_without_capability:
        forbidden = client_without_capability.post("/api/v1/setup/owner", json=owner_payload())
    assert forbidden.status_code == 403
    assert forbidden.json()["error"]["code"] == "BOOTSTRAP_FORBIDDEN"
    response = auth_client.post("/api/v1/setup/owner", json=owner_payload())
    assert response.status_code == 201
    assert response.json()["success"] is True
    assert auth_client.cookies.get("hayva_session")
    assert auth_client.cookies.get("hayva_csrf")
    assert auth_client.get("/api/v1/setup/status").json() == {"setup_required": False}
    assert auth_client.get("/health/database").json() == {"status": "ok", "service": "database"}

    profile = auth_client.get("/api/v1/auth/me")
    assert profile.status_code == 200
    assert profile.json()["user"]["email"] == "owner@example.com"
    assert "approvals.approve" in profile.json()["permissions"]
    sessions = auth_client.get("/api/v1/auth/sessions").json()["sessions"]
    assert sessions[0]["current"] is True
    assert "token_hash" not in sessions[0]

    repeated = auth_client.post("/api/v1/setup/owner", json=owner_payload())
    assert repeated.status_code == 409
    assert repeated.json()["error"]["code"] == "SETUP_ALREADY_COMPLETED"
    assert repeated.json()["error"]["request_id"]


def test_login_is_generic_and_logout_requires_matching_csrf(auth_client):
    auth_client.post("/api/v1/setup/owner", json=owner_payload())
    auth_client.cookies.clear()

    invalid = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": "incorrect"})
    assert invalid.status_code == 401
    assert invalid.json()["error"]["code"] == "INVALID_CREDENTIALS"
    unknown = auth_client.post("/api/v1/auth/login", json={
        "email": "unknown@example.com", "password": "incorrect"})
    assert unknown.status_code == invalid.status_code
    assert unknown.json()["error"]["code"] == invalid.json()["error"]["code"]
    assert unknown.json()["error"]["message"] == invalid.json()["error"]["message"]

    logged_in = auth_client.post("/api/v1/auth/login", json={
        "email": "OWNER@example.com", "password": "correct horse battery staple",
        "remember_device": True, "device_name": "Test browser"})
    assert logged_in.status_code == 200
    csrf = auth_client.cookies.get("hayva_csrf")
    assert csrf

    rejected = auth_client.post("/api/v1/auth/logout", headers={"x-csrf-token": "wrong"})
    assert rejected.status_code == 403
    assert rejected.json()["error"]["code"] == "CSRF_INVALID"

    logout = auth_client.post("/api/v1/auth/logout", headers={"x-csrf-token": csrf})
    assert logout.status_code == 204
    assert auth_client.cookies.get("hayva_session") is None


def test_logout_all_revokes_every_owner_session(auth_client):
    auth_client.post("/api/v1/setup/owner", json=owner_payload())
    auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": "correct horse battery staple",
        "device_name": "Second browser"})
    csrf = auth_client.cookies.get("hayva_csrf")
    response = auth_client.post("/api/v1/auth/logout-all", headers={"x-csrf-token": csrf})
    assert response.status_code == 204
    assert auth_client.cookies.get("hayva_session") is None


def test_inactive_user_or_membership_invalidates_existing_session(auth_client):
    auth_client.post("/api/v1/setup/owner", json=owner_payload())

    async def deactivate_user():
        async with auth_client.session_factory() as session:
            await session.execute(update(User).values(is_active=False))
            await session.commit()

    asyncio.run(deactivate_user())
    assert auth_client.get("/api/v1/auth/me").status_code == 401

    async def reactivate_user_and_suspend_membership():
        async with auth_client.session_factory() as session:
            await session.execute(update(User).values(is_active=True))
            await session.execute(update(WorkspaceMembership).values(status="suspended"))
            await session.commit()

    asyncio.run(reactivate_user_and_suspend_membership())
    assert auth_client.get("/api/v1/auth/me").status_code == 401


def test_login_rate_limit_is_enforced_per_privacy_preserving_account_key(auth_client):
    for _ in range(10):
        response = auth_client.post("/api/v1/auth/login", json={
            "email": "unknown@example.com", "password": "incorrect"})
        assert response.status_code == 401
    limited = auth_client.post("/api/v1/auth/login", json={
        "email": "unknown@example.com", "password": "incorrect"})
    assert limited.status_code == 429
    assert limited.json()["error"]["code"] == "AUTH_RATE_LIMITED"


def test_account_failures_do_not_lock_out_valid_credentials(auth_client):
    auth_client.post("/api/v1/setup/owner", json=owner_payload())
    auth_client.cookies.clear()
    for attempt in range(11):
        response = auth_client.post("/api/v1/auth/login", json={
            "email": "owner@example.com", "password": "incorrect"})
        assert response.status_code == (401 if attempt < 10 else 429)

    recovered = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": "correct horse battery staple"})
    assert recovered.status_code == 200


def test_production_authentication_cookies_are_secure(auth_client):
    settings = get_settings()
    original_environment = settings.app_env
    settings.app_env = "production"
    try:
        response = auth_client.post(
            "/api/v1/setup/owner", json=owner_payload(), headers={"host": "localhost"}
        )
    finally:
        settings.app_env = original_environment
    assert response.status_code == 201
    assert all("Secure" in value for value in response.headers.get_list("set-cookie"))


def test_mfa_enrollment_login_recovery_and_disable_are_fail_closed(auth_client):
    auth_client.post("/api/v1/setup/owner", json=owner_payload())
    csrf = auth_client.cookies.get("hayva_csrf")
    setup = auth_client.post(
        "/api/v1/auth/mfa/setup",
        json={"current_password": owner_payload()["password"]},
        headers={"x-csrf-token": csrf},
    )
    assert setup.status_code == 200
    secret = setup.json()["secret"]
    assert setup.json()["provisioning_uri"].startswith("otpauth://totp/")
    current_step = int(time.time() // 30)
    verify = auth_client.post(
        "/api/v1/auth/mfa/verify",
        json={"code": _totp_value(secret, current_step)},
        headers={"x-csrf-token": csrf},
    )
    assert verify.status_code == 200
    recovery_codes = verify.json()["recovery_codes"]
    assert len(recovery_codes) == 10
    assert auth_client.get("/api/v1/auth/me").json()["user"]["mfa_enabled"] is True

    auth_client.cookies.clear()
    missing = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": owner_payload()["password"],
    })
    assert missing.status_code == 401
    assert missing.json()["error"]["code"] == "MFA_REQUIRED"
    replay = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": owner_payload()["password"],
        "mfa_code": _totp_value(secret, current_step),
    })
    assert replay.status_code == 401
    assert replay.json()["error"]["code"] == "MFA_INVALID"
    recovered = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": owner_payload()["password"],
        "mfa_code": recovery_codes[0],
    })
    assert recovered.status_code == 200
    auth_client.cookies.clear()
    reused = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": owner_payload()["password"],
        "mfa_code": recovery_codes[0],
    })
    assert reused.status_code == 401

    next_code = _totp_value(secret, current_step + 1)
    logged_in = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": owner_payload()["password"],
        "mfa_code": next_code,
    })
    assert logged_in.status_code == 200
    csrf = auth_client.cookies.get("hayva_csrf")
    disabled = auth_client.post(
        "/api/v1/auth/mfa/disable",
        json={"current_password": owner_payload()["password"], "code": recovery_codes[1]},
        headers={"x-csrf-token": csrf},
    )
    assert disabled.status_code == 200
    assert auth_client.get("/api/v1/auth/me").json()["user"]["mfa_enabled"] is False

    async def recovery_state():
        async with auth_client.session_factory() as session:
            return await session.scalar(select(MfaRecoveryCode.id))

    assert asyncio.run(recovery_state()) is None


def test_session_rotation_idle_expiry_and_device_revocation(auth_client):
    auth_client.post("/api/v1/setup/owner", json=owner_payload())
    original_session = auth_client.cookies.get("hayva_session")
    original_csrf = auth_client.cookies.get("hayva_csrf")
    rotated = auth_client.post(
        "/api/v1/auth/session/rotate", headers={"x-csrf-token": original_csrf}
    )
    assert rotated.status_code == 200
    assert auth_client.cookies.get("hayva_session") != original_session
    assert auth_client.cookies.get("hayva_csrf") != original_csrf

    auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": owner_payload()["password"],
        "device_name": "Second browser",
    })
    sessions = auth_client.get("/api/v1/auth/sessions").json()["sessions"]
    other = next(item for item in sessions if not item["current"] and not item["revoked_at"])
    csrf = auth_client.cookies.get("hayva_csrf")
    revoked = auth_client.post(
        f"/api/v1/auth/sessions/{other['id']}/revoke",
        headers={"x-csrf-token": csrf},
    )
    assert revoked.status_code == 204

    async def expire_current():
        async with auth_client.session_factory() as session:
            token_hash = hash_token(auth_client.cookies.get("hayva_session"))
            await session.execute(update(DeviceSession).where(
                DeviceSession.token_hash == token_hash
            ).values(idle_expires_at=datetime.now(UTC) - timedelta(seconds=1)))
            await session.commit()

    asyncio.run(expire_current())
    expired = auth_client.get("/api/v1/auth/me")
    assert expired.status_code == 401
    assert expired.json()["error"]["code"] == "SESSION_INVALID"


def test_password_reset_is_one_time_revokes_sessions_and_requires_mfa_when_enabled(auth_client):
    auth_client.post("/api/v1/setup/owner", json=owner_payload())
    raw_reset = generate_token()

    async def issue_reset():
        async with auth_client.session_factory() as session:
            user = await session.scalar(select(User))
            membership = await session.scalar(select(WorkspaceMembership))
            session.add(PasswordResetToken(
                id=uuid.uuid4(), workspace_id=membership.workspace_id, user_id=user.id,
                token_hash=hash_token(raw_reset),
                expires_at=datetime.now(UTC) + timedelta(minutes=15),
            ))
            await session.commit()

    asyncio.run(issue_reset())
    reset = auth_client.post("/api/v1/auth/password-reset/confirm", json={
        "token": raw_reset, "new_password": "a completely new secure passphrase",
    })
    assert reset.status_code == 200
    assert auth_client.get("/api/v1/auth/me").status_code == 401
    replay = auth_client.post("/api/v1/auth/password-reset/confirm", json={
        "token": raw_reset, "new_password": "another completely new passphrase",
    })
    assert replay.status_code == 401
    old_login = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": owner_payload()["password"],
    })
    assert old_login.status_code == 401
    new_login = auth_client.post("/api/v1/auth/login", json={
        "email": "owner@example.com", "password": "a completely new secure passphrase",
    })
    assert new_login.status_code == 200
