"""Unit tests for the ``/webhooks/nextcloud`` HTTP receiver.

Builds a minimal Starlette app around ``handle_nextcloud_webhook`` so we can
drive it with ``TestClient`` without standing up the full FastMCP server.
"""

import anyio
import pytest
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient

from nextcloud_mcp_server.config import Settings
from nextcloud_mcp_server.vector import webhook_receiver
from nextcloud_mcp_server.vector.webhook_receiver import handle_nextcloud_webhook

pytestmark = pytest.mark.unit


@pytest.fixture(autouse=True)
def _reset_warned_flag():
    """The receiver warns once per process when WEBHOOK_SECRET is missing.
    Reset between tests so each gets a clean slate."""
    webhook_receiver._warned_about_missing_secret = False
    yield
    webhook_receiver._warned_about_missing_secret = False


def _patch_secret(monkeypatch, secret: str | None) -> None:
    """Make ``get_settings()`` (as called inside the receiver) return a
    Settings instance with the given ``webhook_secret``."""
    monkeypatch.setattr(
        webhook_receiver,
        "get_settings",
        lambda: Settings(webhook_secret=secret),
    )


def _make_app(send_stream=None) -> Starlette:
    app = Starlette(
        routes=[
            Route("/webhooks/nextcloud", handle_nextcloud_webhook, methods=["POST"])
        ]
    )
    app.state.document_send_stream = send_stream
    return app


_NOTE_CREATED = {
    "user": {"uid": "admin", "displayName": "admin"},
    "time": 1762850245,
    "event": {
        "class": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
        "node": {
            "id": 437,
            "path": "/admin/files/Notes/Webhooks/Webhook Test Note.md",
        },
    },
}


_NOTE_DELETED = {
    "user": {"uid": "alice"},
    "time": 1762851093,
    "event": {
        "class": "OCP\\Files\\Events\\Node\\BeforeNodeDeletedEvent",
        "node": {"id": 99, "path": "/alice/files/Notes/foo.md"},
    },
}


def test_index_event_queues_task_and_returns_200():
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=_NOTE_CREATED)

    assert response.status_code == 200
    assert response.json()["status"] == "queued"
    assert response.json()["operation"] == "index"
    assert response.json()["doc_id"] == "437"

    task = receive_stream.receive_nowait()
    assert task.user_id == "admin"
    assert task.doc_id == "437"
    assert task.operation == "index"
    assert task.doc_type == "note"


def test_delete_event_queues_delete_task():
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=_NOTE_DELETED)

    assert response.status_code == 200
    assert response.json()["operation"] == "delete"

    task = receive_stream.receive_nowait()
    assert task.operation == "delete"
    assert task.doc_id == "99"
    assert task.user_id == "alice"


def test_unsupported_event_is_ignored():
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    payload = {
        "user": {"uid": "admin"},
        "time": 1,
        "event": {
            "class": "OCP\\Calendar\\Events\\CalendarObjectCreatedEvent",
            "objectData": {"id": 7},
        },
    }

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=payload)

    assert response.status_code == 200
    assert response.json()["status"] == "ignored"

    with pytest.raises(anyio.WouldBlock):
        receive_stream.receive_nowait()


def test_invalid_json_returns_400():
    app = _make_app(send_stream=None)

    with TestClient(app) as client:
        response = client.post(
            "/webhooks/nextcloud",
            content=b"not json",
            headers={"content-type": "application/json"},
        )

    assert response.status_code == 400
    assert response.json()["status"] == "error"


def test_returns_503_when_send_stream_not_wired():
    """Vector sync not running → tell NC to retry instead of dropping the
    event."""
    app = _make_app(send_stream=None)

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=_NOTE_CREATED)

    assert response.status_code == 503
    assert response.json()["status"] == "unavailable"


def test_returns_500_when_stream_is_closed():
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=1)
    receive_stream.close()  # close receiver → send raises BrokenResourceError
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=_NOTE_CREATED)

    assert response.status_code == 500
    assert response.json()["status"] == "error"


def test_returns_503_when_queue_is_full(monkeypatch):
    """When the processor queue is saturated, the handler must time out
    quickly with 503 instead of pinning until NC's outbound timeout fires."""
    # Speed up the test — a 1s deadline matches production but is overkill
    # for a unit test that's specifically exercising the timeout branch.
    # Capture the original BEFORE patching so the override doesn't recurse
    # into itself (the receiver imports the same anyio module object).
    real_fail_after = anyio.fail_after
    monkeypatch.setattr(
        "nextcloud_mcp_server.vector.webhook_receiver.anyio.fail_after",
        lambda _seconds: real_fail_after(0.05),
    )

    # Buffer of 1, no consumer → first send fills it, second blocks.
    send_stream, _receive_stream = anyio.create_memory_object_stream(max_buffer_size=1)
    send_stream.send_nowait("sentinel")  # type: ignore[arg-type]
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=_NOTE_CREATED)

    assert response.status_code == 503
    body = response.json()
    assert body["status"] == "unavailable"
    assert body["reason"] == "queue full"


# --- WEBHOOK_SECRET authentication ---------------------------------------


def test_secret_set_valid_bearer_header_queues_task(monkeypatch):
    _patch_secret(monkeypatch, "supersecret")
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post(
            "/webhooks/nextcloud",
            json=_NOTE_CREATED,
            headers={"Authorization": "Bearer supersecret"},
        )

    assert response.status_code == 200
    assert response.json()["status"] == "queued"
    assert receive_stream.receive_nowait().doc_id == "437"


def test_secret_set_missing_authorization_returns_401(monkeypatch):
    _patch_secret(monkeypatch, "supersecret")
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=_NOTE_CREATED)

    assert response.status_code == 401
    assert response.json()["status"] == "unauthorized"
    with pytest.raises(anyio.WouldBlock):
        receive_stream.receive_nowait()


def test_secret_set_wrong_secret_returns_401(monkeypatch):
    _patch_secret(monkeypatch, "supersecret")
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post(
            "/webhooks/nextcloud",
            json=_NOTE_CREATED,
            headers={"Authorization": "Bearer wrong"},
        )

    assert response.status_code == 401
    with pytest.raises(anyio.WouldBlock):
        receive_stream.receive_nowait()


def test_secret_set_wrong_scheme_returns_401(monkeypatch):
    """A token without the Bearer prefix is rejected."""
    _patch_secret(monkeypatch, "supersecret")
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post(
            "/webhooks/nextcloud",
            json=_NOTE_CREATED,
            headers={"Authorization": "supersecret"},
        )

    assert response.status_code == 401


def test_secret_unset_accepts_unauthenticated(monkeypatch):
    """Backward compat: deployments that haven't yet set WEBHOOK_SECRET keep
    working — the receiver accepts unauthenticated POSTs and logs a one-time
    warning."""
    _patch_secret(monkeypatch, None)
    send_stream, receive_stream = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post("/webhooks/nextcloud", json=_NOTE_CREATED)

    assert response.status_code == 200
    assert receive_stream.receive_nowait().doc_id == "437"


def test_compare_digest_is_called_with_bytes(monkeypatch, mocker):
    """Regression: secret comparison must run on bytes, not strings, so
    that future non-ASCII secret support doesn't depend on Python's
    implicit ASCII encoding."""
    _patch_secret(monkeypatch, "supersecret")
    spy = mocker.spy(webhook_receiver.hmac, "compare_digest")

    send_stream, _receive = anyio.create_memory_object_stream(max_buffer_size=4)
    app = _make_app(send_stream=send_stream)

    with TestClient(app) as client:
        response = client.post(
            "/webhooks/nextcloud",
            json=_NOTE_CREATED,
            headers={"Authorization": "Bearer supersecret"},
        )

    assert response.status_code == 200
    assert spy.call_count == 1
    provided_arg, expected_arg = spy.call_args.args
    assert isinstance(provided_arg, bytes)
    assert isinstance(expected_arg, bytes)
    assert expected_arg == b"Bearer supersecret"
