"""
Tests for PlanAgent (WO-ECHO-PLAN-AGENT-01)

Tests covering:
- Plan generation from various request types
- Step-to-agent mapping
- Reflex safety checks
- Receipt generation with chain-hash
- Intent detection (detect_plan_intent)
- Work order ID generation
"""

import hashlib
import json
import pytest
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch, MagicMock, AsyncMock

# Configure pytest-anyio for async tests
pytest_plugins = ('anyio',)


@pytest.fixture
def anyio_backend():
    return 'asyncio'


# Import components
from helix_cdc.echo.agents.plan_agent import (
    PlanAgent,
    detect_plan_intent,
    STEP_KIND_TO_AGENT,
    PLAN_KEYWORDS,
    PLAN_PATTERNS,
)
from helix_cdc.echo.agents import AgentResult


class TestDetectPlanIntent:
    """Tests for plan intent detection."""

    def test_explicit_plan_keyword(self):
        """Test detection with explicit 'plan' keyword."""
        result = detect_plan_intent("create a plan to refactor the module")
        assert result["is_plan"] is True
        assert "plan" in result["matched_keywords"]
        assert result["confidence"] > 0.0

    def test_work_order_keyword(self):
        """Test detection with 'work order' keyword."""
        result = detect_plan_intent("generate a work order for this task")
        assert result["is_plan"] is True
        assert "work order" in result["matched_keywords"]

    def test_pattern_match(self):
        """Test detection via regex pattern."""
        result = detect_plan_intent("what steps should I take to fix the bug?")
        assert result["is_plan"] is True
        assert len(result["matched_patterns"]) > 0

    def test_strategy_keyword(self):
        """Test detection with 'strategy' keyword combined with another."""
        result = detect_plan_intent("what's the strategy and approach for implementing this?")
        assert result["is_plan"] is True
        assert "strategy" in result["matched_keywords"]
        assert "approach" in result["matched_keywords"]

    def test_decompose_keyword(self):
        """Test detection with 'decompose' or 'break down'."""
        result = detect_plan_intent("break down this task into smaller pieces")
        assert result["is_plan"] is True

    def test_no_plan_intent(self):
        """Test that normal queries don't trigger planning."""
        result = detect_plan_intent("what is the Se formula?")
        assert result["is_plan"] is False
        assert result["confidence"] < 0.3

    def test_simple_question_no_plan(self):
        """Test that simple questions don't trigger planning."""
        result = detect_plan_intent("where is the test file?")
        assert result["is_plan"] is False

    def test_confidence_increases_with_matches(self):
        """Test that confidence increases with more matches."""
        low = detect_plan_intent("fix the bug")
        high = detect_plan_intent("create a plan to design the strategy for fixing the bug")
        assert high["confidence"] > low["confidence"]

    def test_suggested_tool_when_plan(self):
        """Test that suggested_tool is set when plan detected."""
        result = detect_plan_intent("plan to implement the feature")
        assert result["suggested_tool"] == "plan_agent"

    def test_suggested_tool_none_when_not_plan(self):
        """Test that suggested_tool is None when no plan detected."""
        result = detect_plan_intent("show me the code")
        assert result["suggested_tool"] is None


class TestStepKindToAgentMapping:
    """Tests for step kind to agent mapping."""

    def test_read_maps_to_codebase(self):
        """Test that read steps map to codebase agent."""
        assert STEP_KIND_TO_AGENT["read"] == "codebase"

    def test_edit_maps_to_patch(self):
        """Test that edit steps map to patch agent."""
        assert STEP_KIND_TO_AGENT["edit"] == "patch"

    def test_run_cmd_maps_to_test(self):
        """Test that run_cmd steps map to test agent."""
        assert STEP_KIND_TO_AGENT["run_cmd"] == "test"

    def test_helix_tool_maps_to_test(self):
        """Test that helix_tool steps map to test agent."""
        assert STEP_KIND_TO_AGENT["helix_tool"] == "test"

    def test_symbolic_tool_no_agent(self):
        """Test that symbolic_tool has no agent yet."""
        assert STEP_KIND_TO_AGENT["symbolic_tool"] is None

    def test_report_no_agent(self):
        """Test that report steps have no agent."""
        assert STEP_KIND_TO_AGENT["report"] is None


class TestPlanAgentProperties:
    """Tests for PlanAgent properties."""

    def test_agent_type(self):
        """Test agent_type property."""
        agent = PlanAgent()
        assert agent.agent_type == "plan"

    def test_available_with_planner(self):
        """Test available() when planner is importable."""
        agent = PlanAgent()
        # Should be True since we have ginnie.planner
        assert agent.available() is True

    def test_default_receipt_dir(self):
        """Test default receipt directory."""
        agent = PlanAgent()
        assert "receipts/plan_agent" in str(agent.receipt_dir)

    def test_custom_receipt_dir(self):
        """Test custom receipt directory."""
        agent = PlanAgent(receipt_dir="custom/receipts")
        assert "custom/receipts" in str(agent.receipt_dir)


class TestPlanAgentWorkOrderID:
    """Tests for work order ID generation."""

    def test_wo_id_format(self):
        """Test that WO ID follows expected format."""
        agent = PlanAgent()
        wo_id = agent._generate_wo_id()
        assert wo_id.startswith("WO-PLAN-")
        assert len(wo_id) > 20

    def test_wo_id_unique(self):
        """Test that WO IDs are unique."""
        agent = PlanAgent()
        ids = [agent._generate_wo_id() for _ in range(10)]
        assert len(set(ids)) == 10


class TestPlanAgentExecution:
    """Tests for plan agent execution (mocked)."""

    @pytest.fixture
    def mock_planner(self):
        """Create mock planner components."""
        mock_plan = MagicMock()
        mock_plan.request_type = "modify_code"
        mock_plan.warnings = []
        mock_plan.notes = ["Test note"]
        mock_plan.reflex_applied = True
        mock_plan.to_dict.return_value = {
            "request_type": "modify_code",
            "steps": [],
            "warnings": [],
            "notes": ["Test note"],
        }

        mock_step = MagicMock()
        mock_step.kind = "edit"
        mock_step.description = "Edit the file"
        mock_step.target_paths = ["helix_cdc/test.py"]
        mock_step.tool_name = None
        mock_step.args = {}
        mock_step.priority = 1

        mock_plan.steps = [mock_step]

        return mock_plan

    @pytest.mark.anyio
    async def test_execute_basic(self, mock_planner):
        """Test basic plan execution."""
        agent = PlanAgent()

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner
            mock_reflex.return_value = mock_planner

            result = await agent.execute("plan to fix the bug", {})

            assert result.agent_type == "plan"
            assert result.result["status"] == "PASS"
            assert "wo_id" in result.result
            assert "steps" in result.result
            assert "receipt_path" in result.result

    @pytest.mark.anyio
    async def test_execute_with_warnings(self, mock_planner):
        """Test execution when plan has warnings."""
        agent = PlanAgent()
        mock_planner.warnings = ["WARNING: Consider adding tests"]

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner
            mock_reflex.return_value = mock_planner

            result = await agent.execute("plan to edit code", {})

            assert result.result["status"] == "PASS"  # Warnings don't cause FAIL
            assert len(result.result["warnings"]) == 1
            assert result.confidence == 0.7  # Lower confidence with warnings

    @pytest.mark.anyio
    async def test_execute_with_errors(self, mock_planner):
        """Test execution when plan has error warnings."""
        agent = PlanAgent()
        mock_planner.warnings = ["ERROR: rm -rf detected, refusing"]

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner
            mock_reflex.return_value = mock_planner

            result = await agent.execute("plan to rm -rf everything", {})

            assert result.result["status"] == "FAIL"
            assert result.confidence == 0.3

    @pytest.mark.anyio
    async def test_execute_preserves_wo_id_from_context(self, mock_planner):
        """Test that WO ID from context is preserved."""
        agent = PlanAgent()

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner
            mock_reflex.return_value = mock_planner

            result = await agent.execute("plan", {"wo_id": "WO-TEST-123"})

            assert result.result["wo_id"] == "WO-TEST-123"

    @pytest.mark.anyio
    async def test_execute_error_handling(self):
        """Test error handling during execution."""
        agent = PlanAgent()

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow:
            mock_pow.side_effect = Exception("Planner failed")

            result = await agent.execute("plan something", {})

            assert result.result["status"] == "ERROR"
            assert "Planner failed" in result.error
            assert result.confidence == 0.0


class TestPlanAgentStepMapping:
    """Tests for step-to-agent mapping."""

    @pytest.fixture
    def mock_planner_with_steps(self):
        """Create mock planner with various step types."""
        mock_plan = MagicMock()
        mock_plan.request_type = "modify_code"
        mock_plan.warnings = []
        mock_plan.notes = []
        mock_plan.reflex_applied = True

        steps = []
        for i, (kind, desc) in enumerate([
            ("read", "Read the source file"),
            ("edit", "Modify the function"),
            ("run_cmd", "Run tests"),
        ]):
            step = MagicMock()
            step.kind = kind
            step.description = desc
            step.target_paths = [f"file{i}.py"]
            step.tool_name = None
            step.args = {}
            step.priority = i + 1
            steps.append(step)

        mock_plan.steps = steps
        mock_plan.to_dict.return_value = {
            "request_type": "modify_code",
            "steps": [{"kind": s.kind, "description": s.description} for s in steps],
            "warnings": [],
            "notes": [],
        }

        return mock_plan

    @pytest.mark.anyio
    async def test_step_mapping(self, mock_planner_with_steps):
        """Test that steps are correctly mapped to agents."""
        agent = PlanAgent()

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner_with_steps
            mock_reflex.return_value = mock_planner_with_steps

            result = await agent.execute("plan to modify code", {})

            steps = result.result["steps"]
            assert len(steps) == 3

            # Check mappings
            assert steps[0]["kind"] == "read"
            assert steps[0]["agent"] == "codebase"

            assert steps[1]["kind"] == "edit"
            assert steps[1]["agent"] == "patch"

            assert steps[2]["kind"] == "run_cmd"
            assert steps[2]["agent"] == "test"


class TestPlanAgentReceipt:
    """Tests for receipt generation."""

    @pytest.fixture
    def mock_planner(self):
        """Create mock planner."""
        mock_plan = MagicMock()
        mock_plan.request_type = "read_only"
        mock_plan.warnings = []
        mock_plan.notes = []
        mock_plan.reflex_applied = False
        mock_plan.steps = []
        mock_plan.to_dict.return_value = {
            "request_type": "read_only",
            "steps": [],
            "warnings": [],
            "notes": [],
        }
        return mock_plan

    @pytest.mark.anyio
    async def test_receipt_created(self, mock_planner, tmp_path):
        """Test that receipt file is created."""
        agent = PlanAgent(receipt_dir=str(tmp_path / "receipts"))

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner
            mock_reflex.return_value = mock_planner

            result = await agent.execute("plan something", {})

            # Check receipt file exists
            receipt_path = result.result["receipt_path"]
            # Receipt path is relative to PROJECT_ROOT
            full_path = tmp_path / "receipts" / Path(receipt_path).name
            # The agent saves relative to PROJECT_ROOT, so we check if any file was created
            receipt_files = list((tmp_path / "receipts").glob("*.json"))
            assert len(receipt_files) == 1

    @pytest.mark.anyio
    async def test_receipt_has_chain_hash(self, mock_planner, tmp_path):
        """Test that receipt has chain hash."""
        agent = PlanAgent(receipt_dir=str(tmp_path / "receipts"))

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner
            mock_reflex.return_value = mock_planner

            result = await agent.execute("plan something", {})

            assert "chain_hash" in result.result
            assert len(result.result["chain_hash"]) == 64

    @pytest.mark.anyio
    async def test_chain_hash_format(self, mock_planner, tmp_path):
        """Test that chain hash is valid SHA256."""
        agent = PlanAgent(receipt_dir=str(tmp_path / "receipts"))

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner
            mock_reflex.return_value = mock_planner

            result = await agent.execute("plan something", {})

            chain_hash = result.result["chain_hash"]
            assert all(c in "0123456789abcdef" for c in chain_hash)


class TestPlanAgentVerification:
    """Tests for verification criteria extraction."""

    @pytest.fixture
    def mock_planner_with_test_step(self):
        """Create mock planner with test step."""
        mock_plan = MagicMock()
        mock_plan.request_type = "modify_code"
        mock_plan.warnings = []
        mock_plan.notes = []
        mock_plan.reflex_applied = True

        step = MagicMock()
        step.kind = "helix_tool"
        step.description = "Run tests"
        step.target_paths = []
        step.tool_name = "pytest_tests"
        step.args = {}
        step.priority = 1

        mock_plan.steps = [step]
        mock_plan.to_dict.return_value = {
            "request_type": "modify_code",
            "steps": [{"kind": "helix_tool", "tool_name": "pytest_tests"}],
            "warnings": [],
            "notes": [],
        }

        return mock_plan

    @pytest.mark.anyio
    async def test_verification_extracted(self, mock_planner_with_test_step):
        """Test that verification criteria are extracted."""
        agent = PlanAgent()

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_planner_with_test_step
            mock_reflex.return_value = mock_planner_with_test_step

            result = await agent.execute("plan to modify code", {})

            verification = result.result.get("verification", [])
            assert len(verification) > 0


class TestPlanAgentConstraints:
    """Tests for reflex config constraints."""

    @pytest.mark.anyio
    async def test_constraints_included_in_result(self):
        """Test that constraints are included in result."""
        from helix_cdc.ginnie.planner import PlanReflexConfig

        config = PlanReflexConfig(
            max_steps=10,
            forbid_rm_rf=True,
            forbid_sudo=True,
            require_tests_for_code_change=True,
        )
        agent = PlanAgent(reflex_config=config)

        mock_plan = MagicMock()
        mock_plan.request_type = "read_only"
        mock_plan.warnings = []
        mock_plan.notes = []
        mock_plan.reflex_applied = False
        mock_plan.steps = []
        mock_plan.to_dict.return_value = {
            "request_type": "read_only",
            "steps": [],
            "warnings": [],
            "notes": [],
        }

        with patch('helix_cdc.echo.agents.plan_agent.plan_work_order') as mock_pow, \
             patch('helix_cdc.echo.agents.plan_agent.apply_reflex') as mock_reflex:

            mock_pow.return_value = mock_plan
            mock_reflex.return_value = mock_plan

            result = await agent.execute("plan", {})

            constraints = result.result.get("constraints", {})
            assert constraints["max_steps"] == 10
            assert constraints["forbid_rm_rf"] is True
            assert constraints["forbid_sudo"] is True
            assert constraints["require_tests_for_code_change"] is True


class TestPlanAgentRegistration:
    """Tests for agent registration."""

    def test_auto_registration(self):
        """Test that PlanAgent is auto-registered on import."""
        from helix_cdc.echo.agents import AGENT_REGISTRY

        assert "plan" in AGENT_REGISTRY
        assert AGENT_REGISTRY["plan"].agent_type == "plan"


class TestPlanKeywordsAndPatterns:
    """Tests for keyword and pattern constants."""

    def test_plan_keywords_exist(self):
        """Test that plan keywords are defined."""
        assert "plan" in PLAN_KEYWORDS
        assert "work order" in PLAN_KEYWORDS
        assert "strategy" in PLAN_KEYWORDS

    def test_plan_patterns_exist(self):
        """Test that plan patterns are defined."""
        assert len(PLAN_PATTERNS) > 0
        # Check that patterns compile
        import re
        for pattern in PLAN_PATTERNS:
            re.compile(pattern)


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