import asyncio
from types import SimpleNamespace

import pytest
from vllm import SamplingParams

from vllm_omni.engine.cfg_companion_tracker import CfgCompanionTracker
from vllm_omni.engine.messages import OutputMessage
from vllm_omni.engine.orchestrator import Orchestrator, OrchestratorRequestState
from vllm_omni.engine.stage_engine_core_client import StageEngineCoreClient
from vllm_omni.engine.stage_pool import StagePool
from vllm_omni.inputs.data import OmniDiffusionSamplingParams

pytestmark = [pytest.mark.core_model, pytest.mark.cpu]


class _DummySenderStage:
    stage_type = "llm"
    final_output = False

    def __init__(self, sender_info):
        self._sender_info = sender_info

    def get_kv_sender_info(self):
        return self._sender_info


class _DummyDiffusionStage:
    stage_type = "diffusion"
    final_output = True
    custom_process_input_func = None

    def __init__(self, engine_input_source=None):
        self.engine_input_source = engine_input_source or [0]
        self.calls = []

    async def add_request_async(self, request_id, prompt, sampling_params, kv_sender_info=None):
        self.calls.append(
            {
                "request_id": request_id,
                "prompt": prompt,
                "sampling_params": sampling_params,
                "kv_sender_info": kv_sender_info,
            }
        )


def _build_sender_pool(stage_id: int, sender_info: dict[str, object]) -> StagePool:
    return StagePool(
        stage_id,
        _DummySenderStage(sender_info),
        output_processor=object(),
        stage_vllm_config=SimpleNamespace(model_config=SimpleNamespace(max_model_len=64)),
    )


def test_stage_engine_core_client_builds_kv_sender_info_from_tcp_address():
    client = object.__new__(StageEngineCoreClient)
    client.stage_id = 0
    client.client_addresses = {"input_address": "tcp://10.20.30.40:1234"}
    client._omni_kv_config = None
    client._kv_sender_info = None
    client._kv_sender_initialized = False
    client._kv_sender_host = client._resolve_contact_host()
    client._initialize_kv_sender_endpoint()

    assert client.get_kv_sender_info() == {
        "host": "10.20.30.40",
        "zmq_port": 50151,
    }


def test_stage_engine_core_client_falls_back_to_detected_ip_for_loopback(monkeypatch):
    client = object.__new__(StageEngineCoreClient)
    client.stage_id = 1
    client.client_addresses = {"input_address": "tcp://127.0.0.1:1234"}
    client._omni_kv_config = None
    client._kv_sender_info = None
    client._kv_sender_initialized = False
    monkeypatch.setattr(client, "_detect_local_ip", lambda: "192.168.0.12")
    client._kv_sender_host = client._resolve_contact_host()
    client._initialize_kv_sender_endpoint()

    assert client.get_kv_sender_info() == {
        "host": "192.168.0.12",
        "zmq_port": 50152,
    }


def test_stage_engine_core_client_uses_connector_config_for_sender_port():
    client = object.__new__(StageEngineCoreClient)
    client.stage_id = 3
    client.client_addresses = {"input_address": "tcp://10.20.30.40:1234"}
    client._kv_sender_info = None
    client._kv_sender_initialized = False
    client._omni_kv_config = {
        "omni_from_stage": "3",
        "connector_config": {
            "type": "MooncakeTransferEngineConnector",
            "role": "sender",
            "host": "10.20.30.99",
            "zmq_port": 51000,
        },
    }
    client._kv_sender_host = client._resolve_contact_host()
    client._initialize_kv_sender_endpoint()

    assert client.get_kv_sender_info() == {
        "host": "10.20.30.99",
        "zmq_port": 51103,
    }


def test_stage_engine_core_client_preserves_explicit_loopback_sender_host():
    client = object.__new__(StageEngineCoreClient)
    client.stage_id = 2
    client.client_addresses = {"input_address": "tcp://10.20.30.40:1234"}
    client._kv_sender_info = None
    client._kv_sender_initialized = False
    client._omni_kv_config = {
        "omni_from_stage": "2",
        "connector_config": {
            "type": "MooncakeTransferEngineConnector",
            "role": "sender",
            "host": "127.0.0.1",
            "zmq_port": 51000,
        },
    }
    client._kv_sender_host = client._resolve_contact_host()
    client._initialize_kv_sender_endpoint()

    assert client.get_kv_sender_info() == {
        "host": "127.0.0.1",
        "zmq_port": 51102,
    }


def test_forward_to_diffusion_attaches_kv_sender_info():
    orchestrator = object.__new__(Orchestrator)
    diffusion_stage = _DummyDiffusionStage(engine_input_source=[0])
    sender_pool = _build_sender_pool(0, {"host": "10.0.0.2", "zmq_port": 50151})
    diffusion_pool = StagePool(1, diffusion_stage)

    orchestrator.num_stages = 2
    orchestrator.stage_pools = [sender_pool, diffusion_pool]
    orchestrator._cfg_tracker = CfgCompanionTracker()

    params = OmniDiffusionSamplingParams()
    req_state = OrchestratorRequestState(
        request_id="req-1",
        prompt={"prompt": "hello"},
        sampling_params_list=[SamplingParams(max_tokens=4), params],
        final_stage_id=1,
    )

    output = SimpleNamespace(request_id="req-1", finished=True)
    asyncio.run(Orchestrator._forward_to_next_stage(orchestrator, "req-1", sender_pool.stage_id, output, req_state))

    assert diffusion_stage.calls[0]["request_id"] == "req-1"
    assert diffusion_stage.calls[0]["kv_sender_info"] == {
        0: {"host": "10.0.0.2", "zmq_port": 50151},
    }
    assert req_state.stage_submit_ts[1] > 0


def test_forward_to_diffusion_uses_engine_input_source_for_kv_sender_info():
    orchestrator = object.__new__(Orchestrator)
    diffusion_stage = _DummyDiffusionStage(engine_input_source=[0])
    source_pool = _build_sender_pool(0, {"host": "10.0.0.2", "zmq_port": 50151})
    previous_pool = _build_sender_pool(1, {"host": "10.0.0.9", "zmq_port": 59999})
    diffusion_pool = StagePool(2, diffusion_stage)

    orchestrator.num_stages = 3
    orchestrator.stage_pools = [source_pool, previous_pool, diffusion_pool]
    orchestrator._cfg_tracker = CfgCompanionTracker()

    params = OmniDiffusionSamplingParams()
    req_state = OrchestratorRequestState(
        request_id="req-3",
        prompt={"prompt": "hello"},
        sampling_params_list=[SamplingParams(max_tokens=4), SamplingParams(max_tokens=4), params],
        final_stage_id=2,
    )

    output = SimpleNamespace(request_id="req-3", finished=True)
    asyncio.run(Orchestrator._forward_to_next_stage(orchestrator, "req-3", previous_pool.stage_id, output, req_state))

    assert diffusion_stage.calls[0]["kv_sender_info"] == {
        0: {"host": "10.0.0.2", "zmq_port": 50151},
    }


def test_forward_to_diffusion_returns_terminal_error_for_empty_custom_inputs():
    orchestrator = object.__new__(Orchestrator)
    diffusion_stage = _DummyDiffusionStage(engine_input_source=[0])
    diffusion_stage.custom_process_input_func = lambda *_args, **_kwargs: []
    sender_pool = _build_sender_pool(0, {"host": "10.0.0.2", "zmq_port": 50151})
    diffusion_pool = StagePool(1, diffusion_stage)

    class _AsyncQueue:
        def __init__(self):
            self.items = []

        async def put(self, item):
            self.items.append(item)

    orchestrator.num_stages = 2
    orchestrator.stage_pools = [sender_pool, diffusion_pool]
    orchestrator._cfg_tracker = CfgCompanionTracker()
    orchestrator.output_async_queue = _AsyncQueue()
    orchestrator.request_states = {}
    orchestrator._pd_kv_params = {}

    params = OmniDiffusionSamplingParams()
    req_state = OrchestratorRequestState(
        request_id="req-empty",
        prompt={"prompt": "hello"},
        sampling_params_list=[SamplingParams(max_tokens=4), params],
        final_stage_id=1,
    )
    orchestrator.request_states["req-empty"] = req_state

    output = SimpleNamespace(request_id="req-empty", finished=True)
    asyncio.run(Orchestrator._forward_to_next_stage(orchestrator, "req-empty", 0, output, req_state))

    assert diffusion_stage.calls == []
    assert len(orchestrator.output_async_queue.items) == 1
    terminal_msg = orchestrator.output_async_queue.items[0]
    assert isinstance(terminal_msg, OutputMessage)
    assert terminal_msg.type == "output"
    assert terminal_msg.request_id == "req-empty"
    assert terminal_msg.stage_id == 1
    assert terminal_msg.finished is True
    assert "produced no valid inputs" in terminal_msg.engine_outputs.error
    assert "req-empty" not in orchestrator.request_states


def test_prewarm_diffusion_attaches_kv_sender_info():
    orchestrator = object.__new__(Orchestrator)
    diffusion_stage = _DummyDiffusionStage(engine_input_source=[0])
    sender_pool = _build_sender_pool(0, {"host": "10.0.0.3", "zmq_port": 50151})
    diffusion_pool = StagePool(1, diffusion_stage)

    orchestrator.stage_pools = [sender_pool, diffusion_pool]
    orchestrator.num_stages = 2

    req_state = OrchestratorRequestState(
        request_id="req-2",
        prompt={"prompt": "hello"},
        sampling_params_list=[SamplingParams(max_tokens=4), OmniDiffusionSamplingParams()],
        final_stage_id=1,
    )

    stage0_request = SimpleNamespace(prompt_token_ids=[1, 2, 3])
    asyncio.run(Orchestrator._prewarm_async_chunk_stages(orchestrator, "req-2", stage0_request, req_state))

    assert diffusion_stage.calls[0]["request_id"] == "req-2"
    assert diffusion_stage.calls[0]["kv_sender_info"] == {
        0: {"host": "10.0.0.3", "zmq_port": 50151},
    }
    assert req_state.stage_submit_ts[1] > 0
