"""Unit tests for Unity MCP CLI."""

import json
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from click.testing import CliRunner

from cli.main import cli
from cli.utils.config import CLIConfig, get_config, set_config
from cli.utils.output import format_output, format_as_json, format_as_text, format_as_table
from cli.utils.connection import (
    send_command,
    check_connection,
    list_unity_instances,
    UnityConnectionError,
)


# =============================================================================
# Fixtures
# =============================================================================

@pytest.fixture
def runner():
    """Create a CLI test runner."""
    return CliRunner()


@pytest.fixture
def mock_config():
    """Create a mock CLI configuration."""
    return CLIConfig(
        host="127.0.0.1",
        port=8080,
        timeout=30,
        format="text",
        unity_instance=None,
    )


@pytest.fixture
def mock_unity_response():
    """Standard successful Unity response."""
    return {
        "success": True,
        "message": "Operation successful",
        "data": {"test": "data"}
    }


@pytest.fixture
def mock_instances_response():
    """Mock Unity instances response."""
    return {
        "success": True,
        "instances": [
            {
                "session_id": "test-session-123",
                "project": "TestProject",
                "hash": "abc123def456",
                "unity_version": "2022.3.10f1",
                "connected_at": "2024-01-01T00:00:00Z",
            }
        ]
    }


# =============================================================================
# Config Tests
# =============================================================================

class TestConfig:
    """Tests for CLI configuration."""

    def test_default_config(self):
        """Test default configuration values."""
        config = CLIConfig()
        assert config.host == "127.0.0.1"
        assert config.port == 8080
        assert config.timeout == 30
        assert config.format == "text"
        assert config.unity_instance is None

    def test_config_from_env(self, monkeypatch):
        """Test configuration from environment variables."""
        monkeypatch.setenv("UNITY_MCP_HOST", "192.168.1.100")
        monkeypatch.setenv("UNITY_MCP_HTTP_PORT", "9090")
        monkeypatch.setenv("UNITY_MCP_TIMEOUT", "60")
        monkeypatch.setenv("UNITY_MCP_FORMAT", "json")
        monkeypatch.setenv("UNITY_MCP_INSTANCE", "MyProject")

        config = CLIConfig.from_env()
        assert config.host == "192.168.1.100"
        assert config.port == 9090
        assert config.timeout == 60
        assert config.format == "json"
        assert config.unity_instance == "MyProject"

    def test_set_and_get_config(self, mock_config):
        """Test setting and getting global config."""
        set_config(mock_config)
        retrieved = get_config()
        assert retrieved.host == mock_config.host
        assert retrieved.port == mock_config.port


# =============================================================================
# Output Formatting Tests
# =============================================================================

class TestOutputFormatting:
    """Tests for output formatting utilities."""

    def test_format_as_json(self):
        """Test JSON formatting."""
        data = {"key": "value", "number": 42}
        result = format_as_json(data)
        parsed = json.loads(result)
        assert parsed == data

    def test_format_as_json_with_complex_types(self):
        """Test JSON formatting with complex types."""
        from datetime import datetime
        data = {"timestamp": datetime(2024, 1, 1)}
        result = format_as_json(data)
        assert "2024" in result

    def test_format_as_text_success_response(self):
        """Test text formatting for success response."""
        data = {
            "success": True,
            "message": "OK",
            "data": {"name": "Player", "id": 123}
        }
        result = format_as_text(data)
        assert "name" in result
        assert "Player" in result

    def test_format_as_text_error_response(self):
        """Test text formatting for error response."""
        data = {"success": False, "error": "Something went wrong"}
        result = format_as_text(data)
        assert "Error" in result
        assert "Something went wrong" in result

    def test_format_as_text_list(self):
        """Test text formatting for lists."""
        data = [{"name": "Item1"}, {"name": "Item2"}]
        result = format_as_text(data)
        assert "2 items" in result

    def test_format_as_table(self):
        """Test table formatting."""
        data = [
            {"name": "Player", "id": 1},
            {"name": "Enemy", "id": 2},
        ]
        result = format_as_table(data)
        assert "name" in result
        assert "Player" in result
        assert "Enemy" in result

    def test_format_output_dispatch(self):
        """Test format_output dispatches correctly."""
        data = {"key": "value"}

        json_result = format_output(data, "json")
        assert json.loads(json_result) == data

        text_result = format_output(data, "text")
        assert "key" in text_result

        table_result = format_output(data, "table")
        assert "key" in table_result.lower() or "Key" in table_result


# =============================================================================
# Connection Tests
# =============================================================================

class TestConnection:
    """Tests for connection utilities."""

    @pytest.mark.asyncio
    async def test_check_connection_success(self):
        """Test successful connection check."""
        mock_response = MagicMock()
        mock_response.status_code = 200

        with patch("httpx.AsyncClient") as mock_client:
            mock_client.return_value.__aenter__.return_value.get = AsyncMock(
                return_value=mock_response
            )
            result = await check_connection()
            assert result is True

    @pytest.mark.asyncio
    async def test_check_connection_failure(self):
        """Test failed connection check."""
        with patch("httpx.AsyncClient") as mock_client:
            mock_client.return_value.__aenter__.return_value.get = AsyncMock(
                side_effect=Exception("Connection refused")
            )
            result = await check_connection()
            assert result is False

    @pytest.mark.asyncio
    async def test_send_command_success(self, mock_unity_response):
        """Test successful command sending."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = mock_unity_response

        with patch("httpx.AsyncClient") as mock_client:
            mock_client.return_value.__aenter__.return_value.post = AsyncMock(
                return_value=mock_response
            )
            mock_response.raise_for_status = MagicMock()

            result = await send_command("test_command", {"param": "value"})
            assert result == mock_unity_response

    @pytest.mark.asyncio
    async def test_send_command_connection_error(self):
        """Test command sending with connection error."""
        with patch("httpx.AsyncClient") as mock_client:
            mock_client.return_value.__aenter__.return_value.post = AsyncMock(
                side_effect=Exception("Connection refused")
            )

            with pytest.raises(UnityConnectionError):
                await send_command("test_command", {})


# =============================================================================
# CLI Command Tests
# =============================================================================

class TestCLICommands:
    """Tests for CLI commands."""

    def test_cli_help(self, runner):
        """Test CLI help command."""
        result = runner.invoke(cli, ["--help"])
        assert result.exit_code == 0
        assert "Unity MCP Command Line Interface" in result.output

    def test_cli_version(self, runner):
        """Test CLI version command."""
        result = runner.invoke(cli, ["--version"])
        assert result.exit_code == 0

    def test_status_connected(self, runner, mock_instances_response):
        """Test status command when connected."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", return_value=mock_instances_response):
                result = runner.invoke(cli, ["status"])
                assert result.exit_code == 0
                assert "Connected" in result.output

    def test_status_disconnected(self, runner):
        """Test status command when disconnected."""
        with patch("cli.main.run_check_connection", return_value=False):
            result = runner.invoke(cli, ["status"])
            assert result.exit_code == 1
            assert "Cannot connect" in result.output

    def test_instances_command(self, runner, mock_instances_response):
        """Test instances command."""
        with patch("cli.main.run_list_instances", return_value=mock_instances_response):
            result = runner.invoke(cli, ["instances"])
            assert result.exit_code == 0

    def test_raw_command(self, runner, mock_unity_response):
        """Test raw command."""
        with patch("cli.main.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["raw", "test_command", '{"param": "value"}'])
            assert result.exit_code == 0

    def test_raw_command_invalid_json(self, runner):
        """Test raw command with invalid JSON."""
        result = runner.invoke(cli, ["raw", "test_command", "invalid json"])
        assert result.exit_code == 1
        assert "Invalid JSON" in result.output


# =============================================================================
# GameObject Command Tests
# =============================================================================

class TestGameObjectCommands:
    """Tests for GameObject CLI commands."""

    def test_gameobject_find(self, runner, mock_unity_response):
        """Test gameobject find command."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["gameobject", "find", "Player"])
            assert result.exit_code == 0

    def test_gameobject_find_with_options(self, runner, mock_unity_response):
        """Test gameobject find with options."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "gameobject", "find", "Enemy",
                "--method", "by_tag",
                "--include-inactive",
                "--limit", "100"
            ])
            assert result.exit_code == 0

    def test_gameobject_create(self, runner, mock_unity_response):
        """Test gameobject create command."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["gameobject", "create", "NewObject"])
            assert result.exit_code == 0

    def test_gameobject_create_with_primitive(self, runner, mock_unity_response):
        """Test gameobject create with primitive."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "gameobject", "create", "MyCube",
                "--primitive", "Cube",
                "--position", "0", "1", "0"
            ])
            assert result.exit_code == 0

    def test_gameobject_modify(self, runner, mock_unity_response):
        """Test gameobject modify command."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "gameobject", "modify", "Player",
                "--position", "0", "5", "0"
            ])
            assert result.exit_code == 0

    def test_gameobject_delete(self, runner, mock_unity_response):
        """Test gameobject delete command."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["gameobject", "delete", "OldObject", "--force"])
            assert result.exit_code == 0

    def test_gameobject_delete_confirmation(self, runner, mock_unity_response):
        """Test gameobject delete with confirmation prompt."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["gameobject", "delete", "OldObject"], input="y\n")
            assert result.exit_code == 0

    def test_gameobject_duplicate(self, runner, mock_unity_response):
        """Test gameobject duplicate command."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "gameobject", "duplicate", "Player",
                "--name", "Player2",
                "--offset", "5", "0", "0"
            ])
            assert result.exit_code == 0

    def test_gameobject_move(self, runner, mock_unity_response):
        """Test gameobject move command."""
        with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "gameobject", "move", "Chair",
                "--reference", "Table",
                "--direction", "right",
                "--distance", "2"
            ])
            assert result.exit_code == 0


# =============================================================================
# Component Command Tests
# =============================================================================

class TestComponentCommands:
    """Tests for Component CLI commands."""

    def test_component_add(self, runner, mock_unity_response):
        """Test component add command."""
        with patch("cli.commands.component.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["component", "add", "Player", "Rigidbody"])
            assert result.exit_code == 0

    def test_component_remove(self, runner, mock_unity_response):
        """Test component remove command."""
        with patch("cli.commands.component.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["component", "remove", "Player", "Rigidbody", "--force"])
            assert result.exit_code == 0

    def test_component_set(self, runner, mock_unity_response):
        """Test component set command."""
        with patch("cli.commands.component.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["component", "set", "Player", "Rigidbody", "mass", "5.0"])
            assert result.exit_code == 0

    def test_component_modify(self, runner, mock_unity_response):
        """Test component modify command."""
        with patch("cli.commands.component.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "component", "modify", "Player", "Rigidbody",
                "--properties", '{"mass": 5.0, "useGravity": false}'
            ])
            assert result.exit_code == 0


# =============================================================================
# Scene Command Tests
# =============================================================================

class TestSceneCommands:
    """Tests for Scene CLI commands."""

    def test_scene_hierarchy(self, runner, mock_unity_response):
        """Test scene hierarchy command."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["scene", "hierarchy"])
            assert result.exit_code == 0

    def test_scene_hierarchy_with_options(self, runner, mock_unity_response):
        """Test scene hierarchy with options."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "scene", "hierarchy",
                "--max-depth", "5",
                "--include-transform"
            ])
            assert result.exit_code == 0

    def test_scene_active(self, runner, mock_unity_response):
        """Test scene active command."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["scene", "active"])
            assert result.exit_code == 0

    def test_scene_load(self, runner, mock_unity_response):
        """Test scene load command."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["scene", "load", "Assets/Scenes/Main.unity"])
            assert result.exit_code == 0

    def test_scene_save(self, runner, mock_unity_response):
        """Test scene save command."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["scene", "save"])
            assert result.exit_code == 0

    def test_scene_create(self, runner, mock_unity_response):
        """Test scene create command."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["scene", "create", "NewLevel"])
            assert result.exit_code == 0

    def test_scene_screenshot(self, runner, mock_unity_response):
        """Test scene screenshot command."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["scene", "screenshot", "--filename", "test"])
            assert result.exit_code == 0


# =============================================================================
# Asset Command Tests
# =============================================================================

class TestAssetCommands:
    """Tests for Asset CLI commands."""

    def test_asset_search(self, runner, mock_unity_response):
        """Test asset search command."""
        with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["asset", "search", "*.prefab"])
            assert result.exit_code == 0

    def test_asset_info(self, runner, mock_unity_response):
        """Test asset info command."""
        with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["asset", "info", "Assets/Materials/Red.mat"])
            assert result.exit_code == 0

    def test_asset_create(self, runner, mock_unity_response):
        """Test asset create command."""
        with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["asset", "create", "Assets/Materials/New.mat", "Material"])
            assert result.exit_code == 0

    def test_asset_delete(self, runner, mock_unity_response):
        """Test asset delete command."""
        with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["asset", "delete", "Assets/Old.mat", "--force"])
            assert result.exit_code == 0

    def test_asset_duplicate(self, runner, mock_unity_response):
        """Test asset duplicate command."""
        with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "asset", "duplicate",
                "Assets/Materials/Red.mat",
                "Assets/Materials/RedCopy.mat"
            ])
            assert result.exit_code == 0

    def test_asset_move(self, runner, mock_unity_response):
        """Test asset move command."""
        with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "asset", "move",
                "Assets/Old/Mat.mat",
                "Assets/New/Mat.mat"
            ])
            assert result.exit_code == 0

    def test_asset_mkdir(self, runner, mock_unity_response):
        """Test asset mkdir command."""
        with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["asset", "mkdir", "Assets/NewFolder"])
            assert result.exit_code == 0


# =============================================================================
# Editor Command Tests
# =============================================================================

class TestEditorCommands:
    """Tests for Editor CLI commands."""

    def test_editor_play(self, runner, mock_unity_response):
        """Test editor play command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "play"])
            assert result.exit_code == 0

    def test_editor_pause(self, runner, mock_unity_response):
        """Test editor pause command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "pause"])
            assert result.exit_code == 0

    def test_editor_stop(self, runner, mock_unity_response):
        """Test editor stop command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "stop"])
            assert result.exit_code == 0

    def test_editor_console(self, runner, mock_unity_response):
        """Test editor console command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "console"])
            assert result.exit_code == 0

    def test_editor_console_clear(self, runner, mock_unity_response):
        """Test editor console clear command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "console", "--clear"])
            assert result.exit_code == 0

    def test_editor_add_tag(self, runner, mock_unity_response):
        """Test editor add-tag command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "add-tag", "Enemy"])
            assert result.exit_code == 0

    def test_editor_add_layer(self, runner, mock_unity_response):
        """Test editor add-layer command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["editor", "add-layer", "Interactable"])
            assert result.exit_code == 0

    def test_editor_menu(self, runner, mock_unity_response):
        """Test editor menu command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "menu", "File/Save"])
            assert result.exit_code == 0

    def test_editor_tests(self, runner, mock_unity_response):
        """Test editor tests command."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["editor", "tests", "--mode", "EditMode"])
            assert result.exit_code == 0


# =============================================================================
# Prefab Command Tests
# =============================================================================

class TestPrefabCommands:
    """Tests for Prefab CLI commands."""

    def test_prefab_open(self, runner, mock_unity_response):
        """Test prefab open command."""
        with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["prefab", "open", "Assets/Prefabs/Player.prefab"])
            assert result.exit_code == 0

    def test_prefab_close(self, runner, mock_unity_response):
        """Test prefab close command."""
        with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["prefab", "close"])
            assert result.exit_code == 0

    def test_prefab_save(self, runner, mock_unity_response):
        """Test prefab save command."""
        with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["prefab", "save"])
            assert result.exit_code == 0

    def test_prefab_create(self, runner, mock_unity_response):
        """Test prefab create command."""
        with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "prefab", "create", "Player", "Assets/Prefabs/Player.prefab"
            ])
            assert result.exit_code == 0


# =============================================================================
# Material Command Tests
# =============================================================================

class TestMaterialCommands:
    """Tests for Material CLI commands."""

    def test_material_info(self, runner, mock_unity_response):
        """Test material info command."""
        with patch("cli.commands.material.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["material", "info", "Assets/Materials/Red.mat"])
            assert result.exit_code == 0

    def test_material_create(self, runner, mock_unity_response):
        """Test material create command."""
        with patch("cli.commands.material.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["material", "create", "Assets/Materials/New.mat"])
            assert result.exit_code == 0

    def test_material_set_color(self, runner, mock_unity_response):
        """Test material set-color command."""
        with patch("cli.commands.material.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "material", "set-color", "Assets/Materials/Red.mat",
                "1", "0", "0"
            ])
            assert result.exit_code == 0

    def test_material_set_property(self, runner, mock_unity_response):
        """Test material set-property command."""
        with patch("cli.commands.material.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "material", "set-property", "Assets/Materials/Mat.mat",
                "_Metallic", "0.5"
            ])
            assert result.exit_code == 0

    def test_material_assign(self, runner, mock_unity_response):
        """Test material assign command."""
        with patch("cli.commands.material.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "material", "assign", "Assets/Materials/Red.mat", "Cube"
            ])
            assert result.exit_code == 0


# =============================================================================
# Script Command Tests
# =============================================================================

class TestScriptCommands:
    """Tests for Script CLI commands."""

    def test_script_create(self, runner, mock_unity_response):
        """Test script create command."""
        with patch("cli.commands.script.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["script", "create", "PlayerController"])
            assert result.exit_code == 0

    def test_script_create_with_options(self, runner, mock_unity_response):
        """Test script create with options."""
        with patch("cli.commands.script.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "script", "create", "EnemyData",
                "--type", "ScriptableObject",
                "--namespace", "MyGame"
            ])
            assert result.exit_code == 0

    def test_script_read(self, runner):
        """Test script read command."""
        mock_response = {
            "success": True,
            "data": {"content": "using UnityEngine;\n\npublic class Test {}"}
        }
        with patch("cli.commands.script.run_command", return_value=mock_response):
            result = runner.invoke(
                cli, ["script", "read", "Assets/Scripts/Test.cs"])
            assert result.exit_code == 0

    def test_script_delete(self, runner, mock_unity_response):
        """Test script delete command."""
        with patch("cli.commands.script.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["script", "delete", "Assets/Scripts/Old.cs", "--force"])
            assert result.exit_code == 0


# =============================================================================
# Global Options Tests
# =============================================================================

class TestGlobalOptions:
    """Tests for global CLI options."""

    def test_custom_host(self, runner, mock_unity_response):
        """Test custom host option."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", return_value={"instances": []}):
                result = runner.invoke(
                    cli, ["--host", "192.168.1.100", "status"])
                assert result.exit_code == 0

    def test_custom_port(self, runner, mock_unity_response):
        """Test custom port option."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", return_value={"instances": []}):
                result = runner.invoke(cli, ["--port", "9090", "status"])
                assert result.exit_code == 0

    def test_json_format(self, runner, mock_unity_response):
        """Test JSON output format."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["--format", "json", "scene", "active"])
            assert result.exit_code == 0

    def test_table_format(self, runner, mock_unity_response):
        """Test table output format."""
        with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["--format", "table", "scene", "active"])
            assert result.exit_code == 0

    def test_timeout_option(self, runner, mock_unity_response):
        """Test timeout option."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", return_value={"instances": []}):
                result = runner.invoke(cli, ["--timeout", "60", "status"])
                assert result.exit_code == 0


# =============================================================================
# Error Handling Tests
# =============================================================================

class TestErrorHandling:
    """Tests for error handling."""

    def test_connection_error_handling(self, runner):
        """Test connection error is handled gracefully."""
        with patch("cli.commands.scene.run_command", side_effect=UnityConnectionError("Connection failed")):
            result = runner.invoke(cli, ["scene", "hierarchy"])
            assert result.exit_code == 1
            assert "Connection failed" in result.output or "Error" in result.output

    def test_invalid_json_params(self, runner):
        """Test invalid JSON parameters are handled."""
        result = runner.invoke(cli, [
            "component", "modify", "Player", "Rigidbody",
            "--properties", "not valid json"
        ])
        assert result.exit_code == 1
        assert "Invalid JSON" in result.output

    def test_missing_required_argument(self, runner):
        """Test missing required argument."""
        result = runner.invoke(cli, ["gameobject", "find"])
        assert result.exit_code != 0
        assert "Missing argument" in result.output


# =============================================================================
# Integration-style Tests (with mocked responses)
# =============================================================================

class TestIntegration:
    """Integration-style tests with realistic response data."""

    def test_full_gameobject_workflow(self, runner):
        """Test a full GameObject workflow."""
        create_response = {
            "success": True,
            "message": "GameObject created",
            "data": {"instanceID": -12345, "name": "TestObject"}
        }
        modify_response = {
            "success": True,
            "message": "GameObject modified"
        }
        delete_response = {
            "success": True,
            "message": "GameObject deleted"
        }

        # Create
        with patch("cli.commands.gameobject.run_command", return_value=create_response):
            result = runner.invoke(
                cli, ["gameobject", "create", "TestObject", "--primitive", "Cube"])
            assert result.exit_code == 0
            assert "Created" in result.output

        # Modify
        with patch("cli.commands.gameobject.run_command", return_value=modify_response):
            result = runner.invoke(
                cli, ["gameobject", "modify", "TestObject", "--position", "0", "5", "0"])
            assert result.exit_code == 0

        # Delete
        with patch("cli.commands.gameobject.run_command", return_value=delete_response):
            result = runner.invoke(
                cli, ["gameobject", "delete", "TestObject", "--force"])
            assert result.exit_code == 0
            assert "Deleted" in result.output

    def test_scene_hierarchy_with_data(self, runner):
        """Test scene hierarchy with realistic data."""
        hierarchy_response = {
            "success": True,
            "data": {
                "nodes": [
                    {"name": "Main Camera", "instanceID": -100, "childCount": 0},
                    {"name": "Directional Light",
                        "instanceID": -200, "childCount": 0},
                    {"name": "Player", "instanceID": -300, "childCount": 2},
                ]
            }
        }

        with patch("cli.commands.scene.run_command", return_value=hierarchy_response):
            result = runner.invoke(cli, ["scene", "hierarchy"])
            assert result.exit_code == 0

    def test_find_gameobjects_with_results(self, runner):
        """Test finding GameObjects with results."""
        find_response = {
            "success": True,
            "message": "Found 3 GameObjects",
            "data": {
                "instanceIDs": [-100, -200, -300],
                "count": 3,
                "hasMore": False
            }
        }

        with patch("cli.commands.gameobject.run_command", return_value=find_response):
            result = runner.invoke(cli, ["gameobject", "find", "Camera"])
            assert result.exit_code == 0


# =============================================================================
# Instance Command Tests
# =============================================================================

class TestInstanceCommands:
    """Tests for instance management commands."""

    def test_instance_list(self, runner):
        """Test listing Unity instances."""
        mock_instances = {
            "instances": [
                {"project": "TestProject", "hash": "abc123",
                    "unity_version": "2022.3.10f1", "session_id": "sess-1"}
            ]
        }
        with patch("cli.commands.instance.run_list_instances", return_value=mock_instances):
            result = runner.invoke(cli, ["instance", "list"])
            assert result.exit_code == 0
            assert "TestProject" in result.output

    def test_instance_set(self, runner, mock_unity_response):
        """Test setting active instance."""
        with patch("cli.commands.instance.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["instance", "set", "TestProject@abc123"])
            assert result.exit_code == 0

    def test_instance_current(self, runner):
        """Test showing current instance."""
        result = runner.invoke(cli, ["instance", "current"])
        assert result.exit_code == 0
        # Should show info message about no instance set
        assert "instance" in result.output.lower()


# =============================================================================
# Shader Command Tests
# =============================================================================

class TestShaderCommands:
    """Tests for shader commands."""

    def test_shader_read(self, runner):
        """Test reading a shader."""
        read_response = {
            "success": True,
            "data": {"contents": "Shader \"Custom/Test\" { ... }"}
        }
        with patch("cli.commands.shader.run_command", return_value=read_response):
            result = runner.invoke(
                cli, ["shader", "read", "Assets/Shaders/Test.shader"])
            assert result.exit_code == 0

    def test_shader_create(self, runner, mock_unity_response):
        """Test creating a shader."""
        with patch("cli.commands.shader.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["shader", "create", "NewShader", "--path", "Assets/Shaders"])
            assert result.exit_code == 0

    def test_shader_delete(self, runner, mock_unity_response):
        """Test deleting a shader."""
        with patch("cli.commands.shader.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["shader", "delete", "Assets/Shaders/Old.shader", "--force"])
            assert result.exit_code == 0


# =============================================================================
# VFX Command Tests
# =============================================================================

class TestVfxCommands:
    """Tests for VFX commands."""

    def test_vfx_particle_info(self, runner, mock_unity_response):
        """Test getting particle system info."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["vfx", "particle", "info", "Fire"])
            assert result.exit_code == 0

    def test_vfx_particle_play(self, runner, mock_unity_response):
        """Test playing a particle system."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["vfx", "particle", "play", "Fire"])
            assert result.exit_code == 0

    def test_vfx_particle_stop(self, runner, mock_unity_response):
        """Test stopping a particle system."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["vfx", "particle", "stop", "Fire"])
            assert result.exit_code == 0

    def test_vfx_line_info(self, runner, mock_unity_response):
        """Test getting line renderer info."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["vfx", "line", "info", "LaserBeam"])
            assert result.exit_code == 0

    def test_vfx_line_create_line(self, runner, mock_unity_response):
        """Test creating a line."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["vfx", "line", "create-line", "Line", "--start", "0", "0", "0", "--end", "10", "5", "0"])
            assert result.exit_code == 0

    def test_vfx_line_create_circle(self, runner, mock_unity_response):
        """Test creating a circle."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["vfx", "line", "create-circle", "Circle", "--radius", "5"])
            assert result.exit_code == 0

    def test_vfx_trail_info(self, runner, mock_unity_response):
        """Test getting trail renderer info."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["vfx", "trail", "info", "Trail"])
            assert result.exit_code == 0

    def test_vfx_trail_set_time(self, runner, mock_unity_response):
        """Test setting trail time."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["vfx", "trail", "set-time", "Trail", "2.0"])
            assert result.exit_code == 0

    def test_vfx_raw(self, runner, mock_unity_response):
        """Test raw VFX action."""
        with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["vfx", "raw", "particle_set_main", "Fire", "--params", '{"duration": 5}'])
            assert result.exit_code == 0

    def test_vfx_raw_invalid_json(self, runner):
        """Test raw VFX action with invalid JSON."""
        result = runner.invoke(
            cli, ["vfx", "raw", "particle_set_main", "Fire", "--params", "invalid json"])
        assert result.exit_code == 1
        assert "Invalid JSON" in result.output


# =============================================================================
# Batch Command Tests
# =============================================================================

class TestBatchCommands:
    """Tests for batch commands."""

    def test_batch_inline(self, runner, mock_unity_response):
        """Test inline batch execution."""
        batch_response = {
            "success": True,
            "data": {"results": [{"success": True}]}
        }
        with patch("cli.commands.batch.run_command", return_value=batch_response):
            result = runner.invoke(
                cli, ["batch", "inline", '[{"tool": "manage_scene", "params": {"action": "get_active"}}]'])
            assert result.exit_code == 0

    def test_batch_inline_invalid_json(self, runner):
        """Test inline batch with invalid JSON."""
        result = runner.invoke(cli, ["batch", "inline", "not valid json"])
        assert result.exit_code == 1
        assert "Invalid JSON" in result.output

    def test_batch_template(self, runner):
        """Test generating batch template."""
        result = runner.invoke(cli, ["batch", "template"])
        assert result.exit_code == 0
        # Template should be valid JSON
        import json
        template = json.loads(result.output)
        assert isinstance(template, list)
        assert len(template) > 0
        assert "tool" in template[0]

    def test_batch_run_file(self, runner, tmp_path, mock_unity_response):
        """Test running batch from file."""
        # Create a temp batch file
        batch_file = tmp_path / "commands.json"
        batch_file.write_text(
            '[{"tool": "manage_scene", "params": {"action": "get_active"}}]')

        batch_response = {
            "success": True,
            "data": {"results": [{"success": True}]}
        }
        with patch("cli.commands.batch.run_command", return_value=batch_response):
            result = runner.invoke(cli, ["batch", "run", str(batch_file)])
            assert result.exit_code == 0


# =============================================================================
# Enhanced Editor Command Tests
# =============================================================================

class TestEditorEnhancedCommands:
    """Tests for new editor subcommands."""

    def test_editor_refresh(self, runner, mock_unity_response):
        """Test editor refresh."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "refresh"])
            assert result.exit_code == 0

    def test_editor_refresh_with_compile(self, runner, mock_unity_response):
        """Test editor refresh with compile flag."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "refresh", "--compile"])
            assert result.exit_code == 0

    def test_editor_custom_tool(self, runner, mock_unity_response):
        """Test executing custom tool."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, ["editor", "custom-tool", "MyTool"])
            assert result.exit_code == 0

    def test_editor_custom_tool_with_params(self, runner, mock_unity_response):
        """Test executing custom tool with parameters."""
        with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
            result = runner.invoke(
                cli, ["editor", "custom-tool", "BuildTool", "--params", '{"target": "Android"}'])
            assert result.exit_code == 0

    def test_editor_custom_tool_invalid_json(self, runner):
        """Test custom tool with invalid JSON params."""
        result = runner.invoke(
            cli, ["editor", "custom-tool", "MyTool", "--params", "bad json"])
        assert result.exit_code == 1
        assert "Invalid JSON" in result.output

    def test_editor_tests_async(self, runner):
        """Test async test execution."""
        async_response = {
            "success": True,
            "data": {"job_id": "test-job-123", "status": "running"}
        }
        with patch("cli.commands.editor.run_command", return_value=async_response):
            result = runner.invoke(cli, ["editor", "tests", "--async"])
            assert result.exit_code == 0
            assert "test-job-123" in result.output

    def test_editor_poll_test(self, runner):
        """Test polling test job."""
        poll_response = {
            "success": True,
            "data": {
                "job_id": "test-job-123",
                "status": "succeeded",
                "result": {"summary": {"total": 10, "passed": 10, "failed": 0}}
            }
        }
        with patch("cli.commands.editor.run_command", return_value=poll_response):
            result = runner.invoke(
                cli, ["editor", "poll-test", "test-job-123"])
            assert result.exit_code == 0


# =============================================================================
# Code Search Tests
# =============================================================================

class TestCodeSearchCommand:
    """Tests for code search command."""

    def test_code_search(self, runner):
        """Test code search."""
        # Mock manage_script response with file contents
        read_response = {
            "status": "success",
            "result": {
                "success": True,
                "data": {
                    "contents": "using UnityEngine;\n\npublic class Player : MonoBehaviour\n{\n    void Start() {}\n}\n",
                    "contentsEncoded": False,
                }
            }
        }
        with patch("cli.commands.code.run_command", return_value=read_response):
            result = runner.invoke(
                cli, ["code", "search", "class.*Player", "Assets/Scripts/Player.cs"])
            assert result.exit_code == 0
            assert "Line 3" in result.output
            assert "class Player" in result.output

    def test_code_search_no_matches(self, runner):
        """Test code search with no matches."""
        read_response = {
            "status": "success",
            "result": {
                "success": True,
                "data": {
                    "contents": "using UnityEngine;\n\npublic class Test : MonoBehaviour {}\n",
                    "contentsEncoded": False,
                }
            }
        }
        with patch("cli.commands.code.run_command", return_value=read_response):
            result = runner.invoke(
                cli, ["code", "search", "nonexistent", "Assets/Scripts/Test.cs"])
            assert result.exit_code == 0
            assert "No matches" in result.output

    def test_code_search_with_options(self, runner):
        """Test code search with options."""
        read_response = {
            "status": "success",
            "result": {
                "success": True,
                "data": {
                    "contents": "// TODO: implement this\n// FIXME: bug here\nclass Test {}\n",
                    "contentsEncoded": False,
                }
            }
        }
        with patch("cli.commands.code.run_command", return_value=read_response):
            result = runner.invoke(
                cli, ["code", "search", "TODO", "Assets/Utils.cs", "--max-results", "100", "--case-sensitive"])
            assert result.exit_code == 0
            assert "Line 1" in result.output




# =============================================================================
# Texture Command Tests
# =============================================================================

class TestTextureCommands:
    """Tests for Texture CLI commands."""

    def test_texture_create_basic(self, runner, mock_unity_response):
        """Test basic texture create command."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "create", "Assets/Textures/Red.png",
                "--color", "[255,0,0,255]"
            ])
            assert result.exit_code == 0

    def test_texture_create_with_hex_color(self, runner, mock_unity_response):
        """Test texture create with hex color."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "create", "Assets/Textures/Blue.png",
                "--color", "#0000FF"
            ])
            assert result.exit_code == 0

    def test_texture_create_with_pattern(self, runner, mock_unity_response):
        """Test texture create with pattern."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "create", "Assets/Textures/Checker.png",
                "--pattern", "checkerboard",
                "--width", "128",
                "--height", "128"
            ])
            assert result.exit_code == 0

    def test_texture_create_with_import_settings(self, runner, mock_unity_response):
        """Test texture create with import settings."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "create", "Assets/Textures/Sprite.png",
                "--import-settings", '{"texture_type": "sprite", "filter_mode": "point"}'
            ])
            assert result.exit_code == 0

    def test_texture_sprite_basic(self, runner, mock_unity_response):
        """Test sprite create command."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "sprite", "Assets/Sprites/Player.png"
            ])
            assert result.exit_code == 0

    def test_texture_sprite_with_color(self, runner, mock_unity_response):
        """Test sprite create with solid color."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "sprite", "Assets/Sprites/Green.png",
                "--color", "[0,255,0,255]"
            ])
            assert result.exit_code == 0

    def test_texture_sprite_with_pattern(self, runner, mock_unity_response):
        """Test sprite create with pattern."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "sprite", "Assets/Sprites/Dots.png",
                "--pattern", "dots",
                "--ppu", "50"
            ])
            assert result.exit_code == 0

    def test_texture_sprite_with_custom_pivot(self, runner, mock_unity_response):
        """Test sprite create with custom pivot."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "sprite", "Assets/Sprites/Custom.png",
                "--pivot", "[0.25,0.75]"
            ])
            assert result.exit_code == 0

    def test_texture_modify(self, runner, mock_unity_response):
        """Test texture modify command."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "modify", "Assets/Textures/Test.png",
                "--set-pixels", '{"x":0,"y":0,"width":10,"height":10,"color":[255,0,0,255]}'
            ])
            assert result.exit_code == 0

    def test_texture_delete(self, runner, mock_unity_response):
        """Test texture delete command."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "delete", "Assets/Textures/Old.png", "--force"
            ])
            assert result.exit_code == 0

    def test_texture_create_invalid_json(self, runner):
        """Test texture create with invalid JSON."""
        result = runner.invoke(cli, [
            "texture", "create", "Assets/Test.png",
            "--import-settings", "not valid json"
        ])
        assert result.exit_code == 1
        assert "Invalid JSON" in result.output

    def test_texture_sprite_color_and_pattern_precedence(self, runner, mock_unity_response):
        """Test that color takes precedence over default pattern in sprite command."""
        with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
            result = runner.invoke(cli, [
                "texture", "sprite", "Assets/Sprites/Solid.png",
                "--color", "[255,0,0,255]"
            ])
            assert result.exit_code == 0


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
