"""Unit tests for FabricNotebookService."""

import base64
import json
from unittest.mock import Mock, patch

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.job import FabricJob
from ms_fabric_mcp_server.models.results import JobStatusResult
from tests.fixtures.mocks import MockResponseFactory


def _make_response(status_code=200, json_data=None, text="", headers=None):
    response = Mock()
    response.status_code = status_code
    response.ok = 200 <= status_code < 300
    response.json.return_value = json_data or {}
    response.text = text
    response.headers = headers or {}
    return response


def _encode_ipynb(payload):
    return base64.b64encode(json.dumps(payload).encode("utf-8")).decode("utf-8")


@pytest.fixture
def mock_workspace_service():
    service = Mock()
    service.resolve_workspace_id.return_value = "workspace-123"
    return service


@pytest.fixture
def mock_item_service():
    return Mock()


@pytest.fixture
def notebook_service(mock_fabric_client, mock_item_service, mock_workspace_service):
    from ms_fabric_mcp_server.services.notebook import FabricNotebookService

    return FabricNotebookService(
        mock_fabric_client,
        mock_item_service,
        mock_workspace_service,
    )


@pytest.mark.unit
class TestFabricNotebookService:
    """Test suite for FabricNotebookService."""

    def test_encode_notebook_content_success(self, notebook_service):
        """Encode a notebook dict to base64 JSON."""
        payload = {"cells": [], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
        encoded = notebook_service._encode_notebook_content(payload)

        decoded = base64.b64decode(encoded.encode("utf-8")).decode("utf-8")
        assert json.loads(decoded) == payload

    def test_encode_notebook_content_invalid(self, notebook_service):
        """Invalid notebook content raises validation error."""
        with pytest.raises(FabricValidationError):
            notebook_service._encode_notebook_content({})

    def test_create_notebook_definition_includes_description(self, notebook_service):
        """Notebook definition includes description when provided."""
        notebook_service._encode_notebook_content = Mock(return_value="encoded")

        definition = notebook_service._create_notebook_definition(
            notebook_name="Test Notebook",
            notebook_content={"cells": []},
            description="Test description",
        )

        assert definition["displayName"] == "Test Notebook"
        assert definition["type"] == "Notebook"
        assert definition["description"] == "Test description"
        part = definition["definition"]["parts"][0]
        assert part["path"] == "Test Notebook.ipynb"
        assert part["payload"] == "encoded"

    def test_create_notebook_definition_without_description(self, notebook_service):
        """Notebook definition omits description when not provided."""
        notebook_service._encode_notebook_content = Mock(return_value="encoded")

        definition = notebook_service._create_notebook_definition(
            notebook_name="Test Notebook",
            notebook_content={"cells": []},
        )

        assert "description" not in definition

    def test_create_notebook_definition_with_folder(self, notebook_service):
        """Notebook definition includes folderId when provided."""
        notebook_service._encode_notebook_content = Mock(return_value="encoded")

        definition = notebook_service._create_notebook_definition(
            notebook_name="Test Notebook",
            notebook_content={"cells": []},
            folder_id="folder-1",
        )

        assert definition["folderId"] == "folder-1"

    def test_create_notebook_success(self, notebook_service, mock_item_service, mock_workspace_service):
        """Create notebook returns success result."""
        notebook_service._create_notebook_definition = Mock(return_value={"definition": {}})
        mock_workspace_service.resolve_workspace_id.return_value = "ws-1"
        mock_item_service.resolve_folder_id_from_path.return_value = "folder-1"
        mock_item_service.create_item.return_value = FabricItem(
            id="nb-123",
            display_name="Test",
            type="Notebook",
            workspace_id="ws-1",
        )

        result = notebook_service.create_notebook(
            workspace_name="Workspace",
            notebook_name="Notebook",
            notebook_content={"cells": []},
            folder_path="Notebooks/Finance",
        )

        assert result.status == "success"
        assert result.notebook_id == "nb-123"
        notebook_service._create_notebook_definition.assert_called_once_with(
            "Notebook",
            {"cells": []},
            None,
            folder_id="folder-1",
        )
        mock_item_service.create_item.assert_called_once()

    @pytest.mark.parametrize(
        "exception",
        [
            FabricItemNotFoundError("Notebook", "Notebook", "Workspace"),
            FabricValidationError("field", "value", "bad"),
            FabricAPIError(400, "bad"),
        ],
    )
    def test_create_notebook_expected_errors(self, notebook_service, exception):
        """Create notebook maps known exceptions to error result."""
        notebook_service._create_notebook_definition = Mock(side_effect=exception)

        result = notebook_service.create_notebook(
            workspace_name="Workspace",
            notebook_name="Notebook",
            notebook_content={"cells": []},
        )

        assert result.status == "error"
        assert result.message

    def test_create_notebook_invalid_name(self, notebook_service):
        """Notebook names with separators raise validation error."""
        with pytest.raises(FabricValidationError):
            notebook_service.create_notebook(
                workspace_name="Workspace",
                notebook_name="Bad/Name",
                notebook_content={"cells": []},
            )

    def test_create_notebook_unexpected_error(self, notebook_service):
        """Unexpected exceptions return error with prefix."""
        notebook_service._create_notebook_definition = Mock(side_effect=RuntimeError("boom"))

        result = notebook_service.create_notebook(
            workspace_name="Workspace",
            notebook_name="Notebook",
            notebook_content={"cells": []},
        )

        assert result.status == "error"
        assert "Unexpected error" in result.message

    def test_get_notebook_definition_success(self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client):
        """Get notebook content decodes ipynb payload."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        notebook_content = {"cells": [{"cell_type": "code", "source": ["print(1)"]}]}
        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook.ipynb",
                        "payload": _encode_ipynb(notebook_content),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }

        response = _make_response(200, definition_response)
        mock_fabric_client.make_api_request.return_value = response

        result = notebook_service.get_notebook_definition("Workspace", "Notebook")

        assert result == notebook_content
        mock_fabric_client.make_api_request.assert_called_once()

    def test_get_notebook_definition_lro_success(self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client):
        """Handle 202 LRO with success status."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        notebook_content = {"cells": [{"cell_type": "markdown", "source": ["hi"]}]}
        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook.ipynb",
                        "payload": _encode_ipynb(notebook_content),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }

        initial = _make_response(202, headers={"Location": "https://poll", "Retry-After": "0"})
        poll = _make_response(200, {"status": "Succeeded"})
        result_resp = _make_response(200, definition_response)

        mock_fabric_client.make_api_request.side_effect = [initial, poll, result_resp]

        with patch("time.sleep", return_value=None):
            result = notebook_service.get_notebook_definition("Workspace", "Notebook")

        assert result == notebook_content

    def test_get_notebook_definition_lro_failed(self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client):
        """LRO failed status raises FabricError."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        initial = _make_response(202, headers={"Location": "https://poll", "Retry-After": "0"})
        poll = _make_response(200, {"status": "Failed", "error": {"message": "boom"}})
        mock_fabric_client.make_api_request.side_effect = [initial, poll]

        with patch("time.sleep", return_value=None):
            with pytest.raises(FabricError):
                notebook_service.get_notebook_definition("Workspace", "Notebook")

    def test_get_notebook_definition_lro_retry_after_non_integer(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """LRO handles non-integer Retry-After headers."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        notebook_content = {"cells": [{"cell_type": "markdown", "source": ["hi"]}]}
        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook.ipynb",
                        "payload": _encode_ipynb(notebook_content),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }

        initial = _make_response(
            202,
            headers={"Location": "https://poll", "Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT"},
        )
        poll = _make_response(200, {"status": "Succeeded"})
        result_resp = _make_response(200, definition_response)
        mock_fabric_client.make_api_request.side_effect = [initial, poll, result_resp]

        with patch("time.sleep", return_value=None) as sleep_mock:
            result = notebook_service.get_notebook_definition("Workspace", "Notebook")

        assert result == notebook_content
        sleep_mock.assert_called_once_with(5)

    def test_get_notebook_definition_lro_in_progress(self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client):
        """LRO continues polling on non-terminal status."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        notebook_content = {"cells": [{"cell_type": "markdown", "source": ["hi"]}]}
        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook.ipynb",
                        "payload": _encode_ipynb(notebook_content),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }

        initial = _make_response(202, headers={"Location": "https://poll", "Retry-After": "0"})
        poll_running = _make_response(200, {"status": "Running"}, headers={"Retry-After": "0"})
        poll_accepted = _make_response(202, headers={"Retry-After": "0"})
        poll_succeeded = _make_response(200, {"status": "Succeeded"})
        result_resp = _make_response(200, definition_response)

        mock_fabric_client.make_api_request.side_effect = [
            initial,
            poll_running,
            poll_accepted,
            poll_succeeded,
            result_resp,
        ]

        with patch("time.sleep", return_value=None):
            result = notebook_service.get_notebook_definition("Workspace", "Notebook")

        assert result == notebook_content

    def test_get_notebook_definition_lro_timeout(self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client):
        """LRO timeout raises FabricError after max retries."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        initial = _make_response(202, headers={"Location": "https://poll", "Retry-After": "0"})
        poll = _make_response(202, headers={"Retry-After": "0"})
        mock_fabric_client.make_api_request.side_effect = [initial] + [poll] * 30

        with patch("time.sleep", return_value=None):
            with pytest.raises(FabricError):
                notebook_service.get_notebook_definition("Workspace", "Notebook")

    def test_get_notebook_definition_lro_missing_location(self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client):
        """LRO without Location header raises FabricError."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        initial = _make_response(202)
        mock_fabric_client.make_api_request.return_value = initial

        with pytest.raises(FabricError):
            notebook_service.get_notebook_definition("Workspace", "Notebook")

    def test_get_notebook_definition_returns_raw_definition(self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client):
        """Return raw definition when no ipynb part exists."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        mock_item_service.get_item_by_name.return_value = FabricItem(
            id="nb-123",
            display_name="Notebook",
            type="Notebook",
            workspace_id="ws-123",
        )

        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook-content.py",
                        "payload": base64.b64encode(b"print(1)").decode("utf-8"),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }
        response = _make_response(200, definition_response)
        mock_fabric_client.make_api_request.return_value = response

        result = notebook_service.get_notebook_definition("Workspace", "Notebook")

        assert result == definition_response

    def test_list_notebooks(self, notebook_service, mock_item_service, mock_workspace_service):
        """List notebooks delegates to item service."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        items = [
            FabricItem(id="nb-1", display_name="A", type="Notebook", workspace_id="ws-123"),
            FabricItem(id="nb-2", display_name="B", type="Notebook", workspace_id="ws-123"),
        ]
        mock_item_service.list_items.return_value = items

        result = notebook_service.list_notebooks("Workspace")

        assert result == items
        mock_item_service.list_items.assert_called_once_with("ws-123", "Notebook")

    def test_get_notebook_by_name(self, notebook_service, mock_item_service, mock_workspace_service):
        """Get notebook by name delegates to item service."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        item = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="ws-123")
        mock_item_service.get_item_by_name.return_value = item

        result = notebook_service.get_notebook_by_name("Workspace", "Notebook")

        assert result == item
        mock_item_service.get_item_by_name.assert_called_once_with("ws-123", "Notebook", "Notebook")

    def test_update_notebook_metadata(self, notebook_service, mock_item_service, mock_workspace_service):
        """Update notebook metadata delegates to item service."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="ws-123")
        updated = FabricItem(id="nb-1", display_name="Notebook v2", type="Notebook", workspace_id="ws-123")
        mock_item_service.get_item_by_name.return_value = notebook
        mock_item_service.update_item.return_value = updated

        result = notebook_service.update_notebook_metadata("Workspace", "Notebook", {"displayName": "Notebook v2"})

        assert result == updated
        mock_item_service.update_item.assert_called_once_with("ws-123", "nb-1", {"displayName": "Notebook v2"})

    def test_delete_notebook(self, notebook_service, mock_item_service, mock_workspace_service):
        """Delete notebook delegates to item service."""
        mock_workspace_service.resolve_workspace_id.return_value = "ws-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="ws-123")
        mock_item_service.get_item_by_name.return_value = notebook

        notebook_service.delete_notebook("Workspace", "Notebook")

        mock_item_service.delete_item.assert_called_once_with("ws-123", "nb-1")

    def test_execute_notebook_success(self, notebook_service, mock_workspace_service, mock_item_service):
        """Execute notebook returns mapped ExecuteNotebookResult."""
        job = FabricJob(
            job_instance_id="job-1",
            item_id="nb-1",
            job_type="RunNotebook",
            status="Completed",
            invoke_type="Manual",
            root_activity_id="root-1",
            start_time_utc="2025-01-01T00:00:00Z",
            end_time_utc="2025-01-01T00:05:00Z",
        )
        job_result = JobStatusResult(status="success", job=job, message="ok")

        with patch("ms_fabric_mcp_server.services.job.FabricJobService") as mock_job_service:
            mock_job_service.return_value.run_notebook_job.return_value = job_result
            result = notebook_service.execute_notebook("Workspace", "Notebook", parameters={"a": 1})

        assert result.status == "success"
        assert result.job_instance_id == "job-1"
        assert result.job_status == "Completed"

    def test_execute_notebook_error_result(self, notebook_service):
        """Execute notebook returns error when job result is error."""
        job_result = JobStatusResult(status="error", message="boom")

        with patch("ms_fabric_mcp_server.services.job.FabricJobService") as mock_job_service:
            mock_job_service.return_value.run_notebook_job.return_value = job_result
            result = notebook_service.execute_notebook("Workspace", "Notebook")

        assert result.status == "error"
        assert result.message == "boom"

    def test_execute_notebook_wait_false_success(self, notebook_service):
        """Execute notebook succeeds when wait=False returns job metadata."""
        job_result = JobStatusResult(status="success", job_instance_id="job-2", message="started")

        with patch("ms_fabric_mcp_server.services.job.FabricJobService") as mock_job_service:
            mock_job_service.return_value.run_notebook_job.return_value = job_result
            result = notebook_service.execute_notebook("Workspace", "Notebook", wait=False)

        assert result.status == "success"
        assert result.job_instance_id == "job-2"

    def test_execute_notebook_unexpected_error(self, notebook_service):
        """Execute notebook wraps unexpected exceptions."""
        with patch("ms_fabric_mcp_server.services.job.FabricJobService") as mock_job_service:
            mock_job_service.return_value.run_notebook_job.side_effect = RuntimeError("boom")
            result = notebook_service.execute_notebook("Workspace", "Notebook")

        assert result.status == "error"
        assert "Unexpected error" in result.message

    def test_update_notebook_definition_with_lakehouse(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """Update notebook definition applies lakehouse dependencies when provided."""
        mock_workspace_service.resolve_workspace_id.side_effect = ["workspace-123", "workspace-123"]

        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        lakehouse = FabricItem(id="lh-1", display_name="Lakehouse", type="Lakehouse", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.side_effect = [notebook, lakehouse]

        existing_payload = {"cells": [], "metadata": {}}
        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook.ipynb",
                        "payload": _encode_ipynb(existing_payload),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }

        get_def = _make_response(200, definition_response)
        update_def = MockResponseFactory.success({})
        mock_fabric_client.make_api_request.side_effect = [get_def, update_def]

        result = notebook_service.update_notebook_definition(
            workspace_name="Workspace",
            notebook_name="Notebook",
            notebook_content={"cells": []},
            default_lakehouse_name="Lakehouse",
        )

        assert result.status == "success"
        update_call = mock_fabric_client.make_api_request.call_args_list[1]
        payload = update_call.kwargs["payload"]
        encoded = payload["definition"]["parts"][0]["payload"]
        decoded = json.loads(base64.b64decode(encoded).decode("utf-8"))
        lakehouse_meta = decoded["metadata"]["dependencies"]["lakehouse"]
        assert lakehouse_meta["default_lakehouse"] == "lh-1"
        assert lakehouse_meta["default_lakehouse_name"] == "Lakehouse"
        assert lakehouse_meta["default_lakehouse_workspace_id"] == "workspace-123"
        assert lakehouse_meta["known_lakehouses"] == [{"id": "lh-1"}]

    def test_update_notebook_definition_preserves_dependencies(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """Update notebook definition preserves existing dependencies when no lakehouse specified."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"

        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.return_value = notebook

        existing_payload = {"cells": [], "metadata": {"dependencies": {"foo": "bar"}}}
        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook.ipynb",
                        "payload": _encode_ipynb(existing_payload),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }

        get_def = _make_response(200, definition_response)
        update_def = MockResponseFactory.success({})
        mock_fabric_client.make_api_request.side_effect = [get_def, update_def]

        result = notebook_service.update_notebook_definition(
            workspace_name="Workspace",
            notebook_name="Notebook",
            notebook_content={"cells": []},
        )

        assert result.status == "success"
        update_call = mock_fabric_client.make_api_request.call_args_list[1]
        payload = update_call.kwargs["payload"]
        encoded = payload["definition"]["parts"][0]["payload"]
        decoded = json.loads(base64.b64decode(encoded).decode("utf-8"))
        assert decoded["metadata"]["dependencies"] == {"foo": "bar"}

    def test_update_notebook_definition_uses_existing_definition(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """Update notebook definition can reuse existing definition when content omitted."""
        mock_workspace_service.resolve_workspace_id.side_effect = ["workspace-123", "workspace-123"]

        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        lakehouse = FabricItem(id="lh-1", display_name="Lakehouse", type="Lakehouse", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.side_effect = [notebook, lakehouse]

        existing_payload = {"cells": [], "metadata": {"dependencies": {"foo": "bar"}}}
        definition_response = {
            "definition": {
                "parts": [
                    {
                        "path": "notebook.ipynb",
                        "payload": _encode_ipynb(existing_payload),
                        "payloadType": "InlineBase64",
                    }
                ]
            }
        }

        get_def = _make_response(200, definition_response)
        update_def = MockResponseFactory.success({})
        mock_fabric_client.make_api_request.side_effect = [get_def, update_def]

        result = notebook_service.update_notebook_definition(
            workspace_name="Workspace",
            notebook_name="Notebook",
            notebook_content=None,
            default_lakehouse_name="Lakehouse",
        )

        assert result.status == "success"
        update_call = mock_fabric_client.make_api_request.call_args_list[1]
        payload = update_call.kwargs["payload"]
        encoded = payload["definition"]["parts"][0]["payload"]
        decoded = json.loads(base64.b64decode(encoded).decode("utf-8"))
        assert decoded["metadata"]["dependencies"]["lakehouse"]["default_lakehouse"] == "lh-1"

    def test_update_notebook_definition_missing_lakehouse(
        self, notebook_service, mock_item_service, mock_workspace_service
    ):
        """Missing lakehouse returns error result."""
        mock_workspace_service.resolve_workspace_id.side_effect = ["workspace-123", "workspace-123"]

        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.side_effect = [
            notebook,
            FabricItemNotFoundError("Lakehouse", "Lakehouse", "workspace-123"),
        ]

        result = notebook_service.update_notebook_definition(
            workspace_name="Workspace",
            notebook_name="Notebook",
            notebook_content={"cells": []},
            default_lakehouse_name="Lakehouse",
        )

        assert result.status == "error"

    def test_get_notebook_run_details_success(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """Return execution details and summary."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.return_value = notebook

        list_response = _make_response(
            200,
            {
                "value": [
                    {
                        "jobInstanceId": "job-1",
                        "livyId": "livy-1",
                        "state": "Success",
                        "sparkApplicationId": "app-1",
                        "operationName": "RunNotebook",
                        "submittedDateTime": "2025-01-01T00:00:00Z",
                        "startDateTime": "2025-01-01T00:01:00Z",
                        "endDateTime": "2025-01-01T00:05:00Z",
                        "totalDuration": {"value": 240},
                    }
                ]
            },
        )
        detail_response = _make_response(
            200,
            {
                "state": "Success",
                "sparkApplicationId": "app-1",
                "livyId": "livy-1",
                "jobInstanceId": "job-1",
                "operationName": "RunNotebook",
                "submittedDateTime": "2025-01-01T00:00:00Z",
                "startDateTime": "2025-01-01T00:01:00Z",
                "endDateTime": "2025-01-01T00:05:00Z",
                "queuedDuration": {"value": 5},
                "runningDuration": {"value": 235},
                "totalDuration": {"value": 240},
            },
        )
        job_response = _make_response(200, {"failureReason": {"message": "none"}})

        mock_fabric_client.make_api_request.side_effect = [
            list_response,
            detail_response,
            job_response,
        ]

        result = notebook_service.get_notebook_run_details("Workspace", "Notebook", "job-1")

        assert result["status"] == "success"
        assert result["execution_summary"]["failure_reason"] == {"message": "none"}
        assert result["notebook_id"] == "nb-1"

    def test_get_notebook_run_details_no_session(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """Return error when no matching session found."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.return_value = notebook

        list_response = _make_response(200, {"value": []})
        mock_fabric_client.make_api_request.return_value = list_response

        result = notebook_service.get_notebook_run_details("Workspace", "Notebook", "job-1")

        assert result["status"] == "error"
        assert result["available_sessions"] == 0

    def test_get_notebook_run_details_api_error(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """API errors return error status."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.return_value = notebook

        mock_fabric_client.make_api_request.side_effect = FabricAPIError(500, "boom")

        result = notebook_service.get_notebook_run_details("Workspace", "Notebook", "job-1")

        assert result["status"] == "error"
        assert "boom" in result["message"]

    def test_list_notebook_runs_success_with_limit(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """List executions applies limit to session summaries."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.return_value = notebook

        list_response = _make_response(
            200,
            {
                "value": [
                    {"jobInstanceId": "job-1", "livyId": "1", "state": "Success"},
                    {"jobInstanceId": "job-2", "livyId": "2", "state": "Failed"},
                ]
            },
        )
        mock_fabric_client.make_api_request.return_value = list_response

        result = notebook_service.list_notebook_runs("Workspace", "Notebook", limit=1)

        assert result["status"] == "success"
        assert len(result["sessions"]) == 1
        assert result["total_count"] == 2

    def test_list_notebook_runs_item_not_found(
        self, notebook_service, mock_item_service, mock_workspace_service
    ):
        """Item not found maps to error response."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"
        mock_item_service.get_item_by_name.side_effect = FabricItemNotFoundError("Notebook", "Notebook", "workspace-123")

        result = notebook_service.list_notebook_runs("Workspace", "Notebook")

        assert result["status"] == "error"

    def test_get_notebook_driver_logs_success_truncated(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """Driver log retrieval truncates to max lines."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.return_value = notebook

        notebook_service.get_notebook_run_details = Mock(
            return_value={
                "status": "success",
                "execution_summary": {
                    "livy_id": "livy-1",
                    "spark_application_id": "app-1",
                },
            }
        )

        meta_response = _make_response(200, {"sizeInBytes": 123})
        log_response = _make_response(200, text="line1\nline2\nline3")
        mock_fabric_client.make_api_request.side_effect = [meta_response, log_response]

        result = notebook_service.get_notebook_driver_logs(
            "Workspace",
            "Notebook",
            "job-1",
            log_type="stdout",
            max_lines=2,
        )

        assert result["status"] == "success"
        assert result["truncated"] is True
        assert result["log_content"] == "line2\nline3"

    def test_get_notebook_driver_logs_invalid_log_type(self, notebook_service):
        """Invalid log_type returns error result."""
        result = notebook_service.get_notebook_driver_logs(
            "Workspace", "Notebook", "job-1", log_type="bad"
        )

        assert result["status"] == "error"
        assert "Invalid log_type" in result["message"]

    def test_get_notebook_driver_logs_exec_details_error(self, notebook_service):
        """Propagates execution detail error."""
        notebook_service.get_notebook_run_details = Mock(
            return_value={"status": "error", "message": "boom"}
        )

        result = notebook_service.get_notebook_driver_logs(
            "Workspace", "Notebook", "job-1"
        )

        assert result["status"] == "error"
        assert result["message"] == "boom"

    def test_get_notebook_driver_logs_missing_summary_fields(self, notebook_service):
        """Missing livy or spark app IDs returns error."""
        notebook_service.get_notebook_run_details = Mock(
            return_value={"status": "success", "execution_summary": {}}
        )

        result = notebook_service.get_notebook_driver_logs(
            "Workspace", "Notebook", "job-1"
        )

        assert result["status"] == "error"

    def test_get_notebook_driver_logs_api_error(
        self, notebook_service, mock_item_service, mock_workspace_service, mock_fabric_client
    ):
        """API errors are mapped to error result."""
        mock_workspace_service.resolve_workspace_id.return_value = "workspace-123"
        notebook = FabricItem(id="nb-1", display_name="Notebook", type="Notebook", workspace_id="workspace-123")
        mock_item_service.get_item_by_name.return_value = notebook
        notebook_service.get_notebook_run_details = Mock(
            return_value={
                "status": "success",
                "execution_summary": {"livy_id": "livy-1", "spark_application_id": "app-1"},
            }
        )
        mock_fabric_client.make_api_request.side_effect = FabricAPIError(500, "boom")

        result = notebook_service.get_notebook_driver_logs(
            "Workspace", "Notebook", "job-1"
        )

        assert result["status"] == "error"
