"""Tests for mode enforcement in agent loop."""

import asyncio
from unittest.mock import AsyncMock, MagicMock

import pytest

from fliiq.runtime.agent.config import AgentConfig
from fliiq.runtime.agent.loop import agent_loop
from fliiq.runtime.agent.tools import ToolRegistry
from fliiq.runtime.llm.providers import LLMProvider, LLMResponse, ToolCall


def _make_text_response(text: str) -> LLMResponse:
    return LLMResponse(
        content=text,
        model="test-model",
        provider=LLMProvider.ANTHROPIC,
        stop_reason="end_turn",
        raw_content=[{"type": "text", "text": text}],
    )


def _make_tool_response(text: str, tool_calls: list[ToolCall]) -> LLMResponse:
    raw = []
    if text:
        raw.append({"type": "text", "text": text})
    for tc in tool_calls:
        raw.append({"type": "tool_use", "id": tc.id, "name": tc.name, "input": tc.input})
    return LLMResponse(
        content=text,
        model="test-model",
        provider=LLMProvider.ANTHROPIC,
        tool_calls=tool_calls,
        stop_reason="tool_use",
        raw_content=raw,
    )


async def test_on_before_tool_call_approve():
    """on_before_tool_call returning True allows tool execution."""
    tc = ToolCall(id="tc_1", name="test_tool", input={"arg": "value"})
    callback_calls = []

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Using tool", [tc]),
        _make_text_response("Done."),
    ])

    tools = ToolRegistry()
    tools.register("test_tool", "Test tool", {"type": "object"}, lambda p: "executed")

    def on_before(name, inp):
        callback_calls.append((name, inp))
        return True  # Approve

    messages = [{"role": "user", "content": "do something"}]
    result = await agent_loop(llm, messages, tools, on_before_tool_call=on_before)

    assert len(callback_calls) == 1
    assert callback_calls[0][0] == "test_tool"
    # Tool should have been executed (result in messages)
    tool_result_msg = result.messages[2]
    assert tool_result_msg["content"][0]["content"] == "executed"


async def test_on_before_tool_call_reject():
    """on_before_tool_call returning False skips tool execution."""
    tc = ToolCall(id="tc_1", name="test_tool", input={"arg": "value"})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Using tool", [tc]),
        _make_text_response("Tool was skipped."),
    ])

    tools = ToolRegistry()
    tools.register("test_tool", "Test tool", {"type": "object"}, lambda p: "executed")

    messages = [{"role": "user", "content": "do something"}]
    result = await agent_loop(
        llm, messages, tools,
        on_before_tool_call=lambda name, inp: False,  # Reject
    )

    # Tool should have been skipped
    tool_result_msg = result.messages[2]
    assert tool_result_msg["content"][0]["content"] == "Tool skipped by user"


async def test_on_before_tool_call_mixed_decisions():
    """on_before_tool_call can approve some tools and reject others."""
    tc1 = ToolCall(id="tc_1", name="allowed_tool", input={})
    tc2 = ToolCall(id="tc_2", name="blocked_tool", input={})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Using tools", [tc1, tc2]),
        _make_text_response("Done."),
    ])

    tools = ToolRegistry()
    tools.register("allowed_tool", "Allowed", {"type": "object"}, lambda p: "allowed_result")
    tools.register("blocked_tool", "Blocked", {"type": "object"}, lambda p: "blocked_result")

    def selective_approve(name, inp):
        return name == "allowed_tool"

    messages = [{"role": "user", "content": "test"}]
    result = await agent_loop(llm, messages, tools, on_before_tool_call=selective_approve)

    tool_results = result.messages[2]["content"]
    assert tool_results[0]["content"] == "allowed_result"
    assert tool_results[1]["content"] == "Tool skipped by user"


async def test_autonomous_mode_no_callback():
    """Autonomous mode runs tools without on_before_tool_call."""
    tc = ToolCall(id="tc_1", name="test_tool", input={})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("", [tc]),
        _make_text_response("Done."),
    ])

    tools = ToolRegistry()
    tools.register("test_tool", "Test", {"type": "object"}, lambda p: "result")

    config = AgentConfig(mode="autonomous")
    messages = [{"role": "user", "content": "test"}]

    # No callback = autonomous behavior
    result = await agent_loop(llm, messages, tools, config=config)

    tool_result = result.messages[2]["content"][0]["content"]
    assert tool_result == "result"


def test_plan_mode_tool_filtering():
    """Plan mode filtering via tool_defs_override restricts visible tools to builtins."""
    tools = ToolRegistry()

    # Register skills AND builtins (immutable registry — all tools always present)
    tools.register("file_read", "Read file", {"type": "object"}, lambda p: "content")
    tools.register("shell", "Run shell", {"type": "object"}, lambda p: "output")
    tools.register_builtins(ask_user_handler=lambda q: "answer")

    # Simulate plan mode filtering at the caller layer
    BUILTIN_TOOLS = {"todo", "ask_user", "ask_user_choice"}
    all_defs = tools.get_tool_definitions()
    plan_defs = [t for t in all_defs if t["name"] in BUILTIN_TOOLS]

    plan_names = [t["name"] for t in plan_defs]
    assert "todo" in plan_names
    assert "ask_user" in plan_names
    assert "ask_user_choice" in plan_names
    assert "file_read" not in plan_names
    assert "shell" not in plan_names
    assert len(plan_names) == 3


def test_full_mode_has_all_tools():
    """Non-plan modes have all registered tools."""
    tools = ToolRegistry()

    # Register a skill-like tool
    tools.register("file_read", "Read file", {"type": "object"}, lambda p: "content")
    tools.register_builtins(ask_user_handler=lambda q: "answer")

    tool_defs = tools.get_tool_definitions()
    tool_names = [t["name"] for t in tool_defs]

    assert "file_read" in tool_names
    assert "todo" in tool_names
    assert "ask_user" in tool_names
    assert "ask_user_choice" in tool_names
    assert "install_skill" in tool_names
    assert "plan_complete" in tool_names
    assert len(tool_names) == 6


async def test_on_tool_call_still_fires_when_skipped():
    """on_tool_call callback fires even when tool is skipped."""
    tc = ToolCall(id="tc_1", name="test_tool", input={})
    tool_call_log = []

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("", [tc]),
        _make_text_response("Done."),
    ])

    tools = ToolRegistry()
    tools.register("test_tool", "Test", {"type": "object"}, lambda p: "should_not_run")

    messages = [{"role": "user", "content": "test"}]
    result = await agent_loop(
        llm, messages, tools,
        on_tool_call=lambda name, inp, res: tool_call_log.append((name, res)),
        on_before_tool_call=lambda name, inp: False,  # Reject
    )

    # on_tool_call should still have been called with the skipped result
    assert len(tool_call_log) == 1
    assert tool_call_log[0][0] == "test_tool"
    assert tool_call_log[0][1] == "Tool skipped by user"


async def test_async_on_before_tool_call_approve():
    """Async on_before_tool_call returning True allows tool execution."""
    tc = ToolCall(id="tc_1", name="test_tool", input={"arg": "value"})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Using tool", [tc]),
        _make_text_response("Done."),
    ])

    tools = ToolRegistry()
    tools.register("test_tool", "Test tool", {"type": "object"}, lambda p: "executed")

    async def async_approve(name, inp):
        return True

    messages = [{"role": "user", "content": "do something"}]
    result = await agent_loop(llm, messages, tools, on_before_tool_call=async_approve)

    tool_result_msg = result.messages[2]
    assert tool_result_msg["content"][0]["content"] == "executed"


async def test_async_on_before_tool_call_reject():
    """Async on_before_tool_call returning False skips tool execution."""
    tc = ToolCall(id="tc_1", name="test_tool", input={"arg": "value"})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Using tool", [tc]),
        _make_text_response("Skipped."),
    ])

    tools = ToolRegistry()
    tools.register("test_tool", "Test tool", {"type": "object"}, lambda p: "executed")

    async def async_reject(name, inp):
        return False

    messages = [{"role": "user", "content": "do something"}]
    result = await agent_loop(llm, messages, tools, on_before_tool_call=async_reject)

    tool_result_msg = result.messages[2]
    assert tool_result_msg["content"][0]["content"] == "Tool skipped by user"


async def test_agent_loop_tool_defs_override():
    """tool_defs_override restricts what tool definitions the LLM sees."""
    tc = ToolCall(id="tc_1", name="todo", input={"action": "add", "text": "test"})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Planning", [tc]),
        _make_text_response("Plan complete."),
    ])

    tools = ToolRegistry()
    tools.register("file_read", "Read file", {"type": "object"}, lambda p: "content")
    tools.register_builtins(ask_user_handler=lambda q: "answer")

    # Only send builtins to the LLM
    BUILTIN_TOOLS = {"todo", "ask_user", "ask_user_choice"}
    all_defs = tools.get_tool_definitions()
    override = [t for t in all_defs if t["name"] in BUILTIN_TOOLS]

    messages = [{"role": "user", "content": "plan something"}]
    result = await agent_loop(llm, messages, tools, tool_defs_override=override)

    # Verify LLM was called with only builtin tool defs
    call_args = llm.generate.call_args_list[0]
    sent_tools = call_args.kwargs.get("tools") or call_args[1].get("tools")
    sent_names = [t["name"] for t in sent_tools]
    assert "file_read" not in sent_names
    assert "todo" in sent_names


async def test_plan_mode_callback_rejects_skill_tools():
    """Plan mode on_before_tool_call rejects non-builtin tools as a safety net."""
    tc = ToolCall(id="tc_1", name="file_read", input={"path": "/etc/passwd"})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Trying file_read", [tc]),
        _make_text_response("Skipped."),
    ])

    tools = ToolRegistry()
    tools.register("file_read", "Read file", {"type": "object"}, lambda p: "content")
    tools.register_builtins(ask_user_handler=lambda q: "answer")

    BUILTIN_TOOLS = {"todo", "ask_user", "ask_user_choice"}

    def plan_callback(name, tool_input):
        return name in BUILTIN_TOOLS

    messages = [{"role": "user", "content": "plan something"}]
    result = await agent_loop(
        llm, messages, tools,
        on_before_tool_call=plan_callback,
    )

    # file_read should have been rejected
    tool_result_msg = result.messages[2]
    assert tool_result_msg["content"][0]["content"] == "Tool skipped by user"
