"""Unit tests for FabricItemService."""

from unittest.mock import Mock, call

import pytest

from ms_fabric_mcp_server.client.exceptions import (
    FabricAPIError,
    FabricError,
    FabricItemNotFoundError,
    FabricValidationError,
)
from ms_fabric_mcp_server.models.item import FabricItem
from ms_fabric_mcp_server.models.lakehouse import FabricLakehouse
from tests.fixtures.mocks import FabricDataFactory, MockResponseFactory


@pytest.fixture
def item_service(mock_fabric_client):
    from ms_fabric_mcp_server.services.item import FabricItemService

    return FabricItemService(mock_fabric_client)


@pytest.mark.unit
class TestFabricItemService:
    """Test suite for FabricItemService."""

    def test_validate_item_type_success(self, item_service):
        """Known item types are accepted."""
        item_service._validate_item_type("Notebook")

    def test_validate_item_type_invalid(self, item_service):
        """Unknown item types raise validation error."""
        with pytest.raises(FabricValidationError):
            item_service._validate_item_type("NotAType")

    def test_list_items_success(self, item_service, mock_fabric_client):
        """List items returns FabricItem objects."""
        items_data = FabricDataFactory.item_list(2, "Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(items_data)

        items = item_service.list_items("ws-1", "Notebook")

        assert len(items) == 2
        assert all(isinstance(item, FabricItem) for item in items)
        mock_fabric_client.make_api_request.assert_called_once_with(
            "GET", "workspaces/ws-1/items?type=Notebook"
        )

    def test_list_items_without_type(self, item_service, mock_fabric_client):
        """List items without type omits query filter."""
        items_data = FabricDataFactory.item_list(1, "Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(items_data)

        items = item_service.list_items("ws-1")

        assert len(items) == 1
        mock_fabric_client.make_api_request.assert_called_once_with(
            "GET", "workspaces/ws-1/items"
        )

    def test_list_items_with_folder_scope(self, item_service, mock_fabric_client):
        """List items with folder scope includes folder and recursion params."""
        items_data = FabricDataFactory.item_list(1, "Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(items_data)

        items = item_service.list_items(
            "ws-1", "Notebook", root_folder_id="folder-1", recursive=False
        )

        assert len(items) == 1
        mock_fabric_client.make_api_request.assert_called_once_with(
            "GET", "workspaces/ws-1/items?type=Notebook&rootFolderId=folder-1&recursive=false"
        )

    def test_list_items_invalid_type(self, item_service):
        """Invalid type filter raises validation error."""
        with pytest.raises(FabricValidationError):
            item_service.list_items("ws-1", "NotAType")

    def test_resolve_folder_id_from_path_creates_missing(
        self, item_service, mock_fabric_client
    ):
        """Resolve folder path creates missing folders when requested."""
        existing = {
            "value": [
                {"id": "f-root", "displayName": "Root", "parentFolderId": None},
            ],
            "continuationToken": None,
        }
        created = {"id": "f-child", "displayName": "Child", "parentFolderId": "f-root"}
        mock_fabric_client.make_api_request.side_effect = [
            MockResponseFactory.success(existing),
            MockResponseFactory.success(created, status_code=201),
        ]

        folder_id = item_service.resolve_folder_id_from_path(
            "ws-1", "Root/Child", create_missing=True
        )

        assert folder_id == "f-child"
        assert mock_fabric_client.make_api_request.call_count == 2

    def test_resolve_folder_id_from_path_missing_raises(
        self, item_service, mock_fabric_client
    ):
        """Missing folder path raises validation error when not creating."""
        existing = {"value": [], "continuationToken": None}
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(existing)

        with pytest.raises(FabricValidationError):
            item_service.resolve_folder_id_from_path("ws-1", "Missing/Path", create_missing=False)

    def test_get_item_by_name_success(self, item_service, mock_fabric_client):
        """Get item by name returns matching item."""
        items_data = FabricDataFactory.item_list(2, "Notebook")
        items_data["value"][1]["displayName"] = "Target"
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(items_data)

        item = item_service.get_item_by_name("ws-1", "Target", "Notebook")

        assert item.display_name == "Target"

    def test_get_item_by_name_not_found(self, item_service, mock_fabric_client):
        """Missing item raises FabricItemNotFoundError."""
        items_data = FabricDataFactory.item_list(2, "Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(items_data)

        with pytest.raises(FabricItemNotFoundError):
            item_service.get_item_by_name("ws-1", "Missing", "Notebook")

    def test_get_item_by_id_success(self, item_service, mock_fabric_client):
        """Get item by ID maps response to FabricItem."""
        item_data = FabricDataFactory.item(item_id="item-1", item_type="Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(item_data)

        item = item_service.get_item_by_id("ws-1", "item-1")

        assert item.id == "item-1"
        assert item.type == "Notebook"

    def test_get_item_by_id_not_found(self, item_service, mock_fabric_client):
        """404 response raises FabricItemNotFoundError."""
        mock_fabric_client.make_api_request.side_effect = FabricAPIError(404, "missing")

        with pytest.raises(FabricItemNotFoundError):
            item_service.get_item_by_id("ws-1", "missing")

    def test_get_item_by_id_unexpected_error(self, item_service, mock_fabric_client):
        """Unexpected errors raise FabricError."""
        mock_fabric_client.make_api_request.side_effect = RuntimeError("boom")

        with pytest.raises(FabricError):
            item_service.get_item_by_id("ws-1", "item-1")

    def test_get_item_definition_success(self, item_service, mock_fabric_client):
        """Definition response is returned."""
        definition = {"definition": {"parts": []}}
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(definition)

        result = item_service.get_item_definition("ws-1", "item-1")

        assert result == definition

    def test_get_item_definition_unexpected_error(self, item_service, mock_fabric_client):
        """Unexpected errors raise FabricError."""
        mock_fabric_client.make_api_request.side_effect = RuntimeError("boom")

        with pytest.raises(FabricError):
            item_service.get_item_definition("ws-1", "item-1")

    def test_get_item_definition_api_error_propagates(self, item_service, mock_fabric_client):
        """API errors are re-raised."""
        mock_fabric_client.make_api_request.side_effect = FabricAPIError(500, "boom")

        with pytest.raises(FabricAPIError):
            item_service.get_item_definition("ws-1", "item-1")

    def test_create_item_validation_missing_field(self, item_service):
        """Missing required fields raise validation error."""
        with pytest.raises(FabricValidationError):
            item_service.create_item("ws-1", {"type": "Notebook"})

    def test_create_item_validation_missing_type(self, item_service):
        """Missing type raises validation error."""
        with pytest.raises(FabricValidationError):
            item_service.create_item("ws-1", {"displayName": "Item"})

    def test_create_item_success_201(self, item_service, mock_fabric_client):
        """Create item maps 201 response."""
        item_data = FabricDataFactory.item(item_id="item-1", item_type="Notebook")
        response = MockResponseFactory.success(item_data, status_code=201)
        mock_fabric_client.make_api_request.return_value = response

        item = item_service.create_item("ws-1", {"displayName": "Item", "type": "Notebook"})

        assert item.id == "item-1"
        assert item.type == "Notebook"

    def test_create_item_accepted_202_fetches_item(self, item_service, mock_fabric_client):
        """Async create with id fetches item by ID."""
        response = MockResponseFactory.success({"id": "item-1"}, status_code=202)
        mock_fabric_client.make_api_request.return_value = response
        item_service.get_item_by_id = Mock(
            return_value=FabricItem(
                id="item-1",
                display_name="Item",
                type="Notebook",
                workspace_id="ws-1",
            )
        )

        item = item_service.create_item("ws-1", {"displayName": "Item", "type": "Notebook"})

        assert item.id == "item-1"
        item_service.get_item_by_id.assert_called_once_with("ws-1", "item-1")

    def test_create_item_accepted_202_without_id_raises(self, item_service, mock_fabric_client):
        """Async create without id raises FabricError."""
        response = MockResponseFactory.success({}, status_code=202)
        mock_fabric_client.make_api_request.return_value = response
        item_service.get_item_by_id = Mock()

        with pytest.raises(FabricError):
            item_service.create_item(
                "ws-1",
                {"displayName": "Item", "type": "Notebook", "description": "desc"},
            )

        item_service.get_item_by_id.assert_not_called()

    def test_create_item_unexpected_status_raises(self, item_service, mock_fabric_client):
        """Unexpected status codes raise FabricAPIError."""
        response = MockResponseFactory.success({}, status_code=500)
        mock_fabric_client.make_api_request.return_value = response

        with pytest.raises(FabricAPIError):
            item_service.create_item("ws-1", {"displayName": "Item", "type": "Notebook"})

    def test_update_item_success(self, item_service, mock_fabric_client):
        """Update item maps response."""
        item_data = FabricDataFactory.item(item_id="item-1", item_type="Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(item_data)

        item = item_service.update_item("ws-1", "item-1", {"displayName": "Updated"})

        assert item.id == "item-1"
        assert item.display_name == item_data["displayName"]
        mock_fabric_client.make_api_request.assert_called_once_with(
            "PATCH",
            "workspaces/ws-1/items/item-1",
            payload={"displayName": "Updated"},
        )

    def test_update_item_api_error(self, item_service, mock_fabric_client):
        """API errors propagate."""
        mock_fabric_client.make_api_request.side_effect = FabricAPIError(500, "boom")

        with pytest.raises(FabricAPIError):
            item_service.update_item("ws-1", "item-1", {"displayName": "Updated"})

    def test_update_item_unexpected_error(self, item_service, mock_fabric_client):
        """Unexpected errors raise FabricError."""
        mock_fabric_client.make_api_request.side_effect = RuntimeError("boom")

        with pytest.raises(FabricError):
            item_service.update_item("ws-1", "item-1", {"displayName": "Updated"})

    def test_rename_item_success(self, item_service, mock_fabric_client):
        """Rename item uses update endpoint."""
        item_data = FabricDataFactory.item(item_id="item-1", item_type="Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(item_data)

        item = item_service.rename_item(
            "ws-1", "item-1", new_display_name="Renamed", description="desc"
        )

        assert item.id == "item-1"
        mock_fabric_client.make_api_request.assert_called_once_with(
            "PATCH",
            "workspaces/ws-1/items/item-1",
            payload={"displayName": "Renamed", "description": "desc"},
        )

    def test_rename_item_validation(self, item_service):
        """Empty name raises validation error."""
        with pytest.raises(FabricValidationError):
            item_service.rename_item("ws-1", "item-1", " ")

    def test_move_item_to_folder_success(self, item_service, mock_fabric_client):
        """Move item posts to move endpoint."""
        item_data = FabricDataFactory.item(item_id="item-1", item_type="Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(item_data)

        item = item_service.move_item_to_folder("ws-1", "item-1", "folder-1")

        assert item.id == "item-1"
        mock_fabric_client.make_api_request.assert_called_once_with(
            "POST",
            "workspaces/ws-1/items/item-1/move",
            payload={"targetFolderId": "folder-1"},
            wait_for_lro=True,
        )

    def test_move_item_to_root_success(self, item_service, mock_fabric_client):
        """Move item to root sends null target folder id."""
        item_data = FabricDataFactory.item(item_id="item-1", item_type="Notebook")
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(item_data)

        item = item_service.move_item_to_folder("ws-1", "item-1", None)

        assert item.id == "item-1"
        mock_fabric_client.make_api_request.assert_called_once_with(
            "POST",
            "workspaces/ws-1/items/item-1/move",
            payload={"targetFolderId": None},
            wait_for_lro=True,
        )

    def test_move_item_to_folder_validation(self, item_service):
        """Empty folder id raises validation error."""
        with pytest.raises(FabricValidationError):
            item_service.move_item_to_folder("ws-1", "item-1", " ")

    def test_move_item_to_folder_missing_response(self, item_service, mock_fabric_client):
        """Empty response falls back to fetching the item."""
        item_data = FabricDataFactory.item(item_id="item-1", item_type="Notebook")
        mock_fabric_client.make_api_request.side_effect = [
            MockResponseFactory.success({}),
            MockResponseFactory.success(item_data),
        ]

        item = item_service.move_item_to_folder("ws-1", "item-1", "folder-1")

        assert item.id == "item-1"
        assert mock_fabric_client.make_api_request.call_args_list == [
            call(
                "POST",
                "workspaces/ws-1/items/item-1/move",
                payload={"targetFolderId": "folder-1"},
                wait_for_lro=True,
            ),
            call("GET", "workspaces/ws-1/items/item-1"),
        ]

    def test_move_folder_success(self, item_service, mock_fabric_client):
        """Move folder posts to move endpoint."""
        folder_data = {
            "id": "folder-1",
            "displayName": "Folder",
            "parentFolderId": "parent-1",
        }
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(
            folder_data
        )

        result = item_service.move_folder("ws-1", "folder-1", "parent-1")

        assert result["id"] == "folder-1"
        mock_fabric_client.make_api_request.assert_called_once_with(
            "POST",
            "workspaces/ws-1/folders/folder-1/move",
            payload={"targetFolderId": "parent-1"},
        )

    def test_move_folder_validation(self, item_service):
        """Empty IDs raise validation errors."""
        with pytest.raises(FabricValidationError):
            item_service.move_folder("ws-1", " ", "parent-1")
        with pytest.raises(FabricValidationError):
            item_service.move_folder("ws-1", "folder-1", " ")

    def test_move_folder_missing_response(self, item_service, mock_fabric_client):
        """Empty response raises FabricError."""
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success({})

        with pytest.raises(FabricError):
            item_service.move_folder("ws-1", "folder-1", "parent-1")

    def test_delete_folder_success(self, item_service, mock_fabric_client):
        """Delete folder calls endpoint."""
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success({})

        result = item_service.delete_folder("ws-1", "folder-1")

        assert result["id"] == "folder-1"
        mock_fabric_client.make_api_request.assert_called_once_with(
            "DELETE",
            "workspaces/ws-1/folders/folder-1",
        )

    def test_delete_folder_validation(self, item_service):
        """Empty folder id raises validation error."""
        with pytest.raises(FabricValidationError):
            item_service.delete_folder("ws-1", " ")

    def test_delete_item_success(self, item_service, mock_fabric_client):
        """Delete item calls endpoint."""
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success({})

        item_service.delete_item("ws-1", "item-1")

        mock_fabric_client.make_api_request.assert_called_once_with(
            "DELETE", "workspaces/ws-1/items/item-1"
        )

    def test_delete_item_api_error(self, item_service, mock_fabric_client):
        """API errors propagate."""
        mock_fabric_client.make_api_request.side_effect = FabricAPIError(500, "boom")

        with pytest.raises(FabricAPIError):
            item_service.delete_item("ws-1", "item-1")

    def test_delete_item_unexpected_error(self, item_service, mock_fabric_client):
        """Unexpected errors raise FabricError."""
        mock_fabric_client.make_api_request.side_effect = RuntimeError("boom")

        with pytest.raises(FabricError):
            item_service.delete_item("ws-1", "item-1")

    def test_create_lakehouse_success(self, item_service, mock_fabric_client):
        """create_lakehouse maps response to FabricLakehouse."""
        response_data = {
            "id": "lh-1",
            "displayName": "Lakehouse",
            "description": "desc",
        }
        mock_fabric_client.make_api_request.return_value = MockResponseFactory.success(response_data)

        lakehouse = item_service.create_lakehouse(
            workspace_id="ws-1",
            display_name="Lakehouse",
            description="desc",
            enable_schemas=False,
        )

        assert isinstance(lakehouse, FabricLakehouse)
        assert lakehouse.id == "lh-1"
        call_kwargs = mock_fabric_client.make_api_request.call_args.kwargs
        payload = call_kwargs["payload"]
        assert payload["creationPayload"]["enableSchemas"] is False
