"""Tests for WorkspaceManager."""

import os
from pathlib import Path
from unittest.mock import MagicMock, patch

import docker as docker_mod
import git
import pytest

from app_factory.core.workspace import (
    DockerProvisionError,
    GitWorktreeError,
    WorkspaceError,
    WorkspaceManager,
)


@pytest.fixture
def temp_git_repo(tmp_path):
    """Create a real temporary git repository."""
    repo_dir = tmp_path / "repo"
    repo_dir.mkdir()
    repo = git.Repo.init(repo_dir)
    readme = repo_dir / "README.md"
    readme.write_text("# Test Repo")
    repo.index.add(["README.md"])
    repo.index.commit("Initial commit")
    if repo.active_branch.name != "main":
        repo.git.branch("-M", "main")
    return str(repo_dir)


@pytest.fixture
def mock_docker_client():
    """Create a mock Docker client."""
    mock_client = MagicMock()
    mock_client.images.pull.return_value = MagicMock()
    mock_container = MagicMock()
    mock_container.id = "abc123container"
    mock_client.containers.create.return_value = mock_container
    return mock_client


@pytest.fixture
def workspace_manager(temp_git_repo, mock_docker_client):
    """Create a WorkspaceManager with a real git repo and mocked Docker."""
    with patch("app_factory.core.workspace.docker.from_env", return_value=mock_docker_client):
        wm = WorkspaceManager(temp_git_repo)
    return wm


class TestWorkspaceManagerInit:
    def test_init_valid_repo(self, temp_git_repo, mock_docker_client):
        with patch("app_factory.core.workspace.docker.from_env", return_value=mock_docker_client):
            wm = WorkspaceManager(temp_git_repo)
        assert wm.repo_path == Path(temp_git_repo).resolve()
        assert wm.docker_image == "python:3.11-slim"
        assert wm.active_workspaces == {}

    def test_init_custom_image(self, temp_git_repo, mock_docker_client):
        with patch("app_factory.core.workspace.docker.from_env", return_value=mock_docker_client):
            wm = WorkspaceManager(temp_git_repo, docker_image="node:20-slim")
        assert wm.docker_image == "node:20-slim"

    def test_init_invalid_repo(self, tmp_path, mock_docker_client):
        with patch("app_factory.core.workspace.docker.from_env", return_value=mock_docker_client):
            with pytest.raises(GitWorktreeError, match="Invalid git repository"):
                WorkspaceManager(str(tmp_path))

    def test_init_nonexistent_path(self, mock_docker_client):
        with patch("app_factory.core.workspace.docker.from_env", return_value=mock_docker_client):
            with pytest.raises(GitWorktreeError, match="Repository path not found"):
                WorkspaceManager("/nonexistent/path/to/repo")

    def test_init_docker_unavailable(self, temp_git_repo):
        with patch(
            "app_factory.core.workspace.docker.from_env",
            side_effect=docker_mod.errors.DockerException("Connection refused"),
        ):
            with pytest.raises(DockerProvisionError, match="Failed to connect to Docker"):
                WorkspaceManager(temp_git_repo)


@pytest.mark.asyncio
class TestCreateWorktree:
    async def test_create_worktree_success(self, workspace_manager):
        wm = workspace_manager
        path = await wm.create_worktree("task-001")
        assert os.path.isdir(path)
        assert "task-001" in path
        assert "feature/task-task-001" in [b.name for b in wm.repo.branches]
        # Cleanup
        wm.repo.git.worktree("remove", path, "--force")

    async def test_create_worktree_invalid_base_branch(self, workspace_manager):
        wm = workspace_manager
        with pytest.raises(GitWorktreeError, match="does not exist"):
            await wm.create_worktree("task-002", base_branch="nonexistent")

    async def test_create_worktree_branch_already_exists(self, workspace_manager):
        wm = workspace_manager
        wm.repo.git.branch("feature/task-task-003")
        with pytest.raises(GitWorktreeError, match="Branch already exists"):
            await wm.create_worktree("task-003")

    async def test_create_worktree_path_already_exists(self, workspace_manager):
        wm = workspace_manager
        worktree_dir = wm.repo_path.parent / "worktrees" / "task-004"
        worktree_dir.mkdir(parents=True)
        with pytest.raises(GitWorktreeError, match="Worktree path already exists"):
            await wm.create_worktree("task-004")


@pytest.mark.asyncio
class TestSpinUpCleanRoom:
    async def test_spin_up_clean_room_success(self, workspace_manager, mock_docker_client):
        wm = workspace_manager
        container = await wm.spin_up_clean_room("/tmp/fake_worktree", "task-010")

        assert container.id == "abc123container"
        mock_docker_client.images.pull.assert_called_once_with("python:3.11-slim")
        mock_docker_client.containers.create.assert_called_once_with(
            image="python:3.11-slim",
            name="appfactory-task-task-010",
            volumes={"/tmp/fake_worktree": {"bind": "/workspace", "mode": "rw"}},
            working_dir="/workspace",
            network_mode="none",
            auto_remove=False,
            detach=True,
            command="sleep infinity",
        )
        assert "task-010" in wm.active_workspaces
        info = wm.active_workspaces["task-010"]
        assert info["worktree_path"] == "/tmp/fake_worktree"
        assert info["container_id"] == "abc123container"

    async def test_spin_up_image_pull_failure(self, workspace_manager, mock_docker_client):
        wm = workspace_manager
        mock_docker_client.images.pull.side_effect = docker_mod.errors.APIError("pull failed")
        with pytest.raises(DockerProvisionError, match="Failed to pull image"):
            await wm.spin_up_clean_room("/tmp/fake", "task-011")

    async def test_spin_up_container_create_failure(self, workspace_manager, mock_docker_client):
        wm = workspace_manager
        mock_docker_client.containers.create.side_effect = docker_mod.errors.APIError("create failed")
        with pytest.raises(DockerProvisionError, match="Failed to create container"):
            await wm.spin_up_clean_room("/tmp/fake", "task-012")


@pytest.mark.asyncio
class TestCleanupWorkspace:
    async def test_cleanup_workspace_success(self, workspace_manager):
        wm = workspace_manager
        path = await wm.create_worktree("task-020")
        mock_container = MagicMock()
        wm.active_workspaces["task-020"] = {
            "task_id": "task-020",
            "worktree_path": path,
            "container_id": "cont123",
            "container": mock_container,
        }

        await wm.cleanup_workspace("task-020")

        mock_container.stop.assert_called_once_with(timeout=5)
        mock_container.remove.assert_called_once_with(force=True)
        assert "task-020" not in wm.active_workspaces
        assert not os.path.exists(path)

    async def test_cleanup_already_stopped_container(self, workspace_manager):
        wm = workspace_manager
        mock_container = MagicMock()
        mock_container.stop.side_effect = Exception("already stopped")
        mock_container.remove.return_value = None

        wm.active_workspaces["task-021"] = {
            "task_id": "task-021",
            "worktree_path": "/tmp/nonexistent",
            "container_id": "cont456",
            "container": mock_container,
        }

        # Should not raise even if stop fails
        await wm.cleanup_workspace("task-021")
        assert "task-021" not in wm.active_workspaces

    async def test_cleanup_no_container(self, workspace_manager):
        wm = workspace_manager
        path = await wm.create_worktree("task-022")
        wm.active_workspaces["task-022"] = {
            "task_id": "task-022",
            "worktree_path": path,
            "container_id": None,
            "container": None,
        }
        await wm.cleanup_workspace("task-022")
        assert "task-022" not in wm.active_workspaces


class TestGetActiveWorkspaces:
    def test_get_empty(self, workspace_manager):
        assert workspace_manager.get_active_workspaces() == []

    def test_get_with_workspaces(self, workspace_manager):
        wm = workspace_manager
        wm.active_workspaces["t1"] = {
            "task_id": "t1",
            "worktree_path": "/path/t1",
            "container_id": "c1",
            "container": MagicMock(),
        }
        wm.active_workspaces["t2"] = {
            "task_id": "t2",
            "worktree_path": "/path/t2",
            "container_id": "c2",
            "container": MagicMock(),
        }

        result = wm.get_active_workspaces()
        assert len(result) == 2
        assert {"task_id": "t1", "worktree_path": "/path/t1", "container_id": "c1"} in result
        assert {"task_id": "t2", "worktree_path": "/path/t2", "container_id": "c2"} in result
        for item in result:
            assert "container" not in item


@pytest.mark.asyncio
class TestCleanupAll:
    async def test_cleanup_all_success(self, workspace_manager):
        wm = workspace_manager
        mock_c1 = MagicMock()
        mock_c2 = MagicMock()
        wm.active_workspaces["t1"] = {
            "task_id": "t1",
            "worktree_path": "/tmp/nonexistent1",
            "container_id": "c1",
            "container": mock_c1,
        }
        wm.active_workspaces["t2"] = {
            "task_id": "t2",
            "worktree_path": "/tmp/nonexistent2",
            "container_id": "c2",
            "container": mock_c2,
        }

        await wm.cleanup_all()

        mock_c1.stop.assert_called_once()
        mock_c2.stop.assert_called_once()
        assert wm.active_workspaces == {}

    async def test_cleanup_all_with_errors(self, workspace_manager):
        wm = workspace_manager
        mock_c1 = MagicMock()
        mock_c1.stop.side_effect = Exception("stop failed")
        mock_c1.remove.side_effect = Exception("remove failed")

        wm.active_workspaces["t1"] = {
            "task_id": "t1",
            "worktree_path": "/tmp/nonexistent",
            "container_id": "c1",
            "container": mock_c1,
        }

        with pytest.raises(WorkspaceError, match="Cleanup all completed with errors"):
            await wm.cleanup_all()

        assert wm.active_workspaces == {}
