#!/usr/bin/env python3
"""
Test suite for the parallel command functionality.
Tests all handle methods and input possibilities for the parallel command.
"""

import os
import sys
import pytest
from unittest.mock import patch, Mock, MagicMock

# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))

from cai.repl.commands.parallel import ParallelCommand, ParallelConfig
import cai.repl.commands.parallel as parallel_module
from cai.repl.commands.base import Command


class TestParallelCommand:
    """Test cases for ParallelCommand."""

    @pytest.fixture(autouse=True)
    def setup_and_cleanup(self):
        """Setup and cleanup for each test."""
        # Clear parallel configs before each test
        parallel_module.PARALLEL_CONFIGS.clear()

        # Set up test environment
        os.environ["CAI_TELEMETRY"] = "false"
        os.environ["CAI_TRACING"] = "false"

        yield

        # Cleanup after each test
        parallel_module.PARALLEL_CONFIGS.clear()

    @pytest.fixture
    def parallel_command(self):
        """Create a ParallelCommand instance for testing."""
        return ParallelCommand()

    def test_command_initialization(self, parallel_command):
        """Test that ParallelCommand initializes correctly."""
        assert parallel_command.name == "/parallel"
        assert (
            parallel_command.description
            == "Configure multiple agents to run in parallel with different settings"
        )
        assert parallel_command.aliases == ["/par", "/p"]

        # Check subcommands are registered
        expected_subcommands = [
            "add",
            "list",
            "clear",
            "remove",
            "override-models",
            "merge",
            "prompt",
            "run",
        ]
        assert set(parallel_command.get_subcommands()) == set(expected_subcommands)

    def test_parallel_config_initialization(self):
        """Test ParallelConfig initialization."""
        config = ParallelConfig("test_agent", "gpt-4", "Test prompt")
        assert config.agent_name == "test_agent"
        assert config.model == "gpt-4"
        assert config.prompt == "Test prompt"
        assert config.id is None  # ID should be None initially

        # Test default values
        config_default = ParallelConfig("test_agent")
        assert config_default.agent_name == "test_agent"
        assert config_default.model is None
        assert config_default.prompt is None
        assert config_default.id is None

    def test_parallel_config_str_representation(self):
        """Test ParallelConfig string representation."""
        # Test with all parameters
        config = ParallelConfig("test_agent", "gpt-4", "Test prompt")
        str_repr = str(config)
        assert "Agent: test_agent" in str_repr
        assert "model: gpt-4" in str_repr
        assert "prompt: 'Test prompt'" in str_repr

        # Test with long prompt (should be truncated)
        long_prompt = "This is a very long prompt that should be truncated when displayed"
        config_long = ParallelConfig("test_agent", "gpt-4", long_prompt)
        str_repr_long = str(config_long)
        assert "..." in str_repr_long

        # Test with minimal parameters
        config_minimal = ParallelConfig("test_agent")
        str_repr_minimal = str(config_minimal)
        assert "Agent: test_agent" in str_repr_minimal
        assert "model:" not in str_repr_minimal
        assert "prompt:" not in str_repr_minimal

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_handle_add_valid_agent(self, mock_get_agents, parallel_command):
        """Test adding a valid agent to parallel config."""
        # Mock available agents
        mock_get_agents.return_value = {"test_agent": Mock(), "another_agent": Mock()}

        # Test basic add
        result = parallel_command.handle_add(["test_agent"])
        assert result is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 1
        assert parallel_module.PARALLEL_CONFIGS[0].agent_name == "test_agent"
        assert parallel_module.PARALLEL_CONFIGS[0].model is None
        assert parallel_module.PARALLEL_CONFIGS[0].prompt is None
        assert parallel_module.PARALLEL_CONFIGS[0].id == "P1"  # Should be assigned P1

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_handle_add_with_model_and_prompt(self, mock_get_agents, parallel_command):
        """Test adding agent with model and prompt parameters."""
        mock_get_agents.return_value = {"test_agent": Mock()}

        args = ["test_agent", "--model", "gpt-4", "--prompt", "Custom prompt"]
        result = parallel_command.handle_add(args)

        assert result is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 1
        config = parallel_module.PARALLEL_CONFIGS[0]
        assert config.agent_name == "test_agent"
        assert config.model == "gpt-4"
        assert config.prompt == "Custom prompt"

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_handle_add_invalid_agent(self, mock_get_agents, parallel_command):
        """Test adding an invalid agent name."""
        mock_get_agents.return_value = {"valid_agent": Mock()}

        result = parallel_command.handle_add(["invalid_agent"])
        assert result is False
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    def test_handle_add_no_args(self, parallel_command):
        """Test add command with no arguments."""
        result = parallel_command.handle_add([])
        assert result is False
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_handle_add_multiple_agents(self, mock_get_agents, parallel_command):
        """Test adding multiple agents."""
        mock_get_agents.return_value = {"agent1": Mock(), "agent2": Mock(), "agent3": Mock()}

        # Add first agent
        result1 = parallel_command.handle_add(["agent1", "--model", "gpt-4"])
        assert result1 is True

        # Add second agent
        result2 = parallel_command.handle_add(["agent2", "--prompt", "Second prompt"])
        assert result2 is True

        # Add third agent
        result3 = parallel_command.handle_add(["agent3"])
        assert result3 is True

        assert len(parallel_module.PARALLEL_CONFIGS) == 3
        assert parallel_module.PARALLEL_CONFIGS[0].agent_name == "agent1"
        assert parallel_module.PARALLEL_CONFIGS[1].agent_name == "agent2"
        assert parallel_module.PARALLEL_CONFIGS[2].agent_name == "agent3"

        # Check IDs are assigned correctly
        assert parallel_module.PARALLEL_CONFIGS[0].id == "P1"
        assert parallel_module.PARALLEL_CONFIGS[1].id == "P2"
        assert parallel_module.PARALLEL_CONFIGS[2].id == "P3"

    def test_handle_list_empty(self, parallel_command):
        """Test listing when no parallel configs exist."""
        result = parallel_command.handle_list([])
        assert result is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    def test_handle_list_with_configs(self, parallel_command):
        """Test listing existing parallel configs."""
        # Add some configs
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent1", "gpt-4", "Prompt 1"))
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent2", None, None))
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent3", "claude", "Long prompt"))

        result = parallel_command.handle_list([])
        assert result is True

    def test_handle_clear_empty(self, parallel_command):
        """Test clearing empty parallel configs."""
        result = parallel_command.handle_clear([])
        assert result is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    def test_handle_clear_with_configs(self, parallel_command):
        """Test clearing existing parallel configs."""
        # Add some configs
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent1"))
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent2"))
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent3"))

        assert len(parallel_module.PARALLEL_CONFIGS) == 3

        result = parallel_command.handle_clear([])
        assert result is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    def test_handle_remove_valid_index(self, parallel_command):
        """Test removing a config by valid index."""
        # Add some configs
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent1"))
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent2"))
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent3"))

        # Remove the second config (index 2)
        result = parallel_command.handle_remove(["2"])
        assert result is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 2
        assert parallel_module.PARALLEL_CONFIGS[0].agent_name == "agent1"
        assert parallel_module.PARALLEL_CONFIGS[1].agent_name == "agent3"

    def test_handle_remove_invalid_index(self, parallel_command):
        """Test removing with invalid index."""
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent1"))

        # Test invalid numeric index
        result1 = parallel_command.handle_remove(["5"])
        assert result1 is False
        assert len(parallel_module.PARALLEL_CONFIGS) == 1

        # Test negative index
        result2 = parallel_command.handle_remove(["-1"])
        assert result2 is False
        assert len(parallel_module.PARALLEL_CONFIGS) == 1

        # Test non-numeric index
        result3 = parallel_command.handle_remove(["invalid"])
        assert result3 is False
        assert len(parallel_module.PARALLEL_CONFIGS) == 1

    def test_handle_remove_no_args(self, parallel_command):
        """Test remove command with no arguments."""
        parallel_module.PARALLEL_CONFIGS.append(ParallelConfig("agent1"))

        result = parallel_command.handle_remove([])
        assert result is False
        assert len(parallel_module.PARALLEL_CONFIGS) == 1

    def test_command_base_functionality(self, parallel_command):
        """Test that the command inherits from base Command properly."""
        assert isinstance(parallel_command, Command)
        assert parallel_command.name == "/parallel"
        assert "/par" in parallel_command.aliases
        assert "/p" in parallel_command.aliases

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_handle_main_command_routing(self, mock_get_agents, parallel_command):
        """Test that main handle method routes to correct subcommands."""
        mock_get_agents.return_value = {"test_agent": Mock()}

        # Test routing to add
        result1 = parallel_command.handle(["add", "test_agent"])
        assert result1 is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 1

        # Test routing to list
        result2 = parallel_command.handle(["list"])
        assert result2 is True

        # Test routing to clear
        result3 = parallel_command.handle(["clear"])
        assert result3 is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    def test_handle_unknown_subcommand(self, parallel_command):
        """Test handling of unknown subcommands."""
        # This will use the default handle method from base class
        # which should route to handle_unknown_subcommand
        result = parallel_command.handle(["unknown_subcommand"])
        assert result is False

    def test_handle_no_args(self, parallel_command):
        """Test handling when no arguments provided."""
        # The base handle method should route to handle_no_args
        result = parallel_command.handle([])
        assert result is True  # handle_no_args returns True when successful


@pytest.mark.integration
class TestParallelCommandIntegration:
    """Integration tests for parallel command functionality."""

    @pytest.fixture(autouse=True)
    def setup_integration(self):
        """Setup for integration tests."""
        parallel_module.PARALLEL_CONFIGS.clear()
        yield
        parallel_module.PARALLEL_CONFIGS.clear()

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_full_workflow(self, mock_get_agents):
        """Test a complete workflow of adding, listing, and removing configs."""
        mock_get_agents.return_value = {"agent1": Mock(), "agent2": Mock(), "agent3": Mock()}

        cmd = ParallelCommand()

        # Start with empty configs
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

        # Add multiple configs
        cmd.handle(["add", "agent1", "--model", "gpt-4"])
        cmd.handle(["add", "agent2", "--prompt", "Test prompt"])
        cmd.handle(["add", "agent3", "--model", "claude", "--prompt", "Another prompt"])

        assert len(parallel_module.PARALLEL_CONFIGS) == 3

        # List configs (should not change count)
        cmd.handle(["list"])
        assert len(parallel_module.PARALLEL_CONFIGS) == 3

        # Remove one config
        cmd.handle(["remove", "2"])
        assert len(parallel_module.PARALLEL_CONFIGS) == 2

        # Clear all configs
        cmd.handle(["clear"])
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_edge_case_combinations(self, mock_get_agents):
        """Test edge cases and unusual parameter combinations."""
        mock_get_agents.return_value = {"test_agent": Mock()}

        cmd = ParallelCommand()

        # Test partial parameters
        result1 = cmd.handle(["add", "test_agent", "--model"])
        assert result1 is True  # Should still work with incomplete args

        # Test empty string parameters
        result2 = cmd.handle(["add", "test_agent", "--prompt", ""])
        assert result2 is True

        # Test parameters in different order
        result3 = cmd.handle(["add", "test_agent", "--prompt", "Test", "--model", "gpt-4"])
        assert result3 is True

        assert len(parallel_module.PARALLEL_CONFIGS) == 3

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_handle_remove_by_id(self, mock_get_agents):
        """Test removing agents by ID."""
        mock_get_agents.return_value = {"agent1": Mock(), "agent2": Mock(), "agent3": Mock()}

        cmd = ParallelCommand()

        # Add multiple configs
        cmd.handle_add(["agent1"])
        cmd.handle_add(["agent2"])
        cmd.handle_add(["agent3"])

        assert len(parallel_module.PARALLEL_CONFIGS) == 3
        assert parallel_module.PARALLEL_CONFIGS[0].id == "P1"
        assert parallel_module.PARALLEL_CONFIGS[1].id == "P2"
        assert parallel_module.PARALLEL_CONFIGS[2].id == "P3"

        # Remove by ID
        result = cmd.handle_remove(["P2"])
        assert result is True
        assert len(parallel_module.PARALLEL_CONFIGS) == 2
        assert parallel_module.PARALLEL_CONFIGS[0].agent_name == "agent1"
        assert parallel_module.PARALLEL_CONFIGS[1].agent_name == "agent3"

        # Check IDs are reassigned after removal
        assert parallel_module.PARALLEL_CONFIGS[0].id == "P1"
        assert parallel_module.PARALLEL_CONFIGS[1].id == "P2"

        # Test invalid ID
        result2 = cmd.handle_remove(["P99"])
        assert result2 is False
        assert len(parallel_module.PARALLEL_CONFIGS) == 2

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_parse_agent_names_with_ids(self, mock_get_agents):
        """Test parsing agent names that includes IDs."""
        mock_agent = Mock()
        mock_agent.name = "Test Agent"

        mock_get_agents.return_value = {"test_agent": mock_agent}

        cmd = ParallelCommand()

        # Add agents to parallel_module.PARALLEL_CONFIGS
        cmd.handle_add(["test_agent"])
        cmd.handle_add(["test_agent"])

        # Mock all_histories to simulate agents with message history
        all_histories = {"Test Agent #1": [], "Test Agent #2": []}

        # Test parsing IDs
        result = cmd._parse_agent_names(["P1", "P2"], all_histories)
        assert len(result) == 2
        assert "Test Agent #1" in result
        assert "Test Agent #2" in result

        # Test mixed IDs and names
        result2 = cmd._parse_agent_names(["P1", "Test Agent #2"], all_histories)
        assert len(result2) == 2

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_merge_with_remove_sources(self, mock_get_agents):
        """Test merging agents with --remove-sources flag."""
        # This is a simplified test that just checks the remove functionality
        # The actual merge logic is complex and requires many mocks
        cmd = ParallelCommand()

        # Mock available agents
        mock_agent = Mock()
        mock_agent.name = "Test Agent"
        mock_get_agents.return_value = {"test_agent": mock_agent}

        # Add agents to parallel configs
        cmd.handle_add(["test_agent"])
        cmd.handle_add(["test_agent"])

        assert len(parallel_module.PARALLEL_CONFIGS) == 2

        # Test removal after merge
        # When we merge with --remove-sources and less than 2 agents remain,
        # all configs should be cleared
        cmd.handle_remove(["1"])
        assert len(parallel_module.PARALLEL_CONFIGS) == 1

        # Removing one more should clear all configs (less than 2 agents)
        # This simulates what happens after merge with --remove-sources
        cmd.handle_clear([])
        assert len(parallel_module.PARALLEL_CONFIGS) == 0

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    @patch("cai.repl.commands._parallel_monolith.get_all_agent_histories")
    def test_merge_case_insensitive(self, mock_get_histories, mock_get_agents):
        """Test that agent name matching is case-insensitive in merge."""
        mock_agent = Mock()
        mock_agent.name = "Test Agent"

        mock_get_agents.return_value = {"test_agent": mock_agent}

        # Mock message histories with mixed case
        mock_get_histories.return_value = {
            "Test Agent": [
                {"role": "user", "content": "Hello"},
                {"role": "assistant", "content": "Hi there"},
            ],
            "Another Agent": [
                {"role": "user", "content": "Greetings"},
                {"role": "assistant", "content": "Hello!"},
            ],
        }

        cmd = ParallelCommand()

        # Test case-insensitive parsing
        result = cmd._parse_agent_names(
            ["test agent", "ANOTHER AGENT"], mock_get_histories.return_value
        )
        assert len(result) == 2
        assert "Test Agent" in result
        assert "Another Agent" in result

    @patch("cai.repl.commands._parallel_monolith.get_available_agents")
    def test_handle_prompt_command(self, mock_get_agents):
        """Test the prompt subcommand."""
        mock_get_agents.return_value = {"test_agent": Mock(name="Test Agent")}

        cmd = ParallelCommand()

        # Add an agent
        cmd.handle_add(["test_agent"])
        assert parallel_module.PARALLEL_CONFIGS[0].prompt is None

        # Set prompt using ID
        result = cmd.handle_prompt(["P1", "Focus on SQL injection"])
        assert result is True
        assert parallel_module.PARALLEL_CONFIGS[0].prompt == "Focus on SQL injection"

        # Update prompt
        result2 = cmd.handle_prompt(["P1", "Look for XSS vulnerabilities"])
        assert result2 is True
        assert parallel_module.PARALLEL_CONFIGS[0].prompt == "Look for XSS vulnerabilities"

        # Test with invalid ID
        result3 = cmd.handle_prompt(["P99", "Invalid"])
        assert result3 is False

        # Test with no args
        result4 = cmd.handle_prompt([])
        assert result4 is False


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