"""Integration tests against a real nats-server.

Run only when ``ANA_NATS_URL`` is set in the environment or a local
nats-server is reachable on 127.0.0.1:4222. The fixture probes once and
skips the module otherwise — CI without NATS still passes.
"""
from __future__ import annotations

import asyncio
import os
import socket

import pytest

NATS_URL = os.environ.get("ANA_NATS_URL", "nats://127.0.0.1:4222")
NATS_HOST = NATS_URL.split("://", 1)[1].split(":")[0]
NATS_PORT = int(NATS_URL.rsplit(":", 1)[1])


def _nats_reachable() -> bool:
    try:
        with socket.create_connection((NATS_HOST, NATS_PORT), timeout=0.5):
            return True
    except OSError:
        return False


if not _nats_reachable():
    pytest.skip(f"nats-server not reachable at {NATS_URL}", allow_module_level=True)


from ana import AgentBus, Query, Reply, SubjectScheme  # noqa: E402
from ana.client import InboundContext  # noqa: E402


@pytest.mark.asyncio
async def test_query_and_reply_round_trip() -> None:
    prefix = f"ana-test-{os.getpid()}"
    scheme = SubjectScheme(prefix=prefix)

    received: list[str] = []

    async def responder_handler(env, ctx: InboundContext) -> None:
        if isinstance(env, Query):
            received.append(env.query)
            await ctx.reply({"echo": env.query})

    responder = AgentBus("responder", nats_url=NATS_URL, scheme=scheme)
    caller = AgentBus("caller", nats_url=NATS_URL, scheme=scheme)
    await responder.connect()
    await caller.connect()
    try:
        await responder.subscribe_to_me(responder_handler)
        # let the subscription propagate
        await asyncio.sleep(0.1)
        reply = await caller.query_and_wait("responder", "ping", timeout_s=3.0)
        assert reply is not None
        assert isinstance(reply, Reply)
        assert reply.data == {"echo": "ping"}
        assert received == ["ping"]
    finally:
        await caller.close()
        await responder.close()


@pytest.mark.asyncio
async def test_query_and_wait_timeout_returns_none() -> None:
    prefix = f"ana-test-{os.getpid()}-timeout"
    scheme = SubjectScheme(prefix=prefix)
    caller = AgentBus("caller", nats_url=NATS_URL, scheme=scheme)
    await caller.connect()
    try:
        reply = await caller.query_and_wait("ghost", "anybody?", timeout_s=0.5)
        assert reply is None
    finally:
        await caller.close()


@pytest.mark.asyncio
async def test_broadcast_alert_reaches_scope_self_subscriber() -> None:
    """v0.4 — broadcast goes to <prefix>.broadcast.alert.<event>; a
    listener that subscribes to its own scope=self PLUS broadcasts must
    pick it up. (Was the v0.3 design gap ITX caught.)"""
    prefix = f"ana-test-{os.getpid()}-bcast"
    scheme = SubjectScheme(prefix=prefix)

    seen: list[str] = []

    async def listener_handler(env, _ctx: InboundContext) -> None:
        if env.type == "alert":
            seen.append(env.event)  # type: ignore[attr-defined]

    listener = AgentBus("scope-self-bot", nats_url=NATS_URL, scheme=scheme)
    sender = AgentBus("any-sender", nats_url=NATS_URL, scheme=scheme)
    await listener.connect()
    await sender.connect()
    try:
        await listener.subscribe_to_me(listener_handler)
        await listener.subscribe_broadcasts(listener_handler)
        await asyncio.sleep(0.1)
        await sender.broadcast_alert(event="bcast-test", data={"x": 1})
        await asyncio.sleep(0.2)
        assert "bcast-test" in seen, "scope=self listener with broadcast sub did not receive broadcast"
    finally:
        await sender.close()
        await listener.close()


@pytest.mark.asyncio
async def test_broadcast_identity_rejected_for_real_agent() -> None:
    """Real agents must not use the reserved broadcast identity."""
    import pytest as _pt
    with _pt.raises(ValueError):
        AgentBus("broadcast", nats_url=NATS_URL)


@pytest.mark.asyncio
async def test_discovery_announcement_picked_up_by_listener() -> None:
    prefix = f"ana-test-{os.getpid()}-disc"
    scheme = SubjectScheme(prefix=prefix)

    seen: list[str] = []

    async def listener_handler(env, _ctx: InboundContext) -> None:
        if env.type == "discovery":
            seen.append(env.from_)

    listener = AgentBus("listener", nats_url=NATS_URL, scheme=scheme)
    announcer = AgentBus("announcer", nats_url=NATS_URL, scheme=scheme)
    await listener.connect()
    await announcer.connect()
    try:
        await listener.subscribe(scheme.all_under_prefix(), listener_handler)
        await asyncio.sleep(0.1)
        await announcer.announce_discovery(role="worker", cluster="test")
        await asyncio.sleep(0.2)
        assert "announcer" in seen
    finally:
        await listener.close()
        await announcer.close()
