"""Unit tests for AI Brain Node.

This module tests the AI Brain Node's core functionality:
- Task type analysis
- AI options configuration
- State updates from results
- Node integration
"""

from unittest.mock import MagicMock, patch

import pytest
from claude_agent_sdk import ClaudeAgentOptions

from fwauto.nodes.ai_brain import (
    ai_brain_node,
    analyze_task_type,
    get_ai_options,
    update_state_from_result,
)
from fwauto.state import FirmwareState


@pytest.fixture
def base_state() -> FirmwareState:
    """Provide a base FirmwareState for testing."""
    return {
        "project_root": "/path/to/stm32/project",
        "firmware_path": "/path/to/firmware.hex",
        "platform": "stm32_keil",
        "build_status": "idle",
        "build_errors": [],
        "build_log": "",
        "iteration": 0,
        "max_retries": 3,
        "user_prompt": "",
        "execution_mode": "auto",
        "flash_status": "idle",
        "flash_output": "",
        "serial_port": "COM3",
        "baud_rate": 115200,
        "log_file_path": "",
        "log_analysis_result": "",
        "flash_retries": 0,
        "max_flash_retries": 3,
        "last_error": "",
    }


class TestAnalyzeTaskType:
    """Test task type analysis logic."""

    def test_build_errors_detected(self, base_state: FirmwareState):
        """
        Test that build errors trigger fix_build_errors task type.

        Given: State has build errors and error status
        When: analyze_task_type is called
        Then: Should return 'fix_build_errors'
        """
        state = {
            **base_state,
            "build_errors": ["error: undefined reference to 'HAL_GPIO_Init'"],
            "build_status": "error",
        }
        result = analyze_task_type(state)
        assert result == "fix_build_errors"

    def test_feature_mode(self, base_state: FirmwareState):
        """
        Test that feat mode triggers implement_feature task type.

        Given: State has execution_mode set to 'feat'
        When: analyze_task_type is called
        Then: Should return 'implement_feature'
        """
        state = {**base_state, "execution_mode": "feat"}
        result = analyze_task_type(state)
        assert result == "implement_feature"

    def test_fix_mode(self, base_state: FirmwareState):
        """
        Test that fix mode raises ValueError (not supported).

        Given: State has execution_mode set to 'fix'
        When: analyze_task_type is called
        Then: Should raise ValueError with clear error message
        """
        state = {**base_state, "execution_mode": "fix"}
        with pytest.raises(ValueError) as exc_info:
            analyze_task_type(state)
        assert "Invalid execution_mode for AI Brain: fix" in str(exc_info.value)
        assert "AI Brain only supports 'feat', 'rag' mode or build error auto-fix" in str(exc_info.value)

    def test_natural_language_mode(self, base_state: FirmwareState):
        """
        Test that natural mode raises ValueError (not supported).

        Given: State has execution_mode set to 'natural'
        When: analyze_task_type is called
        Then: Should raise ValueError with clear error message
        """
        state = {**base_state, "execution_mode": "natural"}
        with pytest.raises(ValueError) as exc_info:
            analyze_task_type(state)
        assert "Invalid execution_mode for AI Brain: natural" in str(exc_info.value)
        assert "AI Brain only supports 'feat', 'rag' mode or build error auto-fix" in str(exc_info.value)

    def test_default_mode(self, base_state: FirmwareState):
        """
        Test that unknown/unsupported mode raises ValueError.

        Given: State has no specific execution mode markers
        When: analyze_task_type is called
        Then: Should raise ValueError with clear error message
        """
        with pytest.raises(ValueError) as exc_info:
            analyze_task_type(base_state)
        assert "Invalid execution_mode for AI Brain" in str(exc_info.value)
        assert "AI Brain only supports 'feat', 'rag' mode or build error auto-fix" in str(exc_info.value)

    def test_build_errors_priority(self, base_state: FirmwareState):
        """
        Test that build errors have priority over execution mode.

        Given: State has both build errors and feat mode
        When: analyze_task_type is called
        Then: Should return 'fix_build_errors' (errors have priority)
        """
        state = {
            **base_state,
            "build_errors": ["error: syntax error"],
            "build_status": "error",
            "execution_mode": "feat",
        }
        result = analyze_task_type(state)
        assert result == "fix_build_errors"


class TestGetAIOptions:
    """Test AI options configuration."""

    def test_with_project_root(self, base_state: FirmwareState):
        """
        Test AI options include project directory when project_root is set.

        Given: State has a valid project_root
        When: get_ai_options is called
        Then: Should return options with add_dirs and cwd set to project_root
        """
        options = get_ai_options(base_state)

        assert isinstance(options, ClaudeAgentOptions)
        assert options.add_dirs == ["/path/to/stm32/project"]
        assert options.cwd == "/path/to/stm32/project"
        assert "Read" in options.allowed_tools
        assert "Write" in options.allowed_tools
        assert options.permission_mode == "bypassPermissions"

    def test_without_project_root(self, base_state: FirmwareState):
        """
        Test AI options work without project_root.

        Given: State has no project_root
        When: get_ai_options is called
        Then: Should return options with empty add_dirs and no cwd
        """
        state = {**base_state, "project_root": None}
        options = get_ai_options(state)

        assert isinstance(options, ClaudeAgentOptions)
        # When project_root is None, add_dirs should be empty list
        assert options.add_dirs == []
        # cwd should be None when no project_root
        assert options.cwd is None
        assert "Read" in options.allowed_tools

    def test_allowed_tools(self, base_state: FirmwareState):
        """
        Test that all required tools are allowed.

        Given: Valid state
        When: get_ai_options is called
        Then: Should include all necessary tools
        """
        options = get_ai_options(base_state)

        required_tools = ["Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "Bash"]
        for tool in required_tools:
            assert tool in options.allowed_tools

    def test_permission_mode(self, base_state: FirmwareState):
        """
        Test that permission mode is set correctly.

        Given: Valid state
        When: get_ai_options is called
        Then: Should set permission_mode to bypassPermissions
        """
        options = get_ai_options(base_state)
        assert options.permission_mode == "bypassPermissions"


class TestUpdateStateFromResult:
    """Test state updates from AI processing results."""

    def test_success_result(self, base_state: FirmwareState):
        """
        Test state update on successful AI processing.

        Given: AI processing succeeded
        When: update_state_from_result is called
        Then: Should update build_status to success and clear errors
        """
        result = {
            "success": True,
            "responses": ["Fixed successfully"],
            "task_type": "fix_build_errors",
        }
        output_state = update_state_from_result(base_state, result)

        assert output_state["build_status"] == "success"
        assert output_state["build_errors"] == []
        assert "fix_build_errors" in output_state["build_log"]

    def test_failure_result(self, base_state: FirmwareState):
        """
        Test state update on failed AI processing.

        Given: AI processing failed with error
        When: update_state_from_result is called
        Then: Should update build_status to error and set error details
        """
        result = {"success": False, "error": "Failed to fix build errors"}
        output_state = update_state_from_result(base_state, result)

        assert output_state["build_status"] == "error"
        assert "Failed to fix build errors" in output_state["build_errors"]
        assert "Failed to fix build errors" in output_state["build_log"]
        assert output_state["iteration"] == 1

    def test_iteration_increment(self, base_state: FirmwareState):
        """
        Test that iteration counter increments on failure.

        Given: AI processing failed and state has iteration count
        When: update_state_from_result is called
        Then: Should increment iteration counter
        """
        state = {**base_state, "iteration": 2}
        result = {"success": False, "error": "Build failed"}
        output_state = update_state_from_result(state, result)

        assert output_state["iteration"] == 3

    def test_state_preservation(self, base_state: FirmwareState):
        """
        Test that unrelated state fields are preserved.

        Given: State with various fields
        When: update_state_from_result is called
        Then: Should preserve unrelated fields
        """
        result = {"success": True, "task_type": "implement_feature"}
        output_state = update_state_from_result(base_state, result)

        assert output_state["project_root"] == base_state["project_root"]
        assert output_state["firmware_path"] == base_state["firmware_path"]
        assert output_state["platform"] == base_state["platform"]
        assert output_state["max_retries"] == base_state["max_retries"]


class TestAIBrainNode:
    """Test AI Brain Node integration."""

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_successful_execution(
        self, mock_prompt_manager: MagicMock, mock_query: MagicMock, base_state: FirmwareState
    ):
        """
        Test successful AI Brain node execution.

        Given: Valid state with build errors
        When: ai_brain_node is called
        Then: Should execute successfully and update state
        """
        # Mock prompt manager
        mock_prompt_manager.get_prompt.return_value = "Fix these build errors..."

        # Mock query to return async generator
        async def mock_async_gen():
            yield MagicMock(content=[])

        mock_query.return_value = mock_async_gen()

        state = {
            **base_state,
            "build_errors": ["error: syntax error"],
            "build_status": "error",
        }

        result = ai_brain_node(state)

        assert result["build_status"] == "success"
        mock_prompt_manager.get_prompt.assert_called_once()

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_query_exception_handling(
        self, mock_prompt_manager: MagicMock, mock_query: MagicMock, base_state: FirmwareState
    ):
        """
        Test exception handling during query execution.

        Given: Query raises an exception
        When: ai_brain_node is called
        Then: Should handle exception and return error state
        """
        # Mock prompt manager
        mock_prompt_manager.get_prompt.return_value = "Fix these build errors..."

        # Mock query to raise exception
        async def mock_async_gen():
            raise RuntimeError("API connection failed")
            yield  # Make it a generator

        mock_query.return_value = mock_async_gen()

        state = {
            **base_state,
            "build_errors": ["error: syntax error"],
            "build_status": "error",
        }

        result = ai_brain_node(state)

        assert result["build_status"] == "error"
        assert any("API connection failed" in err for err in result["build_errors"])

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_task_type_detection(
        self, mock_prompt_manager: MagicMock, mock_query: MagicMock, base_state: FirmwareState
    ):
        """
        Test that correct task type is detected and used.

        Given: State with feat execution mode
        When: ai_brain_node is called
        Then: Should detect implement_feature task type
        """
        # Mock prompt manager
        mock_prompt_manager.get_prompt.return_value = "Implement feature..."

        # Mock query
        async def mock_async_gen():
            yield MagicMock(content=[])

        mock_query.return_value = mock_async_gen()

        state = {**base_state, "execution_mode": "feat"}

        ai_brain_node(state)

        # Verify get_prompt was called with correct task type
        call_args = mock_prompt_manager.get_prompt.call_args
        assert call_args[0][0] == "implement_feature"

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_project_context_configuration(
        self, mock_prompt_manager: MagicMock, mock_query: MagicMock, base_state: FirmwareState
    ):
        """
        Test that project context is correctly configured.

        Given: State with project_root
        When: ai_brain_node is called
        Then: Should configure AI options with project directory
        """
        # Mock prompt manager
        mock_prompt_manager.get_prompt.return_value = "Fix build errors..."

        # Mock query and capture options
        captured_options = None

        async def mock_async_gen():
            yield MagicMock(content=[])

        def capture_query(prompt, options):
            nonlocal captured_options
            captured_options = options
            return mock_async_gen()

        mock_query.side_effect = capture_query

        state = {
            **base_state,
            "build_errors": ["error"],
            "build_status": "error",
        }

        ai_brain_node(state)

        # Verify options were configured correctly
        assert captured_options is not None
        assert captured_options.cwd == "/path/to/stm32/project"
        assert "/path/to/stm32/project" in captured_options.add_dirs
