"""
Tests for TestAgent (WO-ECHO-TEST-AGENT-01)

Tests covering:
- Command parsing from natural language
- Allowlist enforcement (reject unsafe commands)
- Receipt generation and chain_hash verification
- Integration with AgentResult format
"""

import asyncio
import hashlib
import json
import os
import pytest
from pathlib import Path
from typing import Dict, Any
from unittest.mock import patch, MagicMock

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


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

# Import the test agent and related modules
from helix_cdc.echo.agents.test_agent import TestAgent
from helix_cdc.echo.agents import AgentResult, PROJECT_ROOT
from helix_cdc.echo.ops.helpers import (
    detect_test_intent,
    _parse_test_command,
    validate_command,
    ALLOWED_COMMANDS,
)


class TestDetectTestIntent:
    """Tests for the detect_test_intent function."""

    def test_basic_pytest_command(self):
        """Test detection of basic pytest command."""
        result = detect_test_intent("run pytest tests/ -v")
        assert result["is_test"] is True
        assert result["confidence"] > 0.5
        assert result["parsed_command"] is not None
        assert result["parsed_command"]["command"] == "pytest"

    def test_run_tests_natural_language(self):
        """Test detection of natural language test request."""
        result = detect_test_intent("run the tests")
        assert result["is_test"] is True
        assert result["parsed_command"]["command"] == "pytest"
        assert result["parsed_command"]["args"] == ["tests/"]

    def test_make_test_detection(self):
        """Test detection of make test commands."""
        result = detect_test_intent("make test")
        assert result["is_test"] is True
        assert result["parsed_command"]["command"] == "make"
        assert result["parsed_command"]["args"] == ["test"]

    def test_make_gate_detection(self):
        """Test detection of make gate command."""
        result = detect_test_intent("run make gate")
        assert result["is_test"] is True
        assert result["parsed_command"]["command"] == "make"
        assert result["parsed_command"]["args"] == ["gate"]

    def test_python_m_pytest(self):
        """Test detection of python -m pytest."""
        result = detect_test_intent("python -m pytest tests/")
        assert result["is_test"] is True
        assert result["parsed_command"]["command"] == "python"
        assert result["parsed_command"]["args"][:2] == ["-m", "pytest"]

    def test_python3_m_pytest(self):
        """Test detection of python3 -m pytest (normalized to python)."""
        result = detect_test_intent("python3 -m pytest tests/")
        assert result["is_test"] is True
        # python3 should be normalized to python for allowlist compatibility
        assert result["parsed_command"]["command"] == "python"
        assert result["parsed_command"]["args"][:2] == ["-m", "pytest"]

    def test_non_test_query(self):
        """Test that non-test queries are not detected."""
        result = detect_test_intent("what is the weather today")
        assert result["is_test"] is False
        assert result["parsed_command"] is None

    def test_partial_match_requires_strong_signal(self):
        """Test that ambiguous phrases don't trigger test routing."""
        # These should NOT trigger test routing (false positive prevention)
        false_positives = [
            "test my understanding of Se",
            "this is a test message",
            "I'm testing the server",
            "what is this test about",
        ]
        for prompt in false_positives:
            result = detect_test_intent(prompt)
            assert result["is_test"] is False, f"False positive on: {prompt}"
            assert result["parsed_command"] is None, f"Parsed command on: {prompt}"


class TestParseTestCommand:
    """Tests for the _parse_test_command function."""

    def test_pytest_with_path(self):
        """Test parsing pytest with path."""
        result = _parse_test_command("pytest tests/unit/")
        assert result["command"] == "pytest"
        assert "tests/unit/" in result["args"]

    def test_pytest_with_flags(self):
        """Test parsing pytest with flags."""
        result = _parse_test_command("pytest tests/ -v --tb=short")
        assert result["command"] == "pytest"
        assert "tests/" in result["args"]
        assert "-v" in result["args"]

    def test_make_test_target(self):
        """Test parsing make test target."""
        result = _parse_test_command("make healthcheck")
        assert result["command"] == "make"
        assert "healthcheck" in result["args"]

    def test_generic_run_tests(self):
        """Test parsing generic 'run tests'."""
        result = _parse_test_command("run tests")
        assert result["command"] == "pytest"
        assert "tests/" in result["args"]

    def test_unparseable_command(self):
        """Test that unparseable commands return None."""
        result = _parse_test_command("hello world")
        assert result is None


class TestCommandValidation:
    """Tests for command validation against allowlist."""

    def test_pytest_allowed(self):
        """Test that pytest is allowed."""
        is_valid, reason = validate_command("pytest", ["tests/", "-v"])
        assert is_valid is True

    def test_make_test_allowed(self):
        """Test that make test is allowed."""
        is_valid, reason = validate_command("make", ["test"])
        assert is_valid is True

    def test_make_gate_allowed(self):
        """Test that make gate is allowed."""
        is_valid, reason = validate_command("make", ["gate"])
        assert is_valid is True

    def test_rm_blocked(self):
        """Test that rm commands are blocked."""
        is_valid, reason = validate_command("rm", ["-rf", "/"])
        assert is_valid is False
        assert "not in allowlist" in reason.lower() or "blocked" in reason.lower()

    def test_git_push_blocked(self):
        """Test that git push is blocked (destructive)."""
        is_valid, reason = validate_command("git", ["push"])
        assert is_valid is False
        # git push is not in allowed subcommands for git

    def test_python_m_allowed(self):
        """Test that python -m is allowed."""
        is_valid, reason = validate_command("python", ["-m", "pytest"])
        assert is_valid is True

    def test_python_without_m_blocked(self):
        """Test that python without -m is not allowed."""
        is_valid, reason = validate_command("python", ["script.py"])
        assert is_valid is False


class TestTestAgent:
    """Tests for the TestAgent class."""

    @pytest.fixture
    def agent(self):
        """Create a TestAgent instance."""
        return TestAgent(timeout_seconds=30)

    def test_agent_type(self, agent):
        """Test that agent type is 'test'."""
        assert agent.agent_type == "test"

    def test_available_check(self, agent):
        """Test availability check."""
        # Should generally be available if python3 exists
        result = agent.available()
        assert isinstance(result, bool)

    def test_parse_test_intent_method(self, agent):
        """Test the _parse_test_intent method."""
        result = agent._parse_test_intent("run pytest tests/")
        assert result is not None
        assert result["command"] == "pytest"

    def test_compute_chain_hash(self, agent):
        """Test chain hash computation."""
        hash_result = agent._compute_chain_hash(
            prompt_sha="abc123",
            argv_json_sha="def456",
            stdout_sha="ghi789",
            stderr_sha="jkl012",
            exit_code=0,
        )
        # Should be a hex string of 64 characters (sha256)
        assert len(hash_result) == 64
        assert all(c in "0123456789abcdef" for c in hash_result)

    def test_chain_hash_deterministic(self, agent):
        """Test that chain hash is deterministic."""
        hash1 = agent._compute_chain_hash("a", "b", "c", "d", 0)
        hash2 = agent._compute_chain_hash("a", "b", "c", "d", 0)
        assert hash1 == hash2

    def test_chain_hash_changes_with_input(self, agent):
        """Test that chain hash changes when input changes."""
        hash1 = agent._compute_chain_hash("a", "b", "c", "d", 0)
        hash2 = agent._compute_chain_hash("a", "b", "c", "d", 1)  # Different exit code
        assert hash1 != hash2

    def test_determine_status_pass(self, agent):
        """Test status determination for passing tests."""
        result = {"exit_code": 0, "stdout": "OK", "stderr": ""}
        status = agent._determine_status(result)
        assert status == "PASS"

    def test_determine_status_fail(self, agent):
        """Test status determination for failing tests."""
        result = {"exit_code": 1, "stdout": "", "stderr": "FAILED"}
        status = agent._determine_status(result)
        assert status == "FAIL"

    def test_determine_status_timeout(self, agent):
        """Test status determination for timeout."""
        result = {"exit_code": -1, "stdout": "", "stderr": "Command timed out after 30s"}
        status = agent._determine_status(result)
        assert status == "TIMEOUT"

    def test_determine_status_rejected(self, agent):
        """Test status determination for rejected command."""
        result = {"exit_code": -1, "stdout": "", "stderr": "blocked"}
        status = agent._determine_status(result, was_rejected=True)
        assert status == "REJECTED"

    def test_build_receipt_structure(self, agent):
        """Test that receipt has required fields."""
        receipt = agent._build_receipt(
            prompt="run tests",
            command_argv=["pytest", "tests/"],
            result={
                "exit_code": 0,
                "stdout": "passed",
                "stderr": "",
                "elapsed_seconds": 1.5,
            },
            status="PASS",
        )

        # Check required fields
        assert "receipt_id" in receipt
        assert "receipt_type" in receipt
        assert receipt["receipt_type"] == "test_execution"
        assert "timestamp_utc" in receipt
        assert "work_order" in receipt
        assert receipt["work_order"] == "WO-ECHO-TEST-AGENT-01"
        assert "prompt" in receipt
        assert "prompt_sha256" in receipt
        assert "command_argv" in receipt
        assert "cwd" in receipt
        assert "timeout_s" in receipt
        assert "exit_code" in receipt
        assert "stdout_sha256" in receipt
        assert "stderr_sha256" in receipt
        assert "stdout_preview" in receipt
        assert "stderr_preview" in receipt
        assert "elapsed_ms" in receipt
        assert "status" in receipt
        assert "chain_hash" in receipt

    def test_receipt_chain_hash_verifiable(self, agent):
        """Test that receipt chain_hash is verifiable."""
        prompt = "run tests"
        command_argv = ["pytest", "tests/"]
        result = {
            "exit_code": 0,
            "stdout": "passed",
            "stderr": "",
            "elapsed_seconds": 1.5,
        }

        receipt = agent._build_receipt(prompt, command_argv, result, "PASS")

        # Recompute chain hash manually
        prompt_sha = hashlib.sha256(prompt.encode()).hexdigest()
        argv_json = json.dumps(command_argv, sort_keys=True)
        argv_json_sha = hashlib.sha256(argv_json.encode()).hexdigest()
        stdout_sha = hashlib.sha256(result["stdout"].encode()).hexdigest()
        stderr_sha = hashlib.sha256(result["stderr"].encode()).hexdigest()

        expected_chain_hash = agent._compute_chain_hash(
            prompt_sha, argv_json_sha, stdout_sha, stderr_sha, 0
        )

        assert receipt["chain_hash"] == expected_chain_hash


class TestTestAgentExecution:
    """Integration tests for TestAgent execution."""

    @pytest.fixture
    def agent(self):
        """Create a TestAgent instance with temp receipt dir."""
        return TestAgent(
            timeout_seconds=30,
            receipt_dir="receipts/test_agent_test",
        )

    @pytest.mark.anyio
    async def test_execute_rejected_command(self, agent):
        """Test execution of a rejected command via context bypass."""
        # Provide a parsed command directly to bypass the parser
        # This tests the validation layer rejects dangerous commands
        context = {
            "parsed_command": {
                "command": "rm",
                "args": ["-rf", "/"],
            }
        }
        result = await agent.execute("remove files", context)

        assert isinstance(result, AgentResult)
        assert result.agent_type == "test"
        assert result.result["status"] == "REJECTED"
        assert "receipt_path" in result.result
        assert len(result.sources) > 0

    @pytest.mark.anyio
    async def test_execute_unparseable_query(self, agent):
        """Test execution with unparseable query."""
        result = await agent.execute("hello world", {})

        assert isinstance(result, AgentResult)
        assert result.agent_type == "test"
        assert result.result["status"] == "REJECTED"
        assert "parse" in result.error.lower()

    @pytest.mark.anyio
    async def test_execute_with_context_command(self, agent):
        """Test execution with pre-parsed command in context."""
        context = {
            "parsed_command": {
                "command": "pytest",
                "args": ["--version"],
            }
        }
        result = await agent.execute("run pytest version", context)

        assert isinstance(result, AgentResult)
        assert result.agent_type == "test"
        # pytest --version should pass if pytest is installed
        assert result.result["status"] in ("PASS", "FAIL", "ERROR")
        assert result.result["command_argv"] == ["pytest", "--version"]

    @pytest.mark.anyio
    async def test_execute_creates_receipt_file(self, agent):
        """Test that execution creates a receipt file."""
        result = await agent.execute("pytest --version", {})

        if result.result.get("receipt_path"):
            receipt_path = PROJECT_ROOT / result.result["receipt_path"]
            assert receipt_path.exists()

            # Verify receipt content
            with open(receipt_path) as f:
                receipt = json.load(f)

            assert receipt["receipt_type"] == "test_execution"
            assert receipt["work_order"] == "WO-ECHO-TEST-AGENT-01"
            assert "chain_hash" in receipt

            # Clean up
            receipt_path.unlink()


class TestAgentResultIntegration:
    """Tests for AgentResult format compliance."""

    @pytest.fixture
    def agent(self):
        return TestAgent(timeout_seconds=10)

    @pytest.mark.anyio
    async def test_agent_result_to_dict(self, agent):
        """Test that AgentResult.to_dict() works correctly."""
        result = await agent.execute("pytest --version", {})

        result_dict = result.to_dict()

        assert "agent_id" in result_dict
        assert "agent_type" in result_dict
        assert result_dict["agent_type"] == "test"
        assert "query" in result_dict
        assert "result" in result_dict
        assert "sources" in result_dict
        assert "confidence" in result_dict
        assert "elapsed_ms" in result_dict

    @pytest.mark.anyio
    async def test_agent_result_has_sources(self, agent):
        """Test that AgentResult includes receipt sources."""
        result = await agent.execute("pytest --version", {})

        # Should have at least one source (the receipt)
        if result.result.get("receipt_path"):
            assert len(result.sources) > 0
            assert any("receipt:" in s for s in result.sources)


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