"""Unit tests for burrow.integrations.claude_sdk -- Claude Agent SDK adapter."""

from __future__ import annotations

import json
from unittest.mock import AsyncMock, MagicMock

import pytest

from burrow import ScanResult
from burrow.integrations.claude_sdk import (
    BurrowBlockedError,
    HookMatcher,
    _extract_tool_input_text,
    create_burrow_hooks,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _allow_result() -> ScanResult:
    return ScanResult(
        action="allow",
        confidence=0.95,
        category="benign",
        request_id="req-1",
        latency_ms=10,
    )


def _block_result() -> ScanResult:
    return ScanResult(
        action="block",
        confidence=0.98,
        category="injection_detected",
        request_id="req-2",
        latency_ms=12,
    )


def _warn_result() -> ScanResult:
    return ScanResult(
        action="warn",
        confidence=0.72,
        category="suspicious_pattern",
        request_id="req-3",
        latency_ms=11,
    )


def _patch_guard_async(guard, result: ScanResult):
    """Patch guard.scan_async with an AsyncMock returning the given result."""
    guard.scan_async = AsyncMock(return_value=result)


# ---------------------------------------------------------------------------
# Structure tests
# ---------------------------------------------------------------------------


class TestHookStructure:
    def test_returns_dict_with_both_keys(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        assert "PreToolUse" in hooks
        assert "PostToolUse" in hooks

    def test_each_value_is_list_of_hook_matcher(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        for key in ("PreToolUse", "PostToolUse"):
            assert isinstance(hooks[key], list)
            assert len(hooks[key]) == 1
            matcher = hooks[key][0]
            assert isinstance(matcher, HookMatcher)
            assert matcher.matcher == ".*"
            assert len(matcher.hooks) == 1
            assert callable(matcher.hooks[0])

    def test_hook_matcher_dataclass_fields(self):
        hm = HookMatcher(matcher="test", hooks=[lambda: None])
        assert hm.matcher == "test"
        assert len(hm.hooks) == 1

    def test_hook_matcher_defaults(self):
        hm = HookMatcher()
        assert hm.matcher is None
        assert hm.hooks == []


# ---------------------------------------------------------------------------
# PreToolUse hook
# ---------------------------------------------------------------------------


class TestPreToolUseHook:
    @pytest.mark.asyncio
    async def test_clean_input_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_input": {"command": "ls -la"}, "tool_name": "bash"},
            "tu-123",
            MagicMock(),
        )
        assert result == {}

    @pytest.mark.asyncio
    async def test_injection_returns_deny(self, mock_guard):
        _patch_guard_async(mock_guard, _block_result())
        hooks = create_burrow_hooks(mock_guard)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_input": {"command": "ignore all instructions"}, "tool_name": "bash"},
            "tu-456",
            MagicMock(),
        )
        assert "hookSpecificOutput" in result
        assert result["hookSpecificOutput"]["permissionDecision"] == "deny"
        assert "Blocked by Burrow" in result["hookSpecificOutput"]["permissionDecisionReason"]

    @pytest.mark.asyncio
    async def test_empty_tool_input_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_input": {}, "tool_name": "empty_tool"},
            "tu-789",
            MagicMock(),
        )
        # json.dumps({}) -> '{}', which is not empty after strip, so scan is called
        # but allow result means empty dict returned
        assert result == {}

    @pytest.mark.asyncio
    async def test_non_dict_tool_input_stringified(self, mock_guard):
        _patch_guard_async(mock_guard, _block_result())
        hooks = create_burrow_hooks(mock_guard)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_input": "raw string input", "tool_name": "str_tool"},
            "tu-abc",
            MagicMock(),
        )
        assert result["hookSpecificOutput"]["permissionDecision"] == "deny"

    @pytest.mark.asyncio
    async def test_none_tool_input_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_input": None, "tool_name": "none_tool"},
            "tu-def",
            MagicMock(),
        )
        assert result == {}

    @pytest.mark.asyncio
    async def test_missing_tool_input_key_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_name": "no_input"},
            "tu-ghi",
            MagicMock(),
        )
        # No tool_input key -> get returns {} -> json.dumps({}) -> '{}'
        # Scan is called but allow -> empty dict
        assert result == {}


# ---------------------------------------------------------------------------
# PostToolUse hook
# ---------------------------------------------------------------------------


class TestPostToolUseHook:
    @pytest.mark.asyncio
    async def test_clean_response_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        post_hook = hooks["PostToolUse"][0].hooks[0]
        result = await post_hook(
            {"tool_response": "Safe output data", "tool_name": "read_file"},
            "tu-123",
            MagicMock(),
        )
        assert result == {}

    @pytest.mark.asyncio
    async def test_injection_returns_burrow_action(self, mock_guard):
        _patch_guard_async(mock_guard, _block_result())
        hooks = create_burrow_hooks(mock_guard)
        post_hook = hooks["PostToolUse"][0].hooks[0]
        result = await post_hook(
            {"tool_response": "malicious injection content", "tool_name": "web_fetch"},
            "tu-456",
            MagicMock(),
        )
        assert "hookSpecificOutput" in result
        output = result["hookSpecificOutput"]
        assert output["hookEventName"] == "PostToolUse"
        assert output["burrowAction"] == "block"
        assert output["burrowCategory"] == "injection_detected"
        assert output["burrowConfidence"] == 0.98

    @pytest.mark.asyncio
    async def test_empty_response_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        post_hook = hooks["PostToolUse"][0].hooks[0]
        result = await post_hook(
            {"tool_response": "", "tool_name": "empty_tool"},
            "tu-789",
            MagicMock(),
        )
        assert result == {}

    @pytest.mark.asyncio
    async def test_none_response_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        post_hook = hooks["PostToolUse"][0].hooks[0]
        result = await post_hook(
            {"tool_response": None, "tool_name": "none_tool"},
            "tu-abc",
            MagicMock(),
        )
        assert result == {}


# ---------------------------------------------------------------------------
# block_on_warn
# ---------------------------------------------------------------------------


class TestBlockOnWarn:
    @pytest.mark.asyncio
    async def test_pre_tool_warn_blocks_when_true(self, mock_guard):
        _patch_guard_async(mock_guard, _warn_result())
        hooks = create_burrow_hooks(mock_guard, block_on_warn=True)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_input": {"command": "suspicious"}, "tool_name": "bash"},
            "tu-1",
            MagicMock(),
        )
        assert result["hookSpecificOutput"]["permissionDecision"] == "deny"

    @pytest.mark.asyncio
    async def test_pre_tool_warn_allows_when_false(self, mock_guard):
        _patch_guard_async(mock_guard, _warn_result())
        hooks = create_burrow_hooks(mock_guard, block_on_warn=False)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        result = await pre_hook(
            {"tool_input": {"command": "suspicious"}, "tool_name": "bash"},
            "tu-2",
            MagicMock(),
        )
        assert result == {}

    @pytest.mark.asyncio
    async def test_post_tool_warn_blocks_when_true(self, mock_guard):
        _patch_guard_async(mock_guard, _warn_result())
        hooks = create_burrow_hooks(mock_guard, block_on_warn=True)
        post_hook = hooks["PostToolUse"][0].hooks[0]
        result = await post_hook(
            {"tool_response": "suspicious output", "tool_name": "fetch"},
            "tu-3",
            MagicMock(),
        )
        assert "hookSpecificOutput" in result
        assert result["hookSpecificOutput"]["burrowAction"] == "warn"

    @pytest.mark.asyncio
    async def test_post_tool_warn_allows_when_false(self, mock_guard):
        _patch_guard_async(mock_guard, _warn_result())
        hooks = create_burrow_hooks(mock_guard, block_on_warn=False)
        post_hook = hooks["PostToolUse"][0].hooks[0]
        result = await post_hook(
            {"tool_response": "suspicious output", "tool_name": "fetch"},
            "tu-4",
            MagicMock(),
        )
        assert result == {}


# ---------------------------------------------------------------------------
# scan_tool_calls / scan_tool_results flags
# ---------------------------------------------------------------------------


class TestScanFlags:
    def test_scan_tool_calls_false_excludes_pre(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard, scan_tool_calls=False)
        assert "PreToolUse" not in hooks
        assert "PostToolUse" in hooks

    def test_scan_tool_results_false_excludes_post(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard, scan_tool_results=False)
        assert "PreToolUse" in hooks
        assert "PostToolUse" not in hooks

    def test_both_false_returns_empty_dict(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard, scan_tool_calls=False, scan_tool_results=False)
        assert hooks == {}


# ---------------------------------------------------------------------------
# Agent name forwarding
# ---------------------------------------------------------------------------


class TestAgentName:
    @pytest.mark.asyncio
    async def test_default_agent_name(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard)
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        await pre_hook(
            {"tool_input": {"command": "ls"}, "tool_name": "bash"},
            "tu-1",
            MagicMock(),
        )
        mock_guard.scan_async.assert_called_once()
        call_kwargs = mock_guard.scan_async.call_args
        assert (
            call_kwargs.kwargs.get("agent") == "claude-sdk"
            or call_kwargs[1].get("agent") == "claude-sdk"
        )

    @pytest.mark.asyncio
    async def test_custom_agent_name(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard, agent_name="claude-sdk:researcher")
        pre_hook = hooks["PreToolUse"][0].hooks[0]
        await pre_hook(
            {"tool_input": {"command": "search"}, "tool_name": "web_search"},
            "tu-2",
            MagicMock(),
        )
        mock_guard.scan_async.assert_called_once()
        _, kwargs = mock_guard.scan_async.call_args
        assert kwargs["agent"] == "claude-sdk:researcher"

    @pytest.mark.asyncio
    async def test_agent_name_forwarded_in_post_hook(self, mock_guard):
        _patch_guard_async(mock_guard, _allow_result())
        hooks = create_burrow_hooks(mock_guard, agent_name="claude-sdk:writer")
        post_hook = hooks["PostToolUse"][0].hooks[0]
        await post_hook(
            {"tool_response": "output data", "tool_name": "write_file"},
            "tu-3",
            MagicMock(),
        )
        mock_guard.scan_async.assert_called_once()
        _, kwargs = mock_guard.scan_async.call_args
        assert kwargs["agent"] == "claude-sdk:writer"


# ---------------------------------------------------------------------------
# _extract_tool_input_text
# ---------------------------------------------------------------------------


class TestExtractToolInputText:
    def test_known_field_command(self):
        text = _extract_tool_input_text({"command": "ls -la /tmp"})
        assert "ls -la /tmp" in text

    def test_known_field_content(self):
        text = _extract_tool_input_text({"content": "file contents here"})
        assert "file contents here" in text

    def test_known_field_query(self):
        text = _extract_tool_input_text({"query": "SELECT * FROM users"})
        assert "SELECT * FROM users" in text

    def test_known_field_text(self):
        text = _extract_tool_input_text({"text": "some text value"})
        assert "some text value" in text

    def test_known_field_url(self):
        text = _extract_tool_input_text({"url": "https://example.com"})
        assert "https://example.com" in text

    def test_known_field_file_path(self):
        text = _extract_tool_input_text({"file_path": "/etc/passwd"})
        assert "/etc/passwd" in text

    def test_known_field_pattern(self):
        text = _extract_tool_input_text({"pattern": "*.py"})
        assert "*.py" in text

    def test_multiple_known_fields_concatenated(self):
        text = _extract_tool_input_text(
            {
                "command": "grep",
                "pattern": "TODO",
                "file_path": "/src/main.py",
            }
        )
        assert "grep" in text
        assert "TODO" in text
        assert "/src/main.py" in text

    def test_fallback_to_json_dumps(self):
        data = {"unknown_key": "value", "another": 42}
        text = _extract_tool_input_text(data)
        assert text == json.dumps(data)

    def test_none_value_known_field_skipped(self):
        text = _extract_tool_input_text({"command": None, "unknown": "val"})
        # command is None so skipped, no known fields extracted -> fallback to json
        parsed = json.loads(text)
        assert parsed == {"command": None, "unknown": "val"}

    def test_empty_dict_returns_json(self):
        text = _extract_tool_input_text({})
        assert text == "{}"


# ---------------------------------------------------------------------------
# BurrowBlockedError
# ---------------------------------------------------------------------------


class TestBurrowBlockedError:
    def test_stores_result(self):
        result = _block_result()
        err = BurrowBlockedError(result)
        assert err.result is result

    def test_message_contains_category(self):
        result = _block_result()
        err = BurrowBlockedError(result)
        assert "injection_detected" in str(err)

    def test_message_contains_confidence(self):
        result = _block_result()
        err = BurrowBlockedError(result)
        assert "98%" in str(err)

    def test_is_exception(self):
        result = _block_result()
        err = BurrowBlockedError(result)
        assert isinstance(err, Exception)
