"""Unit tests for CollectivesClient API methods."""

import httpx
import pytest

from nextcloud_mcp_server.client.collectives import CollectivesClient, OCSError
from tests.client.conftest import create_mock_response

pytestmark = pytest.mark.unit


# --- OCS mock helpers ---


def _ocs_response(data: dict | list) -> httpx.Response:
    """Wrap data in an OCS envelope and return as mock response."""
    return create_mock_response(
        status_code=200,
        json_data={"ocs": {"meta": {"status": "ok", "statuscode": 200}, "data": data}},
    )


def _sample_collective(
    collective_id: int = 1, name: str = "Test Wiki", emoji: str | None = None
) -> dict:
    return {
        "id": collective_id,
        "circleId": "circle-abc",
        "emoji": emoji,
        "name": name,
        "level": 9,
        "canEdit": True,
        "canShare": True,
        "pageMode": 0,
    }


def _sample_page(
    page_id: int = 10,
    title: str = "Test Page",
    parent_id: int = 0,
    collective_path: str = "Collectives/Test Wiki",
) -> dict:
    return {
        "id": page_id,
        "title": title,
        "emoji": None,
        "fileName": f"{title}.md",
        "filePath": "",
        "collectivePath": collective_path,
        "parentId": parent_id,
        "timestamp": 1700000000,
        "size": 42,
        "lastUserId": "testuser",
        "lastUserDisplayName": "Test User",
        "subpageOrder": [],
        "isFullWidth": False,
    }


def _sample_tag(
    tag_id: int = 1, name: str = "important", color: str = "FF0000"
) -> dict:
    return {
        "id": tag_id,
        "collectiveId": 1,
        "name": name,
        "color": color,
    }


# --- Collectives ---


async def test_get_collectives(mocker):
    """Test listing collectives unwraps OCS envelope correctly."""
    mock_response = _ocs_response(
        {
            "collectives": [
                _sample_collective(1, "Wiki A"),
                _sample_collective(2, "Wiki B"),
            ]
        }
    )
    mocker.patch.object(CollectivesClient, "_make_request", return_value=mock_response)

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.get_collectives()

    assert isinstance(result, list)
    assert len(result) == 2
    assert result[0]["id"] == 1
    assert result[1]["name"] == "Wiki B"


async def test_create_collective(mocker):
    """Test creating a collective sends name and emoji."""
    mock_response = _ocs_response(
        {"collective": _sample_collective(5, "New Wiki", "📚")}
    )
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.create_collective("New Wiki", emoji="📚")

    assert result["id"] == 5
    assert result["name"] == "New Wiki"
    assert result["emoji"] == "📚"

    call_args = mock_request.call_args
    assert call_args[0][0] == "POST"
    assert call_args[1]["json"]["name"] == "New Wiki"
    assert call_args[1]["json"]["emoji"] == "📚"


async def test_trash_collective(mocker):
    """Test trashing a collective sends DELETE to correct endpoint."""
    mock_response = _ocs_response({})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    await client.trash_collective(collective_id=5)

    call_args = mock_request.call_args
    assert call_args[0][0] == "DELETE"
    assert "/collectives/5" in call_args[0][1]
    assert "/trash" not in call_args[0][1]


async def test_delete_collective(mocker):
    """Test permanently deleting a collective sends DELETE to trash endpoint."""
    mock_response = _ocs_response({})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    await client.delete_collective(collective_id=5)

    call_args = mock_request.call_args
    assert call_args[0][0] == "DELETE"
    assert "/collectives/trash/5" in call_args[0][1]


# --- Pages ---


async def test_get_pages(mocker):
    """Test listing pages in a collective."""
    mock_response = _ocs_response(
        {"pages": [_sample_page(10, "Page A"), _sample_page(20, "Page B")]}
    )
    mocker.patch.object(CollectivesClient, "_make_request", return_value=mock_response)

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.get_pages(collective_id=1)

    assert len(result) == 2
    assert result[0]["title"] == "Page A"
    assert result[1]["id"] == 20


async def test_get_page(mocker):
    """Test getting a single page metadata."""
    mock_response = _ocs_response({"page": _sample_page(10, "My Page")})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.get_page(collective_id=1, page_id=10)

    assert result["id"] == 10
    assert result["title"] == "My Page"
    assert result["collectivePath"] == "Collectives/Test Wiki"

    call_args = mock_request.call_args
    assert "/collectives/1/pages/10" in call_args[0][1]


async def test_create_page(mocker):
    """Test creating a page under a parent."""
    mock_response = _ocs_response({"page": _sample_page(30, "New Page", parent_id=10)})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.create_page(collective_id=1, parent_id=10, title="New Page")

    assert result["id"] == 30
    assert result["parentId"] == 10

    call_args = mock_request.call_args
    assert call_args[0][0] == "POST"
    assert "/collectives/1/pages/10" in call_args[0][1]
    assert call_args[1]["json"]["title"] == "New Page"


async def test_trash_page(mocker):
    """Test trashing a page sends DELETE."""
    mock_response = _ocs_response({})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    await client.trash_page(collective_id=1, page_id=10)

    call_args = mock_request.call_args
    assert call_args[0][0] == "DELETE"
    assert "/collectives/1/pages/10" in call_args[0][1]


async def test_move_page(mocker):
    """Test moving a page sends PUT with correct params."""
    mock_response = _ocs_response(
        {"page": _sample_page(10, "Moved Page", parent_id=20)}
    )
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.move_page(
        collective_id=1, page_id=10, parent_id=20, title="Moved Page"
    )

    assert result["parentId"] == 20
    call_args = mock_request.call_args
    assert call_args[0][0] == "PUT"
    assert call_args[1]["json"]["parentId"] == 20


# --- Search ---


async def test_search_pages(mocker):
    """Test full-text search sends query parameter."""
    mock_response = _ocs_response({"pages": [_sample_page(10, "Result Page")]})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.search_pages(collective_id=1, query="test query")

    assert len(result) == 1
    assert result[0]["title"] == "Result Page"

    call_args = mock_request.call_args
    assert call_args[1]["params"]["searchString"] == "test query"


# --- Tags ---


async def test_get_tags(mocker):
    """Test listing tags."""
    mock_response = _ocs_response(
        {"tags": [_sample_tag(1, "important"), _sample_tag(2, "draft", "00FF00")]}
    )
    mocker.patch.object(CollectivesClient, "_make_request", return_value=mock_response)

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.get_tags(collective_id=1)

    assert len(result) == 2
    assert result[0]["name"] == "important"
    assert result[1]["color"] == "00FF00"


async def test_create_tag(mocker):
    """Test creating a tag."""
    mock_response = _ocs_response({"tag": _sample_tag(3, "review", "0000FF")})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.create_tag(collective_id=1, name="review", color="0000FF")

    assert result["id"] == 3
    assert result["name"] == "review"

    call_args = mock_request.call_args
    assert call_args[0][0] == "POST"
    assert call_args[1]["json"]["name"] == "review"


async def test_assign_tag(mocker):
    """Test assigning a tag to a page."""
    mock_response = _ocs_response({})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    await client.assign_tag(collective_id=1, page_id=10, tag_id=3)

    call_args = mock_request.call_args
    assert call_args[0][0] == "PUT"
    assert "/pages/10/tags/3" in call_args[0][1]


async def test_remove_tag(mocker):
    """Test removing a tag from a page."""
    mock_response = _ocs_response({})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    await client.remove_tag(collective_id=1, page_id=10, tag_id=3)

    call_args = mock_request.call_args
    assert call_args[0][0] == "DELETE"
    assert "/pages/10/tags/3" in call_args[0][1]


# --- Trash ---


async def test_get_trashed_pages(mocker):
    """Test listing trashed pages."""
    trashed = _sample_page(10, "Trashed Page")
    trashed["trashTimestamp"] = 1700000000
    mock_response = _ocs_response({"pages": [trashed]})
    mocker.patch.object(CollectivesClient, "_make_request", return_value=mock_response)

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.get_trashed_pages(collective_id=1)

    assert len(result) == 1
    assert result[0]["title"] == "Trashed Page"


async def test_restore_page(mocker):
    """Test restoring a page from trash."""
    mock_response = _ocs_response({"page": _sample_page(10, "Restored Page")})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.restore_page(collective_id=1, page_id=10)

    assert result["title"] == "Restored Page"
    call_args = mock_request.call_args
    assert call_args[0][0] == "PATCH"
    assert "/pages/trash/10" in call_args[0][1]


# --- Error Handling ---


async def test_ocs_missing_data_raises_ocs_error(mocker):
    """Test that OCS envelope without 'data' key raises OCSError."""
    mock_response = create_mock_response(
        status_code=200,
        json_data={
            "ocs": {
                "meta": {"status": "ok", "statuscode": 200},
            }
        },
    )
    mocker.patch.object(CollectivesClient, "_make_request", return_value=mock_response)

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    with pytest.raises(OCSError, match="missing 'data' field"):
        await client.get_collectives()


async def test_non_ocs_envelope_raises_ocs_error(mocker):
    """Test that a non-OCS response (e.g. proxy error) raises OCSError."""
    mock_response = create_mock_response(
        status_code=200,
        json_data={"error": "Bad Gateway"},
    )
    mocker.patch.object(CollectivesClient, "_make_request", return_value=mock_response)

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    with pytest.raises(OCSError, match="not an OCS envelope"):
        await client.get_collectives()


async def test_ocs_error_status_raises(mocker):
    """Test that OCS envelope with error statuscode raises OCSError."""
    mock_response = create_mock_response(
        status_code=200,
        json_data={
            "ocs": {
                "meta": {
                    "status": "failure",
                    "statuscode": 403,
                    "message": "Not permitted",
                },
                "data": {},
            }
        },
    )
    mocker.patch.object(CollectivesClient, "_make_request", return_value=mock_response)

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    with pytest.raises(OCSError, match="Not permitted"):
        await client.get_collectives()


async def test_get_collectives_403(mocker):
    """Test 403 response raises HTTPStatusError."""
    mock_response = create_mock_response(
        status_code=403, json_data={"message": "Forbidden"}
    )
    mock_response.raise_for_status = lambda: (_ for _ in ()).throw(
        httpx.HTTPStatusError(
            "Forbidden", request=mock_response.request, response=mock_response
        )
    )
    mocker.patch.object(
        CollectivesClient,
        "_make_request",
        side_effect=httpx.HTTPStatusError(
            "Forbidden",
            request=httpx.Request("GET", "http://test"),
            response=mock_response,
        ),
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    with pytest.raises(httpx.HTTPStatusError):
        await client.get_collectives()


async def test_get_page_404(mocker):
    """Test 404 response raises HTTPStatusError."""
    mock_response = create_mock_response(
        status_code=404, json_data={"message": "Not found"}
    )
    mocker.patch.object(
        CollectivesClient,
        "_make_request",
        side_effect=httpx.HTTPStatusError(
            "Not Found",
            request=httpx.Request("GET", "http://test"),
            response=mock_response,
        ),
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    with pytest.raises(httpx.HTTPStatusError):
        await client.get_page(collective_id=1, page_id=999)


# --- Additional coverage ---


async def test_update_collective_no_fields_raises_value_error(mocker):
    """Test that update_collective raises ValueError when called with no fields."""
    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    # Omitting emoji entirely (not passing it) should raise
    with pytest.raises(ValueError, match="At least one field"):
        await client.update_collective(collective_id=1)


async def test_update_collective_clear_emoji(mocker):
    """Test that update_collective sends null emoji to clear it."""
    mock_response = _ocs_response(
        {"collective": _sample_collective(1, "Test Wiki", emoji=None)}
    )
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.update_collective(collective_id=1, emoji=None)

    assert result["emoji"] is None
    call_args = mock_request.call_args
    assert call_args[1]["json"] == {"emoji": None}


async def test_set_page_emoji_clear(mocker):
    """Test that set_page_emoji sends null emoji to clear it."""
    page = _sample_page(10, "Test Page")
    page["emoji"] = None
    mock_response = _ocs_response({"page": page})
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.set_page_emoji(collective_id=1, page_id=10, emoji=None)

    assert result["emoji"] is None
    call_args = mock_request.call_args
    assert call_args[1]["json"] == {"emoji": None}


async def test_get_trashed_collectives(mocker):
    """Test listing trashed collectives."""
    mock_response = _ocs_response(
        {"collectives": [_sample_collective(1, "Trashed Wiki")]}
    )
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.get_trashed_collectives()

    assert len(result) == 1
    assert result[0]["name"] == "Trashed Wiki"
    call_args = mock_request.call_args
    assert "/collectives/trash" in call_args[0][1]
    assert call_args[0][0] == "GET"


async def test_restore_collective(mocker):
    """Test restoring a collective from trash."""
    mock_response = _ocs_response(
        {"collective": _sample_collective(5, "Restored Wiki")}
    )
    mock_request = mocker.patch.object(
        CollectivesClient, "_make_request", return_value=mock_response
    )

    client = CollectivesClient(mocker.AsyncMock(spec=httpx.AsyncClient), "testuser")
    result = await client.restore_collective(collective_id=5)

    assert result["name"] == "Restored Wiki"
    call_args = mock_request.call_args
    assert call_args[0][0] == "PATCH"
    assert "/collectives/trash/5" in call_args[0][1]
