import json
import threading
from types import SimpleNamespace
from unittest.mock import Mock

from agent.protocol.agent_stream import AgentStreamExecutor
from agent.protocol.steer import (
    SteerInbox,
    SteerRegistry,
    SteerResult,
    SteerStatus,
)
from bridge.context import Context, ContextType
from channel.chat_channel import ChatChannel


class _ScriptedExecutor(AgentStreamExecutor):
    def __init__(self, responses, inbox, steer_after_tool=None):
        super().__init__(
            agent=SimpleNamespace(),
            model=SimpleNamespace(model="test-model"),
            system_prompt="",
            tools=[],
            max_turns=8,
            messages=[],
            steer_inbox=inbox,
        )
        self.responses = list(responses)
        self.executed = []
        self.steer_after_tool = steer_after_tool

    def _is_thinking_enabled(self):
        return False

    def _trim_messages(self):
        return None

    def _validate_and_fix_messages(self):
        return None

    def _call_llm_stream(self, retry_on_empty=True):
        text, tool_calls, callback = self.responses.pop(0)
        content = []
        if text:
            content.append({"type": "text", "text": text})
        content.extend({
            "type": "tool_use",
            "id": call["id"],
            "name": call["name"],
            "input": call.get("arguments", {}),
        } for call in tool_calls)
        self.messages.append({"role": "assistant", "content": content})
        if callback:
            callback()
        return text, tool_calls

    def _execute_tool(self, tool_call):
        self.executed.append(tool_call["name"])
        if self.steer_after_tool == tool_call["name"]:
            self.steer_inbox.submit("use the new target")
        return {
            "status": "success",
            "result": f"finished {tool_call['name']}",
            "execution_time": 0.01,
        }


def _tool(name):
    return {"id": f"call-{name}", "name": name, "arguments": {}}


def _blocks(messages, block_type):
    return [
        block
        for message in messages
        for block in (message.get("content") or [])
        if isinstance(block, dict) and block.get("type") == block_type
    ]


def test_registry_accepts_only_one_active_run_and_preserves_order():
    registry = SteerRegistry()
    inbox = registry.register("research::session")

    assert registry.submit("other::session", "ignored").status == SteerStatus.INACTIVE
    assert registry.submit("research::session", "first").accepted
    assert registry.submit("research::session", "second").accepted
    assert inbox.drain() == ["first", "second"]

    second = registry.register("research::session")
    assert registry.submit("research::session", "ambiguous").status == SteerStatus.AMBIGUOUS
    registry.unregister("research::session", second)
    registry.unregister("research::session", inbox)
    assert registry.submit("research::session", "late").status == SteerStatus.INACTIVE


def test_inbox_bounds_and_atomic_close_gate():
    inbox = SteerInbox(max_pending=1, max_chars=5)
    assert inbox.submit("").status == SteerStatus.INVALID
    assert inbox.submit("123456").status == SteerStatus.INVALID
    assert inbox.submit("first").accepted
    assert inbox.submit("again").status == SteerStatus.FULL
    assert not inbox.close_if_empty()
    assert inbox.drain() == ["first"]
    assert inbox.close_if_empty()
    assert inbox.submit("late").status == SteerStatus.CLOSING


def test_steer_arriving_during_model_skips_all_proposed_tools():
    inbox = SteerInbox()
    executor = _ScriptedExecutor([
        ("old plan", [_tool("one"), _tool("two")], lambda: inbox.submit("change course")),
        ("new answer", [], None),
    ], inbox)

    assert executor.run_stream("start") == "new answer"
    assert executor.executed == []
    results = _blocks(executor.messages, "tool_result")
    assert {block["tool_use_id"] for block in results} == {"call-one", "call-two"}
    assert all(block.get("is_error") for block in results)
    assert "change course" in "\n".join(
        block["text"] for block in _blocks(executor.messages, "text")
    )


def test_steer_between_tools_keeps_completed_result_and_skips_remaining_tool():
    inbox = SteerInbox()
    executor = _ScriptedExecutor([
        ("", [_tool("one"), _tool("two")], None),
        ("redirected answer", [], None),
    ], inbox, steer_after_tool="one")

    assert executor.run_stream("start") == "redirected answer"
    assert executor.executed == ["one"]
    results = {block["tool_use_id"]: block for block in _blocks(executor.messages, "tool_result")}
    assert not results["call-one"].get("is_error", False)
    assert results["call-two"]["is_error"] is True


def _fake_agent_bridge(result):
    bridge = SimpleNamespace(
        steer_session=Mock(return_value=result),
    )
    return bridge, SimpleNamespace(get_agent_bridge=lambda: bridge)


def test_chat_steer_command_bypasses_the_normal_queue(monkeypatch):
    bridge, factory = _fake_agent_bridge(SteerResult(SteerStatus.ACCEPTED))
    monkeypatch.setattr("bridge.bridge.Bridge", lambda: factory)
    channel = object.__new__(ChatChannel)
    channel.sessions = {}
    channel.lock = threading.Lock()
    channel._send_reply = Mock()
    context = Context(ContextType.TEXT, "/steer focus on tests", {
        "session_id": "session",
    })

    ChatChannel.produce(channel, context)

    assert channel.sessions == {}
    bridge.steer_session.assert_called_once_with("session", "focus on tests")
    reply_text = channel._send_reply.call_args.args[1].content
    assert "redirect" in reply_text.lower() or "已引导" in reply_text


def test_ordinary_chat_message_keeps_using_the_session_queue(monkeypatch):
    _, factory = _fake_agent_bridge(SteerResult(SteerStatus.ACCEPTED))
    monkeypatch.setattr("bridge.bridge.Bridge", lambda: factory)
    monkeypatch.setattr("channel.chat_channel.conf", lambda: {
        "concurrency_in_session": 1,
    })
    channel = object.__new__(ChatChannel)
    channel.sessions = {}
    channel.lock = threading.Lock()
    context = Context(ContextType.TEXT, "ordinary message", {
        "session_id": "session",
    })

    ChatChannel.produce(channel, context)

    assert list(channel.sessions) == ["session"]
    assert channel.sessions["session"][0].get() is context


def test_web_steer_button_payload_is_handled_inline(monkeypatch):
    from channel.web import web_channel

    bridge, factory = _fake_agent_bridge(SteerResult(SteerStatus.ACCEPTED))
    monkeypatch.setattr("bridge.bridge.Bridge", lambda: factory)
    monkeypatch.setattr(web_channel.web, "data", lambda: json.dumps({
        "session_id": "session",
        "message": "focus on tests",
        "steer": True,
        "lang": "en",
    }).encode())
    raw_class = web_channel.WebChannel.__closure__[0].cell_contents
    instance = object.__new__(raw_class)

    response = json.loads(raw_class.post_message(instance))

    assert response == {
        "status": "success",
        "request_id": "",
        "stream": False,
        "steered": True,
        "inline_reply": "↪️ Active task redirected.",
    }
    bridge.steer_session.assert_called_once_with("session", "focus on tests")


def test_web_steer_does_not_start_a_run_when_session_is_idle(monkeypatch):
    from channel.web import web_channel

    _, factory = _fake_agent_bridge(SteerResult(SteerStatus.INACTIVE))
    monkeypatch.setattr("bridge.bridge.Bridge", lambda: factory)
    monkeypatch.setattr(web_channel.web, "data", lambda: json.dumps({
        "session_id": "idle",
        "message": "/steer change course",
        "stream": True,
        "lang": "en",
    }).encode())
    raw_class = web_channel.WebChannel.__closure__[0].cell_contents
    instance = object.__new__(raw_class)

    response = json.loads(raw_class.post_message(instance))

    assert response["stream"] is False
    assert response["steered"] is False
    assert response["inline_reply"] == "No active task to steer."
