"""Tests for workspace operations including setRoot and findNotebooks."""

import pytest
from pathlib import Path

from skop_runner.file_manager import FileManager
from skop_runner.mock_backend import MockKernelBackend
from skop_runner.message_handler import MessageHandler


@pytest.fixture
def file_manager(tmp_path):
    """Create a file manager with temporary directory."""
    return FileManager(str(tmp_path))


@pytest.fixture
def file_manager_no_root():
    """Create a file manager without a root."""
    return FileManager()


class TestFileManagerSetRoot:
    """Tests for FileManager.set_root functionality."""

    def test_set_root_changes_project_root(self, file_manager_no_root, tmp_path):
        """Test that set_root updates the project root."""
        assert file_manager_no_root.project_root is None

        file_manager_no_root.set_root(str(tmp_path))

        assert file_manager_no_root.project_root == tmp_path.resolve()

    def test_set_root_invalid_path_raises(self, file_manager_no_root):
        """Test that set_root raises for non-existent path."""
        with pytest.raises(ValueError, match="does not exist"):
            file_manager_no_root.set_root("/nonexistent/path")

    def test_set_root_file_not_dir_raises(self, file_manager_no_root, tmp_path):
        """Test that set_root raises when path is a file."""
        file_path = tmp_path / "file.txt"
        file_path.touch()

        with pytest.raises(ValueError, match="not a directory"):
            file_manager_no_root.set_root(str(file_path))

    @pytest.mark.asyncio
    async def test_operations_fail_without_root(self, file_manager_no_root):
        """Test that file operations fail when no root is set."""
        with pytest.raises(ValueError, match="No project root set"):
            await file_manager_no_root.list_directory()

    @pytest.mark.asyncio
    async def test_operations_work_after_set_root(self, file_manager_no_root, tmp_path):
        """Test that operations work after setting root."""
        file_manager_no_root.set_root(str(tmp_path))

        # Should not raise
        items = await file_manager_no_root.list_directory()
        assert isinstance(items, list)


class TestFindNotebooks:
    """Tests for file.findNotebooks functionality."""

    @pytest.mark.asyncio
    async def test_find_notebooks_empty_dir(self, file_manager):
        """Test finding notebooks in empty directory."""
        notebooks = await file_manager.find_notebooks()
        assert notebooks == []

    @pytest.mark.asyncio
    async def test_find_single_notebook(self, file_manager, tmp_path):
        """Test finding a single notebook."""
        notebook = tmp_path / "analysis.ipynb"
        notebook.write_text("{}")

        notebooks = await file_manager.find_notebooks()

        assert len(notebooks) == 1
        assert notebooks[0]["name"] == "analysis.ipynb"
        assert notebooks[0]["path"] == "analysis.ipynb"
        assert notebooks[0]["folder"] == ""

    @pytest.mark.asyncio
    async def test_find_nested_notebooks(self, file_manager, tmp_path):
        """Test finding notebooks in subdirectories."""
        # Create nested structure
        ds_dir = tmp_path / "data-science"
        ds_dir.mkdir()
        (ds_dir / "analysis.ipynb").write_text("{}")

        ml_dir = tmp_path / "ml-experiments"
        ml_dir.mkdir()
        (ml_dir / "training.ipynb").write_text("{}")

        notebooks = await file_manager.find_notebooks()

        assert len(notebooks) == 2
        paths = {nb["path"] for nb in notebooks}
        assert "data-science/analysis.ipynb" in paths
        assert "ml-experiments/training.ipynb" in paths

    @pytest.mark.asyncio
    async def test_skip_checkpoint_files(self, file_manager, tmp_path):
        """Test that .ipynb_checkpoints are skipped."""
        notebook = tmp_path / "analysis.ipynb"
        notebook.write_text("{}")

        checkpoint_dir = tmp_path / ".ipynb_checkpoints"
        checkpoint_dir.mkdir()
        (checkpoint_dir / "analysis-checkpoint.ipynb").write_text("{}")

        notebooks = await file_manager.find_notebooks()

        assert len(notebooks) == 1
        assert notebooks[0]["name"] == "analysis.ipynb"

    @pytest.mark.asyncio
    async def test_skip_venv_notebooks(self, file_manager, tmp_path):
        """Test that notebooks in .venv are skipped."""
        notebook = tmp_path / "analysis.ipynb"
        notebook.write_text("{}")

        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        lib_dir = venv_dir / "lib" / "python3.11" / "site-packages"
        lib_dir.mkdir(parents=True)
        (lib_dir / "some_notebook.ipynb").write_text("{}")

        notebooks = await file_manager.find_notebooks()

        assert len(notebooks) == 1
        assert notebooks[0]["name"] == "analysis.ipynb"

    @pytest.mark.asyncio
    async def test_skip_node_modules_notebooks(self, file_manager, tmp_path):
        """Test that notebooks in node_modules are skipped."""
        notebook = tmp_path / "analysis.ipynb"
        notebook.write_text("{}")

        nm_dir = tmp_path / "node_modules" / "some-package"
        nm_dir.mkdir(parents=True)
        (nm_dir / "example.ipynb").write_text("{}")

        notebooks = await file_manager.find_notebooks()

        assert len(notebooks) == 1

    @pytest.mark.asyncio
    async def test_notebooks_sorted_by_folder_then_name(self, file_manager, tmp_path):
        """Test that notebooks are sorted by folder, then by name."""
        # Create in random order
        (tmp_path / "zebra.ipynb").write_text("{}")
        (tmp_path / "alpha.ipynb").write_text("{}")

        b_dir = tmp_path / "b_folder"
        b_dir.mkdir()
        (b_dir / "second.ipynb").write_text("{}")

        a_dir = tmp_path / "a_folder"
        a_dir.mkdir()
        (a_dir / "first.ipynb").write_text("{}")

        notebooks = await file_manager.find_notebooks()

        # Should be: root files first (sorted), then folders (sorted)
        names = [nb["name"] for nb in notebooks]
        # Root folder comes first (empty string), then a_folder, then b_folder
        assert names == ["alpha.ipynb", "zebra.ipynb", "first.ipynb", "second.ipynb"]

    @pytest.mark.asyncio
    async def test_find_notebooks_requires_root(self, file_manager_no_root):
        """Test that find_notebooks fails without root."""
        with pytest.raises(ValueError, match="No project root set"):
            await file_manager_no_root.find_notebooks()


class TestWorkspaceSetRootHandler:
    """Tests for workspace.setRoot via MessageHandler."""

    @pytest.fixture
    def message_handler(self, tmp_path):
        """Create message handler with temp directory."""
        km = MockKernelBackend()
        fm = FileManager(str(tmp_path))
        return MessageHandler(km, fm)

    @pytest.mark.asyncio
    async def test_workspace_set_root(self, message_handler, tmp_path):
        """Test workspace.setRoot handler."""
        new_root = tmp_path / "new_project"
        new_root.mkdir()

        response = await message_handler.handle_message({
            "id": "1",
            "method": "workspace.setRoot",
            "params": {"path": str(new_root)},
        })

        assert "result" in response
        assert response["result"]["success"] is True
        assert response["result"]["root"] == str(new_root)

    @pytest.mark.asyncio
    async def test_workspace_set_root_invalid_path(self, message_handler):
        """Test workspace.setRoot with invalid path."""
        response = await message_handler.handle_message({
            "id": "1",
            "method": "workspace.setRoot",
            "params": {"path": "/nonexistent/path"},
        })

        assert "error" in response

    @pytest.mark.asyncio
    async def test_workspace_get_root_shows_has_root(self, message_handler):
        """Test workspace.getRoot includes hasRoot flag."""
        response = await message_handler.handle_message({
            "id": "1",
            "method": "workspace.getRoot",
            "params": {},
        })

        assert "result" in response
        assert response["result"]["hasRoot"] is True

    @pytest.mark.asyncio
    async def test_workspace_set_root_writes_project_json(self, tmp_path):
        """workspace.setRoot with project_id → .skop/project.json created."""
        import json

        project_dir = tmp_path / "my_project"
        project_dir.mkdir()

        km = MockKernelBackend()
        fm = FileManager(str(tmp_path))
        handler = MessageHandler(km, fm)

        response = await handler.handle_message({
            "id": "1",
            "method": "workspace.setRoot",
            "params": {"path": str(project_dir), "project_id": "proj_test123"},
        })

        assert "result" in response
        assert response["result"]["success"] is True

        # Verify .skop/project.json was created
        skop_file = project_dir / ".skop" / "project.json"
        assert skop_file.exists()
        data = json.loads(skop_file.read_text())
        assert data["projectId"] == "proj_test123"

        # Verify cached value
        assert fm.get_project_id() == "proj_test123"

    @pytest.mark.asyncio
    async def test_workspace_set_root_without_project_id(self, tmp_path):
        """workspace.setRoot without project_id → no .skop/project.json created."""
        project_dir = tmp_path / "my_project"
        project_dir.mkdir()

        km = MockKernelBackend()
        fm = FileManager(str(tmp_path))
        handler = MessageHandler(km, fm)

        response = await handler.handle_message({
            "id": "1",
            "method": "workspace.setRoot",
            "params": {"path": str(project_dir)},
        })

        assert "result" in response
        assert response["result"]["success"] is True
        assert not (project_dir / ".skop" / "project.json").exists()


class TestFindNotebooksHandler:
    """Tests for file.findNotebooks via MessageHandler."""

    @pytest.fixture
    def message_handler(self, tmp_path):
        """Create message handler with temp directory."""
        km = MockKernelBackend()
        fm = FileManager(str(tmp_path))
        return MessageHandler(km, fm)

    @pytest.mark.asyncio
    async def test_file_find_notebooks(self, message_handler, tmp_path):
        """Test file.findNotebooks handler."""
        # Create some notebooks
        (tmp_path / "analysis.ipynb").write_text("{}")

        subdir = tmp_path / "experiments"
        subdir.mkdir()
        (subdir / "training.ipynb").write_text("{}")

        response = await message_handler.handle_message({
            "id": "1",
            "method": "file.findNotebooks",
            "params": {},
        })

        assert "result" in response
        notebooks = response["result"]["notebooks"]
        assert len(notebooks) == 2

        paths = {nb["path"] for nb in notebooks}
        assert "analysis.ipynb" in paths
        assert "experiments/training.ipynb" in paths
