"""Tests for browser-based login flow (login_flow.py)."""

from __future__ import annotations

import time
from unittest.mock import AsyncMock, patch, MagicMock

import pytest

from cube_cloud.auth.login_flow import (
    _sessions,
    auth_login,
    auth_authenticate,
    auth_session,
    SESSION_TTL,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_request(path: str = "/", query_params: dict | None = None, path_params: dict | None = None):
    """Create a minimal mock Starlette Request."""
    req = MagicMock()
    req.url.path = path
    req.query_params = query_params or {}
    req.path_params = path_params or {}
    return req


def _make_form_request(form_data: dict):
    """Create a mock Request with form data for POST endpoints."""
    req = MagicMock()
    req.url.path = "/auth/authenticate"

    async def mock_form():
        return form_data

    req.form = mock_form
    return req


# ---------------------------------------------------------------------------
# GET /auth/login — renders custom login page
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_auth_login_renders_login_page():
    """auth_login should return an HTML login page (not a redirect)."""
    with patch("cube_cloud.auth.login_flow.settings") as mock_settings:
        mock_settings.cognito_app_client_id = "test-client-id"
        req = _make_request(query_params={"session_id": "test-session-123"})
        resp = await auth_login(req)

    assert resp.status_code == 200
    body = resp.body.decode()
    assert "Sign In" in body
    assert "test-session-123" in body
    assert "EdgescaleAI" in body


@pytest.mark.asyncio
async def test_auth_login_missing_session_id():
    """auth_login without session_id returns 400."""
    req = _make_request(query_params={})
    resp = await auth_login(req)
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_auth_login_not_configured():
    """auth_login with empty Cognito config returns 503."""
    with patch("cube_cloud.auth.login_flow.settings") as mock_settings:
        mock_settings.cognito_app_client_id = ""
        req = _make_request(query_params={"session_id": "test-session"})
        resp = await auth_login(req)

    assert resp.status_code == 503


# ---------------------------------------------------------------------------
# POST /auth/authenticate — initial login
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_auth_authenticate_success():
    """Successful authentication creates API key and shows success page."""
    _sessions.clear()

    fake_claims = {
        "email": "user@example.com",
        "cognito:groups": ["admin"],
    }

    with patch("cube_cloud.auth.login_flow._initiate_auth") as mock_auth:
        mock_auth.return_value = {"id_token": "fake.jwt.token"}
        with patch("cube_cloud.auth.login_flow.jwt.get_unverified_claims", return_value=fake_claims):
            with patch("cube_cloud.auth.login_flow.create_key", return_value=("cmcp_test123", "hash123")) as mock_create:
                req = _make_form_request({
                    "email": "user@example.com",
                    "password": "secret123",
                    "session_id": "session-abc",
                })
                resp = await auth_authenticate(req)

    assert resp.status_code == 200
    assert "Login Successful" in resp.body.decode()

    # Key stored in session
    assert "session-abc" in _sessions
    assert _sessions["session-abc"]["api_key"] == "cmcp_test123"

    # create_key called with correct profile
    mock_create.assert_called_once_with("admin", description="Browser login: user@example.com")

    _sessions.clear()


@pytest.mark.asyncio
async def test_auth_authenticate_wrong_password():
    """Wrong password shows error on login page."""
    with patch("cube_cloud.auth.login_flow._initiate_auth") as mock_auth:
        mock_auth.return_value = {"error": "Incorrect email or password."}
        req = _make_form_request({
            "email": "user@example.com",
            "password": "wrong",
            "session_id": "session-abc",
        })
        resp = await auth_authenticate(req)

    assert resp.status_code == 200
    body = resp.body.decode()
    assert "Incorrect email or password" in body
    # Should still show the login form
    assert "Sign In" in body


@pytest.mark.asyncio
async def test_auth_authenticate_missing_fields():
    """Missing email/password shows error."""
    req = _make_form_request({"session_id": "session-abc", "email": "", "password": ""})
    resp = await auth_authenticate(req)
    body = resp.body.decode()
    assert "Email and password are required" in body


# ---------------------------------------------------------------------------
# POST /auth/authenticate — NEW_PASSWORD_REQUIRED challenge
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_auth_authenticate_new_password_challenge():
    """NEW_PASSWORD_REQUIRED challenge renders set-password page."""
    with patch("cube_cloud.auth.login_flow._initiate_auth") as mock_auth:
        mock_auth.return_value = {"challenge": "NEW_PASSWORD_REQUIRED", "session": "cognito-session-xyz"}
        req = _make_form_request({
            "email": "user@example.com",
            "password": "temppass",
            "session_id": "session-abc",
        })
        resp = await auth_authenticate(req)

    assert resp.status_code == 200
    body = resp.body.decode()
    assert "Set Your Password" in body
    assert "cognito-session-xyz" in body
    assert "user@example.com" in body


@pytest.mark.asyncio
async def test_auth_authenticate_new_password_submit():
    """Submitting new password after challenge completes login."""
    _sessions.clear()

    fake_claims = {
        "email": "user@example.com",
        "cognito:groups": ["admin"],
    }

    with patch("cube_cloud.auth.login_flow._respond_to_challenge") as mock_respond:
        mock_respond.return_value = {"id_token": "new.jwt.token"}
        with patch("cube_cloud.auth.login_flow.jwt.get_unverified_claims", return_value=fake_claims):
            with patch("cube_cloud.auth.login_flow.create_key", return_value=("cmcp_new123", "hash456")):
                req = _make_form_request({
                    "email": "user@example.com",
                    "session_id": "session-abc",
                    "challenge_session": "cognito-session-xyz",
                    "new_password": "MyNewSecurePass1",
                })
                resp = await auth_authenticate(req)

    assert resp.status_code == 200
    assert "Login Successful" in resp.body.decode()

    assert "session-abc" in _sessions
    assert _sessions["session-abc"]["api_key"] == "cmcp_new123"

    _sessions.clear()


@pytest.mark.asyncio
async def test_auth_authenticate_new_password_error():
    """Invalid new password shows error on set-password page."""
    with patch("cube_cloud.auth.login_flow._respond_to_challenge") as mock_respond:
        mock_respond.return_value = {"error": "Password must have uppercase characters"}
        req = _make_form_request({
            "email": "user@example.com",
            "session_id": "session-abc",
            "challenge_session": "cognito-session-xyz",
            "new_password": "weak",
        })
        resp = await auth_authenticate(req)

    body = resp.body.decode()
    assert "Password must have uppercase characters" in body
    assert "Set Your Password" in body


# ---------------------------------------------------------------------------
# GET /auth/session/<session_id>
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_auth_session_pending():
    """Polling a non-existent session returns pending."""
    _sessions.clear()
    req = _make_request(path_params={"session_id": "nonexistent"})
    resp = await auth_session(req)
    data = resp.body.decode()
    assert '"pending"' in data


@pytest.mark.asyncio
async def test_auth_session_ready():
    """Polling a ready session returns the API key."""
    _sessions.clear()
    _sessions["test-session"] = {"api_key": "cmcp_abc123", "created_at": time.time()}

    req = _make_request(path_params={"session_id": "test-session"})
    resp = await auth_session(req)
    data = resp.body.decode()
    assert '"ready"' in data
    assert "cmcp_abc123" in data

    # One-time use: session should be deleted
    assert "test-session" not in _sessions


@pytest.mark.asyncio
async def test_auth_session_ttl_expiry():
    """Expired sessions are cleaned up and return pending."""
    _sessions.clear()
    _sessions["old-session"] = {"api_key": "cmcp_old", "created_at": time.time() - SESSION_TTL - 10}

    req = _make_request(path_params={"session_id": "old-session"})
    resp = await auth_session(req)
    data = resp.body.decode()
    assert '"pending"' in data

    # Session should be cleaned up
    assert "old-session" not in _sessions


@pytest.mark.asyncio
async def test_auth_session_one_time_use():
    """After retrieving a key, subsequent polls return pending."""
    _sessions.clear()
    _sessions["once-session"] = {"api_key": "cmcp_once", "created_at": time.time()}

    req = _make_request(path_params={"session_id": "once-session"})

    # First poll: get the key
    resp1 = await auth_session(req)
    assert '"ready"' in resp1.body.decode()

    # Second poll: pending (key consumed)
    resp2 = await auth_session(req)
    assert '"pending"' in resp2.body.decode()

    _sessions.clear()
