"""Tests for the denden server: servicer, response helpers, module loading, and gRPC integration."""
from __future__ import annotations

import importlib
import sys
import threading
import time
import types
from concurrent import futures
from unittest import mock

import grpc
import pytest

from denden.gen import denden_pb2, denden_pb2_grpc
from denden.modules.base import Module
from denden.server import (
    DENY_ROLE_NOT_ALLOWED,
    ERR_SUBAGENT_FAILURE,
    VERSION,
    DenDenServer,
    DendenServicer,
    denied_response,
    error_response,
    ok_response,
)


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

def _ask_request(request_id: str = "req-1") -> denden_pb2.DenDenRequest:
    return denden_pb2.DenDenRequest(
        denden_version=VERSION,
        request_id=request_id,
        ask_user=denden_pb2.AskUserPayload(question="pick a color"),
    )


def _delegate_request(request_id: str = "req-2") -> denden_pb2.DenDenRequest:
    return denden_pb2.DenDenRequest(
        denden_version=VERSION,
        request_id=request_id,
        delegate=denden_pb2.DelegatePayload(
            delegate_to="implementer",
            task=denden_pb2.Task(text="do the thing"),
        ),
    )


def _echo_handler(request: denden_pb2.DenDenRequest) -> denden_pb2.DenDenResponse:
    """Handler that echoes the question or task text back."""
    if request.WhichOneof("payload") == "ask_user":
        return ok_response(
            request.request_id,
            ask_user_result=denden_pb2.AskUserResult(text=request.ask_user.question),
        )
    return ok_response(
        request.request_id,
        delegate_result=denden_pb2.DelegateResult(
            text=request.delegate.task.text,
        ),
    )


# ---------------------------------------------------------------------------
# Servicer unit tests (no gRPC transport)
# ---------------------------------------------------------------------------

class TestDendenServicer:
    def setup_method(self):
        self.servicer = DendenServicer()

    def test_missing_request_id(self):
        req = denden_pb2.DenDenRequest(
            ask_user=denden_pb2.AskUserPayload(question="hi"),
        )
        resp = self.servicer.Send(req, None)
        assert resp.status == denden_pb2.ERROR
        assert resp.error.code == "INVALID_REQUEST"
        assert "request_id" in resp.error.message

    def test_no_payload(self):
        req = denden_pb2.DenDenRequest(request_id="req-1")
        resp = self.servicer.Send(req, None)
        assert resp.status == denden_pb2.ERROR
        assert "payload" in resp.error.message

    def test_no_handler_registered(self):
        resp = self.servicer.Send(_ask_request(), None)
        assert resp.status == denden_pb2.ERROR
        assert "no handler registered" in resp.error.message

    def test_ask_user_dispatch(self):
        self.servicer.set_handler("ask_user", _echo_handler)
        resp = self.servicer.Send(_ask_request(), None)
        assert resp.status == denden_pb2.OK
        assert resp.ask_user_result.text == "pick a color"

    def test_delegate_dispatch(self):
        self.servicer.set_handler("delegate", _echo_handler)
        resp = self.servicer.Send(_delegate_request(), None)
        assert resp.status == denden_pb2.OK
        assert resp.delegate_result.text == "do the thing"

    def test_handler_exception(self):
        def bad_handler(req):
            raise RuntimeError("boom")

        self.servicer.set_handler("ask_user", bad_handler)
        resp = self.servicer.Send(_ask_request(), None)
        assert resp.status == denden_pb2.ERROR
        assert resp.error.code == ERR_SUBAGENT_FAILURE
        assert "boom" in resp.error.message

    def test_status(self):
        resp = self.servicer.Status(denden_pb2.StatusRequest(), None)
        assert resp.uptime_seconds >= 0

    def test_only_registered_payload_dispatches(self):
        """Register ask_user only; delegate should fail."""
        self.servicer.set_handler("ask_user", _echo_handler)
        resp = self.servicer.Send(_delegate_request(), None)
        assert resp.status == denden_pb2.ERROR
        assert "no handler registered" in resp.error.message


# ---------------------------------------------------------------------------
# Response helper tests
# ---------------------------------------------------------------------------

class TestResponseHelpers:
    def test_ok_response_ask_user(self):
        result = denden_pb2.AskUserResult(text="blue")
        resp = ok_response("req-1", ask_user_result=result)
        assert resp.status == denden_pb2.OK
        assert resp.request_id == "req-1"
        assert resp.denden_version == VERSION
        assert resp.ask_user_result.text == "blue"

    def test_ok_response_delegate(self):
        result = denden_pb2.DelegateResult(text="done")
        resp = ok_response("req-2", delegate_result=result)
        assert resp.status == denden_pb2.OK
        assert resp.delegate_result.text == "done"

    def test_denied_response(self):
        resp = denied_response("req-1", DENY_ROLE_NOT_ALLOWED, "nope")
        assert resp.status == denden_pb2.DENIED
        assert resp.error.code == DENY_ROLE_NOT_ALLOWED
        assert resp.error.message == "nope"
        assert resp.error.retryable is False

    def test_error_response(self):
        resp = error_response("req-1", "SOME_ERR", "failed", retryable=True)
        assert resp.status == denden_pb2.ERROR
        assert resp.error.code == "SOME_ERR"
        assert resp.error.retryable is True

    def test_error_response_default_not_retryable(self):
        resp = error_response("req-1", "ERR", "bad")
        assert resp.error.retryable is False


# ---------------------------------------------------------------------------
# DenDenServer handler registration
# ---------------------------------------------------------------------------

class TestDenDenServer:
    def test_on_ask_user_registers(self):
        server = DenDenServer()
        server.on_ask_user(_echo_handler)
        assert "ask_user" in server._servicer._handlers

    def test_on_delegate_registers(self):
        server = DenDenServer()
        server.on_delegate(_echo_handler)
        assert "delegate" in server._servicer._handlers

    def test_start_binds_and_sets_bound_addr(self):
        """start() with port 0 assigns a real port and sets bound_addr."""
        server = DenDenServer(addr="127.0.0.1:0")
        server.on_ask_user(_echo_handler)
        try:
            server.start()
            assert server.bound_addr.startswith("127.0.0.1:")
            port = int(server.bound_addr.rsplit(":", 1)[1])
            assert port > 0
        finally:
            server.stop(grace=0)

    def test_default_max_workers(self):
        """Default max_workers should be large enough for deep agent nesting."""
        server = DenDenServer()
        assert server.max_workers == 10_000

    def test_bound_addr_before_start_raises(self):
        """Accessing bound_addr before start() raises RuntimeError."""
        server = DenDenServer()
        with pytest.raises(RuntimeError, match="not started"):
            _ = server.bound_addr

    def test_stop_method(self):
        """stop() gracefully shuts down the server."""
        server = DenDenServer(addr="127.0.0.1:0")
        server.start()
        server.stop(grace=0)

    def test_run_backward_compat(self):
        """run() in a thread still works and bound_addr is accessible."""
        server = DenDenServer(addr="127.0.0.1:0")
        server.on_ask_user(_echo_handler)
        t = threading.Thread(target=server.run, daemon=True)
        t.start()
        time.sleep(0.3)
        assert server.bound_addr.startswith("127.0.0.1:")
        server.stop(grace=0)


# ---------------------------------------------------------------------------
# gRPC integration tests (real transport)
# ---------------------------------------------------------------------------

@pytest.fixture()
def grpc_server():
    """Start a real gRPC server with echo handlers, yield a channel, then stop."""
    servicer = DendenServicer()
    servicer.set_handler("ask_user", _echo_handler)
    servicer.set_handler("delegate", _echo_handler)

    server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
    denden_pb2_grpc.add_DendenServicer_to_server(servicer, server)
    port = server.add_insecure_port("127.0.0.1:0")
    server.start()

    channel = grpc.insecure_channel(f"127.0.0.1:{port}")
    yield channel

    channel.close()
    server.stop(grace=0)


class TestGRPCIntegration:
    def test_send_ask_user(self, grpc_server):
        stub = denden_pb2_grpc.DendenStub(grpc_server)
        resp = stub.Send(_ask_request())
        assert resp.status == denden_pb2.OK
        assert resp.ask_user_result.text == "pick a color"

    def test_send_delegate(self, grpc_server):
        stub = denden_pb2_grpc.DendenStub(grpc_server)
        resp = stub.Send(_delegate_request())
        assert resp.status == denden_pb2.OK
        assert resp.delegate_result.text == "do the thing"

    def test_send_no_payload(self, grpc_server):
        stub = denden_pb2_grpc.DendenStub(grpc_server)
        req = denden_pb2.DenDenRequest(request_id="req-x")
        resp = stub.Send(req)
        assert resp.status == denden_pb2.ERROR

    def test_status(self, grpc_server):
        stub = denden_pb2_grpc.DendenStub(grpc_server)
        resp = stub.Status(denden_pb2.StatusRequest())
        assert resp.uptime_seconds >= 0


# ---------------------------------------------------------------------------
# Large message tests (verifies no message size limit)
# ---------------------------------------------------------------------------

@pytest.fixture()
def large_msg_server():
    """Start DenDenServer with echo handler, yield a channel with unlimited msg size."""
    server = DenDenServer(addr="127.0.0.1:0")
    server.on_ask_user(_echo_handler)
    server.start()

    channel = grpc.insecure_channel(
        server.bound_addr,
        options=[
            ("grpc.max_send_message_length", -1),
            ("grpc.max_receive_message_length", -1),
        ],
    )
    yield channel

    channel.close()
    server.stop(grace=0)


class TestLargeMessage:
    def test_message_exceeding_default_4mb_limit(self, large_msg_server):
        """A message >4MB (gRPC default limit) should succeed with unlimited config."""
        large_text = "x" * (5 * 1024 * 1024)  # 5 MB
        req = denden_pb2.DenDenRequest(
            denden_version=VERSION,
            request_id="req-large",
            ask_user=denden_pb2.AskUserPayload(question=large_text),
        )
        stub = denden_pb2_grpc.DendenStub(large_msg_server)
        resp = stub.Send(req)
        assert resp.status == denden_pb2.OK
        assert resp.ask_user_result.text == large_text


# ---------------------------------------------------------------------------
# Module loading tests
# ---------------------------------------------------------------------------

class _EchoModule(Module):
    """Test module that registers echo handlers via methods()."""

    def name(self) -> str:
        return "echo"

    def methods(self) -> dict:
        return {"ask_user": _echo_handler}

    def on_load(self, server: DenDenServer) -> None:
        pass


class _OnLoadModule(Module):
    """Module that registers handlers in on_load instead of methods()."""

    def __init__(self):
        self.loaded = False

    def name(self) -> str:
        return "onload"

    def methods(self) -> dict:
        return {}

    def on_load(self, server: DenDenServer) -> None:
        self.loaded = True
        server.on_delegate(_echo_handler)


class TestModuleLoading:
    def test_methods_registers_handlers(self):
        """Module.methods() should register handlers on the server."""
        from denden.__main__ import main

        mod_instance = _EchoModule()
        fake_mod = types.ModuleType("fake_echo_mod")
        fake_mod.module = mod_instance

        with mock.patch.dict(sys.modules, {"fake_echo_mod": fake_mod}):
            with mock.patch(
                "sys.argv", ["denden-server", "--load-module", "fake_echo_mod"]
            ):
                with mock.patch.object(DenDenServer, "run"):
                    main()

        # Verify: the main() function should have loaded the module and
        # registered ask_user via methods(). We can't access the server
        # instance directly, so test the module contract separately.
        server = DenDenServer()
        for method_name, handler in mod_instance.methods().items():
            server._servicer.set_handler(method_name, handler)
        mod_instance.on_load(server)

        assert "ask_user" in server._servicer._handlers
        resp = server._servicer.Send(_ask_request(), None)
        assert resp.status == denden_pb2.OK
        assert resp.ask_user_result.text == "pick a color"

    def test_on_load_called(self):
        """on_load() should be called and can register additional handlers."""
        server = DenDenServer()
        mod_instance = _OnLoadModule()

        for method_name, handler in mod_instance.methods().items():
            server._servicer.set_handler(method_name, handler)
        mod_instance.on_load(server)

        assert mod_instance.loaded is True
        assert "delegate" in server._servicer._handlers

    def test_module_attribute_loading(self):
        """Loader should accept modules with a 'module' attribute."""
        from denden.__main__ import main

        fake_mod = types.ModuleType("fake_attr_mod")
        fake_mod.module = _EchoModule()

        with mock.patch.dict(sys.modules, {"fake_attr_mod": fake_mod}):
            with mock.patch(
                "sys.argv", ["denden-server", "--load-module", "fake_attr_mod"]
            ):
                with mock.patch.object(DenDenServer, "run"):
                    main()

    def test_create_module_loading(self):
        """Loader should accept modules with a 'create_module()' function."""
        from denden.__main__ import main

        fake_mod = types.ModuleType("fake_factory_mod")
        fake_mod.create_module = lambda: _EchoModule()

        with mock.patch.dict(sys.modules, {"fake_factory_mod": fake_mod}):
            with mock.patch(
                "sys.argv", ["denden-server", "--load-module", "fake_factory_mod"]
            ):
                with mock.patch.object(DenDenServer, "run"):
                    main()

    def test_invalid_module_raises(self):
        """Loader should raise ValueError for modules without module/create_module."""
        from denden.__main__ import main

        fake_mod = types.ModuleType("fake_bad_mod")

        with mock.patch.dict(sys.modules, {"fake_bad_mod": fake_mod}):
            with mock.patch(
                "sys.argv", ["denden-server", "--load-module", "fake_bad_mod"]
            ):
                with mock.patch.object(DenDenServer, "run"):
                    with pytest.raises(ValueError, match="must expose"):
                        main()
