import threading

import pytest

from a2a_agent.config import AgentConfig
from a2a_agent.config import DeviceRole
from a2a_agent.config import ShareConfig
from a2a_agent.config import load_config
from a2a_agent.protocol import Message
from a2a_agent.protocol import Task
from a2a_agent import runtime
from a2a_agent import topics
from a2a_agent.runtime import _cache_task_share_config
from a2a_agent.runtime import resolve_device_name
from a2a_agent.runtime import send_task


def test_resolve_device_name_accepts_alias() -> None:
    config = AgentConfig(
        devices={"windows-cursor": DeviceRole(role="cursor")},
        aliases={"회사 PC": "windows-cursor"},
    )

    assert resolve_device_name(config, "회사 PC") == "windows-cursor"


def test_resolve_device_name_accepts_registered_device() -> None:
    config = AgentConfig(
        devices={"windows-pc": DeviceRole(role="opencode")},
        aliases={"데스크탑 PC": "windows-pc"},
    )

    assert resolve_device_name(config, "windows-pc") == "windows-pc"


def test_resolve_device_name_rejects_unknown_when_devices_are_registered() -> None:
    config = AgentConfig(
        devices={"windows-pc": DeviceRole(role="opencode")},
    )

    with pytest.raises(RuntimeError, match="unknown device or alias"):
        resolve_device_name(config, "회사 PC")


def test_send_task_includes_commander_share(monkeypatch) -> None:
    share = ShareConfig(
        host="nas.example.com",
        port=5001,
        user="a2a-worker",
        password="secret",
        folder="/a2a-share",
        insecure=True,
    )
    config = AgentConfig(
        room="team",
        device_name="commander",
        role="commander",
        share=share,
    )
    fake_client = FakeMqttClient()

    monkeypatch.setattr(runtime, "load_config", lambda: config)
    monkeypatch.setattr(runtime, "MqttClient", lambda client_id: fake_client)
    monkeypatch.setattr(runtime.time, "sleep", lambda seconds: None)

    result = send_task("worker-a", "hello")

    request = Task.from_json(fake_client.request_payload)
    assert result.status == "completed"
    assert request.share == share.to_dict()


def test_worker_caches_task_share_to_config(monkeypatch, tmp_path) -> None:
    monkeypatch.setenv("A2A_HOME", str(tmp_path))
    share = {
        "host": "nas.example.com",
        "port": 5001,
        "user": "a2a-worker",
        "password": "secret",
        "folder": "/a2a-share",
        "insecure": True,
    }
    config = AgentConfig(room="team", device_name="worker-a", role="worker")
    task = Task(
        id="task-001",
        from_="commander",
        to="worker-a",
        status="pending",
        messages=[Message(role="user", parts=[{"type": "text", "text": "hello"}])],
        share=share,
    )

    updated = _cache_task_share_config(config, task)

    expected = ShareConfig.from_dict(share)
    assert updated.share == expected
    assert load_config(tmp_path / "config.yaml").share == expected


def test_worker_callback_enqueues_for_daemon_processing_thread(monkeypatch, tmp_path) -> None:
    config = AgentConfig(
        room="team",
        device_name="worker-a",
        role="worker",
        token="worker-token",
    )
    fake_client = FakeWorkerMqttClient()
    stop = threading.Event()
    execute_started = threading.Event()
    release_execute = threading.Event()
    callback_returned = threading.Event()
    thread_details: dict[str, object] = {}
    worker_errors: list[BaseException] = []

    def fake_download(config: AgentConfig, task: Task):
        thread_details["download_ident"] = threading.get_ident()
        return [], []

    def fake_execute(prompt: str, config: AgentConfig):
        current = threading.current_thread()
        thread_details["execute_ident"] = current.ident
        thread_details["execute_name"] = current.name
        thread_details["execute_daemon"] = current.daemon
        execute_started.set()
        if not release_execute.wait(5):
            return runtime.ExecutorResult(ok=False, text="test release timed out")
        return runtime.ExecutorResult(ok=True, text="ok")

    monkeypatch.setattr(runtime, "load_config", lambda: config)
    monkeypatch.setattr(runtime, "_connected_client", lambda *args: fake_client)
    monkeypatch.setattr(runtime, "_install_signal_handlers", lambda event: None)
    monkeypatch.setattr(runtime, "start_runtime_auto_update_monitor", lambda event: None)
    monkeypatch.setattr(runtime, "_download_input_files", fake_download)
    monkeypatch.setattr(runtime, "_task_out_dir", lambda task_id: tmp_path / task_id)
    monkeypatch.setattr(runtime, "_upload_output_files", lambda *args: [])
    monkeypatch.setattr(runtime, "execute_text", fake_execute)

    def run() -> None:
        try:
            runtime.run_worker(stop)
        except BaseException as exc:
            worker_errors.append(exc)

    worker_thread = threading.Thread(target=run, name="worker-run-test")
    worker_thread.start()
    callback_thread: threading.Thread | None = None
    try:
        assert fake_client.request_subscribed.wait(2)

        task = Task(
            id="task-queued",
            from_="commander",
            to="worker-a",
            status="pending",
            messages=[Message(role="user", parts=[{"type": "text", "text": "hello"}])],
        )
        request_topic = topics.cmd_request_topic("team", "worker-a")

        def invoke_callback() -> None:
            thread_details["callback_ident"] = threading.get_ident()
            fake_client.subscriptions[request_topic](
                request_topic,
                task.to_json().encode("utf-8"),
                None,
            )
            callback_returned.set()

        callback_thread = threading.Thread(target=invoke_callback, name="mqtt-callback-test")
        callback_thread.start()
        assert execute_started.wait(2)
        # 실행기가 막혀 있어도 MQTT 콜백은 큐에 넣은 뒤 이미 반환해야 한다.
        assert callback_returned.wait(1)
        assert fake_client.working_published.is_set()
        assert thread_details["execute_ident"] != thread_details["callback_ident"]
        assert thread_details["download_ident"] == thread_details["execute_ident"]
        assert thread_details["execute_name"] == "a2a-worker-task-processor"
        assert thread_details["execute_daemon"] is True
        release_execute.set()
        assert fake_client.response_published.wait(2)
    finally:
        release_execute.set()
        stop.set()
        if callback_thread is not None:
            callback_thread.join(timeout=2)
        worker_thread.join(timeout=3)

    assert callback_thread is not None
    assert not callback_thread.is_alive()
    assert not worker_thread.is_alive()
    assert worker_errors == []
    assert fake_client.response_status == "completed"


def test_worker_callback_ignores_task_after_stop(monkeypatch) -> None:
    config = AgentConfig(
        room="team",
        device_name="worker-a",
        role="worker",
        token="worker-token",
    )
    fake_client = FakeWorkerMqttClient()
    stop = threading.Event()
    stop.set()

    monkeypatch.setattr(runtime, "load_config", lambda: config)
    monkeypatch.setattr(runtime, "_connected_client", lambda *args: fake_client)
    monkeypatch.setattr(runtime, "_install_signal_handlers", lambda event: None)
    monkeypatch.setattr(runtime, "start_runtime_auto_update_monitor", lambda event: None)
    monkeypatch.setattr(
        runtime,
        "execute_text",
        lambda *args: pytest.fail("executor must not run after worker stop"),
    )

    runtime.run_worker(stop)

    task = Task(
        id="task-after-stop",
        from_="commander",
        to="worker-a",
        status="pending",
        messages=[Message(role="user", parts=[{"type": "text", "text": "hello"}])],
    )
    request_topic = topics.cmd_request_topic("team", "worker-a")
    fake_client.subscriptions[request_topic](
        request_topic,
        task.to_json().encode("utf-8"),
        None,
    )

    assert not fake_client.working_published.is_set()
    assert not fake_client.response_published.is_set()


def test_worker_reports_unexpected_processing_failure(monkeypatch) -> None:
    config = AgentConfig(
        room="team",
        device_name="worker-a",
        role="worker",
        token="worker-token",
    )
    fake_client = FakeWorkerMqttClient()
    stop = threading.Event()
    worker_errors: list[BaseException] = []

    monkeypatch.setattr(runtime, "load_config", lambda: config)
    monkeypatch.setattr(runtime, "_connected_client", lambda *args: fake_client)
    monkeypatch.setattr(runtime, "_install_signal_handlers", lambda event: None)
    monkeypatch.setattr(runtime, "start_runtime_auto_update_monitor", lambda event: None)
    monkeypatch.setattr(
        runtime,
        "_download_input_files",
        lambda *args: (_ for _ in ()).throw(RuntimeError("download crashed")),
    )

    def run() -> None:
        try:
            runtime.run_worker(stop)
        except BaseException as exc:
            worker_errors.append(exc)

    worker_thread = threading.Thread(target=run, name="worker-failure-test")
    worker_thread.start()
    try:
        assert fake_client.request_subscribed.wait(2)
        task = Task(
            id="task-failed",
            from_="commander",
            to="worker-a",
            status="pending",
            messages=[Message(role="user", parts=[{"type": "text", "text": "hello"}])],
        )
        request_topic = topics.cmd_request_topic("team", "worker-a")
        fake_client.subscriptions[request_topic](
            request_topic,
            task.to_json().encode("utf-8"),
            None,
        )

        assert fake_client.response_published.wait(2)
        assert fake_client.failed_status_published.wait(2)
    finally:
        stop.set()
        worker_thread.join(timeout=3)

    assert not worker_thread.is_alive()
    assert worker_errors == []
    assert fake_client.response_status == "failed"


class FakePublishInfo:
    def wait_for_publish(self, timeout: int) -> None:
        return None


class FakeUnacknowledgedPublishInfo(FakePublishInfo):
    def wait_for_publish(self, timeout: int) -> None:
        raise AssertionError("working status publish must not wait inside MQTT callback")


class FakeMqttClient:
    def __init__(self) -> None:
        self.subscriptions = {}
        self.request_payload = ""

    def connect(self, host: str, port: int, *, tls: bool) -> None:
        return None

    def loop_start(self) -> None:
        return None

    def loop_stop(self) -> None:
        return None

    def wait_connected(self) -> bool:
        return True

    def subscribe(self, topic: str, callback) -> None:
        self.subscriptions[topic] = callback

    def publish(
        self,
        topic: str,
        payload: str,
        *,
        qos: int,
        retain: bool,
    ) -> FakePublishInfo:
        if topic == topics.cmd_request_topic("team", "worker-a"):
            self.request_payload = payload
            response = Task(
                id=Task.from_json(payload).id,
                from_="worker-a",
                to="commander",
                status="completed",
                messages=[Message(role="agent", parts=[{"type": "text", "text": "ok"}])],
            )
            response_topic = topics.cmd_response_topic("team", "worker-a")
            self.subscriptions[response_topic](
                response_topic,
                response.to_json().encode("utf-8"),
                None,
            )
        return FakePublishInfo()

    def disconnect(self) -> None:
        return None


class FakeWorkerMqttClient:
    def __init__(self) -> None:
        self.subscriptions = {}
        self.request_subscribed = threading.Event()
        self.working_published = threading.Event()
        self.response_published = threading.Event()
        self.failed_status_published = threading.Event()
        self.response_status = ""

    def subscribe(self, topic: str, callback) -> None:
        self.subscriptions[topic] = callback
        if topic == topics.cmd_request_topic("team", "worker-a"):
            self.request_subscribed.set()

    def publish(
        self,
        topic: str,
        payload: str,
        *,
        qos: int,
        retain: bool,
    ) -> FakePublishInfo:
        if topic == topics.cmd_task_status_topic("team", "worker-a"):
            status = Task.from_json(payload).status
            if status == "working":
                self.working_published.set()
                return FakeUnacknowledgedPublishInfo()
            if status == "failed":
                self.failed_status_published.set()
        elif topic == topics.cmd_response_topic("team", "worker-a"):
            self.response_status = Task.from_json(payload).status
            self.response_published.set()
        return FakePublishInfo()

    def disconnect(self) -> None:
        return None

    def loop_stop(self) -> None:
        return None
