"""Unit tests for Login Flow v2 HTTP client.

Tests the LoginFlowV2Client with mocked HTTP responses for:
- Flow initiation (POST /index.php/login/v2)
- Flow polling (completed, pending, expired)
- Error handling
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from nextcloud_mcp_server.auth.login_flow import (
    LoginFlowInitResponse,
    LoginFlowPollResult,
    LoginFlowV2Client,
    rewrite_url_origin,
)

pytestmark = pytest.mark.unit


@pytest.fixture
def flow_client():
    """Create a LoginFlowV2Client for testing."""
    return LoginFlowV2Client(
        nextcloud_host="https://cloud.example.com",
        verify_ssl=False,
    )


def _mock_response(status_code: int, json_data: dict) -> MagicMock:
    """Create a mock httpx response."""
    response = MagicMock()
    response.status_code = status_code
    response.json.return_value = json_data
    response.raise_for_status = MagicMock()
    if status_code >= 400:
        from httpx import HTTPStatusError

        response.raise_for_status.side_effect = HTTPStatusError(
            "error", request=MagicMock(), response=response
        )
    return response


async def test_initiate_success(flow_client):
    """Test successful Login Flow v2 initiation."""
    mock_response = _mock_response(
        200,
        {
            "login": "https://cloud.example.com/login/v2/grant?token=abc123",
            "poll": {
                "endpoint": "https://cloud.example.com/login/v2/poll",
                "token": "secret-poll-token",
            },
        },
    )

    mock_client = AsyncMock()
    mock_client.post.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch(
        "nextcloud_mcp_server.auth.login_flow.nextcloud_httpx_client",
        return_value=mock_client,
    ):
        result = await flow_client.initiate()

    assert isinstance(result, LoginFlowInitResponse)
    assert result.login_url == "https://cloud.example.com/login/v2/grant?token=abc123"
    assert result.poll_endpoint == "https://cloud.example.com/login/v2/poll"
    assert result.poll_token == "secret-poll-token"


async def test_poll_completed(flow_client):
    """Test polling when user has completed login."""
    mock_response = _mock_response(
        200,
        {
            "server": "https://cloud.example.com",
            "loginName": "alice",
            "appPassword": "aaaaa-bbbbb-ccccc-ddddd-eeeee",
        },
    )

    mock_client = AsyncMock()
    mock_client.post.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch(
        "nextcloud_mcp_server.auth.login_flow.nextcloud_httpx_client",
        return_value=mock_client,
    ):
        result = await flow_client.poll(
            poll_endpoint="https://cloud.example.com/login/v2/poll",
            poll_token="secret-poll-token",
        )

    assert isinstance(result, LoginFlowPollResult)
    assert result.status == "completed"
    assert result.server == "https://cloud.example.com"
    assert result.login_name == "alice"
    assert result.app_password == "aaaaa-bbbbb-ccccc-ddddd-eeeee"


async def test_poll_pending(flow_client):
    """Test polling when login is still pending."""
    mock_response = _mock_response(404, {})

    mock_client = AsyncMock()
    mock_client.post.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch(
        "nextcloud_mcp_server.auth.login_flow.nextcloud_httpx_client",
        return_value=mock_client,
    ):
        result = await flow_client.poll(
            poll_endpoint="https://cloud.example.com/login/v2/poll",
            poll_token="secret-poll-token",
        )

    assert result.status == "pending"
    assert result.server is None
    assert result.app_password is None


async def test_poll_expired(flow_client):
    """Test polling when flow has expired."""
    mock_response = _mock_response(403, {})

    mock_client = AsyncMock()
    mock_client.post.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch(
        "nextcloud_mcp_server.auth.login_flow.nextcloud_httpx_client",
        return_value=mock_client,
    ):
        result = await flow_client.poll(
            poll_endpoint="https://cloud.example.com/login/v2/poll",
            poll_token="expired-token",
        )

    assert result.status == "expired"
    assert result.app_password is None


async def test_initiate_rewrites_login_url_to_public_host():
    """When server↔Nextcloud uses an internal host (e.g. the ``app`` Docker
    service), the browser-facing login URL must be rewritten to the configured
    public host; the poll endpoint stays on the internal host for server-side
    polling. Mock URLs use https to match this file's convention (the rewrite
    is scheme-agnostic, so this exercises the same origin-replacement logic)."""
    client = LoginFlowV2Client(
        nextcloud_host="https://nc-internal.test",  # server↔Nextcloud origin
        verify_ssl=False,
        public_host="https://cloud.example.com",  # browser-reachable origin
    )
    mock_response = _mock_response(
        200,
        {
            # Nextcloud builds these from the request (internal) host.
            "login": "https://nc-internal.test/login/v2/flow/tok123",
            "poll": {
                "endpoint": "https://nc-internal.test/login/v2/poll",
                "token": "tok",  # value irrelevant here; this test asserts the URLs
            },
        },
    )
    mock_client = AsyncMock()
    mock_client.post.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch(
        "nextcloud_mcp_server.auth.login_flow.nextcloud_httpx_client",
        return_value=mock_client,
    ):
        result = await client.initiate()

    # Browser-facing URL uses the public host...
    assert result.login_url == "https://cloud.example.com/login/v2/flow/tok123"
    # ...while the poll endpoint stays on the internal host (server polls it).
    assert result.poll_endpoint == "https://nc-internal.test/login/v2/poll"


async def test_initiate_with_custom_user_agent(flow_client):
    """Test that custom user agent is passed in the request."""
    mock_response = _mock_response(
        200,
        {
            "login": "https://cloud.example.com/login/v2/grant?token=abc",
            "poll": {
                "endpoint": "https://cloud.example.com/login/v2/poll",
                "token": "tok",
            },
        },
    )

    mock_client = AsyncMock()
    mock_client.post.return_value = mock_response
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=False)

    with patch(
        "nextcloud_mcp_server.auth.login_flow.nextcloud_httpx_client",
        return_value=mock_client,
    ):
        await flow_client.initiate(user_agent="my-custom-agent")

    # Verify the user agent was passed
    call_kwargs = mock_client.post.call_args
    assert call_kwargs.kwargs["headers"]["User-Agent"] == "my-custom-agent"


async def test_login_flow_init_response_model():
    """Test LoginFlowInitResponse Pydantic model validation."""
    resp = LoginFlowInitResponse(
        login_url="https://cloud.example.com/login",
        poll_endpoint="https://cloud.example.com/poll",
        poll_token="token123",
    )
    assert resp.login_url == "https://cloud.example.com/login"
    assert resp.poll_endpoint == "https://cloud.example.com/poll"
    assert resp.poll_token == "token123"


async def test_login_flow_poll_result_model():
    """Test LoginFlowPollResult Pydantic model validation."""
    # Completed result
    completed = LoginFlowPollResult(
        status="completed",
        server="https://cloud.example.com",
        login_name="bob",
        app_password="xxxxx-yyyyy-zzzzz-aaaaa-bbbbb",
    )
    assert completed.status == "completed"
    assert completed.login_name == "bob"

    # Pending result
    pending = LoginFlowPollResult(status="pending")
    assert pending.status == "pending"
    assert pending.server is None
    assert pending.app_password is None


# ── rewrite_url_origin tests ─────────────────────────────────────────────


async def test_rewrite_url_origin_basic():
    """Test basic origin rewriting."""
    result = rewrite_url_origin(
        "http://localhost/login/v2/poll", "https://cloud.example.com"
    )
    assert result == "https://cloud.example.com/login/v2/poll"


async def test_rewrite_url_origin_preserves_port():
    """Test that port in target_host is preserved."""
    result = rewrite_url_origin("http://localhost/path", "http://app:8080")
    assert result == "http://app:8080/path"


async def test_rewrite_url_origin_preserves_query():
    """Test that query string and fragment are preserved."""
    result = rewrite_url_origin(
        "http://internal/path?token=abc&foo=bar#section",
        "https://public.example.com",
    )
    assert result == "https://public.example.com/path?token=abc&foo=bar#section"


async def test_rewrite_url_origin_noop_when_same():
    """Test that rewriting to the same origin is a no-op."""
    url = "https://cloud.example.com/login/v2/poll"
    result = rewrite_url_origin(url, "https://cloud.example.com")
    assert result == url
