"""Unit tests for the MCP elicitation helpers."""

from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from nextcloud_mcp_server.auth.elicitation import (
    ASTROLABE_SETTINGS_PATH,
    _astrolabe_settings_url,
    present_provisioning_required,
)

pytestmark = pytest.mark.unit


def _fake_settings(
    public_issuer_url: str | None = None, host: str | None = None
) -> SimpleNamespace:
    """Build a Settings-shaped object exposing only the fields elicitation reads."""
    return SimpleNamespace(
        nextcloud_public_issuer_url=public_issuer_url,
        nextcloud_host=host,
    )


def test_astrolabe_settings_url_prefers_public_issuer():
    """Public issuer wins over host so the link is browser-reachable in Docker."""
    fake = _fake_settings(
        public_issuer_url="https://nc.example.com", host="http://internal:8080"
    )
    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        assert (
            _astrolabe_settings_url()
            == f"https://nc.example.com{ASTROLABE_SETTINGS_PATH}"
        )


def test_astrolabe_settings_url_strips_trailing_slash_from_public_issuer():
    """Trailing slash on nextcloud_public_issuer_url is normalized."""
    fake = _fake_settings(public_issuer_url="https://nc.example.com/")
    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        assert (
            _astrolabe_settings_url()
            == f"https://nc.example.com{ASTROLABE_SETTINGS_PATH}"
        )


def test_astrolabe_settings_url_falls_back_to_host():
    """When only nextcloud_host is set, use it (and strip a trailing slash)."""
    fake = _fake_settings(host="https://only-host.example.com/")
    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        assert (
            _astrolabe_settings_url()
            == f"https://only-host.example.com{ASTROLABE_SETTINGS_PATH}"
        )


def test_astrolabe_settings_url_returns_none_when_unset():
    """No NC URL configured → None (caller renders the tool-only message)."""
    fake = _fake_settings()
    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        assert _astrolabe_settings_url() is None


def test_astrolabe_settings_url_returns_none_when_scheme_missing(caplog):
    """Bare hostname (no http:// or https://) → None + a warning so the operator
    sees the misconfiguration instead of getting a silently-broken URL."""
    fake = _fake_settings(host="internal-host:8080")
    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        with caplog.at_level("WARNING", logger="nextcloud_mcp_server.auth.elicitation"):
            assert _astrolabe_settings_url() is None
    assert any(
        "missing an http:// or https://" in rec.message for rec in caplog.records
    ), (
        f"expected scheme-missing warning, got records={[r.message for r in caplog.records]}"
    )


async def test_present_provisioning_required_elicits_with_url():
    """When NC URL is set and the client supports elicitation, send the URL."""
    fake = _fake_settings(public_issuer_url="https://nc.example.com")
    ctx = MagicMock()
    ctx.elicit = AsyncMock(return_value=SimpleNamespace(action="accept", data=None))

    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        result = await present_provisioning_required(ctx)

    assert result == "accepted"
    ctx.elicit.assert_awaited_once()
    sent_message = ctx.elicit.await_args.kwargs["message"]
    assert "https://nc.example.com/index.php/apps/astrolabe/settings" in sent_message
    assert "nc_auth_provision_access" in sent_message


async def test_present_provisioning_required_without_url():
    """When neither NC URL is set, fall back to the tool-only message."""
    fake = _fake_settings()
    ctx = MagicMock()
    ctx.elicit = AsyncMock(return_value=SimpleNamespace(action="accept", data=None))

    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        result = await present_provisioning_required(ctx)

    assert result == "accepted"
    sent_message = ctx.elicit.await_args.kwargs["message"]
    assert "astrolabe" not in sent_message.lower()
    assert "nc_auth_provision_access" in sent_message


async def test_present_provisioning_required_no_elicit_method():
    """Contexts that don't expose ctx.elicit fall back to message_only."""

    class _NoElicit:
        pass

    ctx = _NoElicit()

    fake = _fake_settings()
    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        result = await present_provisioning_required(ctx)  # type: ignore[arg-type]

    assert result == "message_only"


async def test_present_provisioning_required_handles_not_implemented():
    """SDK clients that don't support elicitation raise NotImplementedError."""
    fake = _fake_settings()
    ctx = MagicMock()
    ctx.elicit = AsyncMock(side_effect=NotImplementedError("client lacks elicit"))

    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        result = await present_provisioning_required(ctx)

    assert result == "message_only"


async def test_present_provisioning_required_handles_unexpected_error():
    """Any other elicit failure (e.g. transport) is fail-open to message_only."""
    fake = _fake_settings()
    ctx = MagicMock()
    ctx.elicit = AsyncMock(side_effect=RuntimeError("transport boom"))

    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        result = await present_provisioning_required(ctx)

    assert result == "message_only"


async def test_present_provisioning_required_decline_returns_declined():
    """User chose 'decline' on the prompt → propagate that to the caller."""
    fake = _fake_settings()
    ctx = MagicMock()
    ctx.elicit = AsyncMock(return_value=SimpleNamespace(action="decline", data=None))

    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        result = await present_provisioning_required(ctx)

    assert result == "declined"


async def test_present_provisioning_required_cancel_returns_cancelled():
    """User chose 'cancel' on the prompt → propagate that to the caller."""
    fake = _fake_settings()
    ctx = MagicMock()
    ctx.elicit = AsyncMock(return_value=SimpleNamespace(action="cancel", data=None))

    with patch("nextcloud_mcp_server.auth.elicitation.get_settings", return_value=fake):
        result = await present_provisioning_required(ctx)

    assert result == "cancelled"
