"""Swarm announce loop: PSK reading, state transitions, context provider."""

import httpx
import pytest

from navi.identity import InstanceIdentity
from navi.swarm import HiveAnnouncer, read_swarm_key

IDENT = InstanceIdentity(name="quiet-otter", instance_id="11111111-2222-3333-4444-555555555555", created_at="")


def _announcer(transport) -> HiveAnnouncer:
    client = httpx.AsyncClient(transport=transport, base_url="http://hive.test")
    return HiveAnnouncer(
        hive_url="http://hive.test",
        key="secret",
        identity=IDENT,
        port=8099,
        interval=120,
        client=client,
    )


def test_read_swarm_key(tmp_path):
    missing = read_swarm_key(tmp_path / "absent")
    assert missing is None
    empty = tmp_path / "empty"
    empty.write_text("   \n")
    assert read_swarm_key(empty) is None
    good = tmp_path / ".swarm-key"
    good.write_text("  deadbeef  \n")
    assert read_swarm_key(good) == "deadbeef"


@pytest.mark.asyncio
async def test_announce_success_marks_reachable():
    seen = {}

    def handler(request: httpx.Request) -> httpx.Response:
        import json

        seen["url"] = str(request.url)
        seen["key"] = request.headers.get("X-Swarm-Key")
        seen["json"] = json.loads(request.content)
        return httpx.Response(200, json={"ok": True})

    announcer = _announcer(httpx.MockTransport(handler))
    ok = await announcer.announce_once()
    assert ok is True
    status = announcer.get_status()
    assert status["reachable"] is True
    assert status["down_since"] is None
    assert status["last_error"] is None
    # request shape
    assert seen["url"] == "http://hive.test/announce"
    assert seen["key"] == "secret"
    assert seen["json"]["name"] == "quiet-otter"
    assert seen["json"]["port"] == 8099
    assert seen["json"]["instance_id"] == IDENT.instance_id
    assert seen["json"]["meta"]["hostname"]


@pytest.mark.asyncio
async def test_announce_failure_sets_down_state_and_keeps_it():
    calls = {"n": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        calls["n"] += 1
        raise httpx.ConnectError("connection refused")

    announcer = _announcer(httpx.MockTransport(handler))
    assert await announcer.announce_once() is False
    status = announcer.get_status()
    assert status["reachable"] is False
    assert status["down_since"] is not None
    assert "ConnectError" in status["last_error"]
    assert status["consecutive_failures"] == 1

    # second failure: down_since does not shift (no re-log of transition)
    await announcer.announce_once()
    assert announcer.get_status()["down_since"] == status["down_since"]
    assert announcer.get_status()["consecutive_failures"] == 2


@pytest.mark.asyncio
async def test_announce_recovers_after_failure():
    fail = {"on": True}

    def handler(request: httpx.Request) -> httpx.Response:
        if fail["on"]:
            raise httpx.ConnectError("down")
        return httpx.Response(200, json={"ok": True})

    announcer = _announcer(httpx.MockTransport(handler))
    await announcer.announce_once()
    assert announcer.get_status()["reachable"] is False

    fail["on"] = False
    assert await announcer.announce_once() is True
    status = announcer.get_status()
    assert status["reachable"] is True
    assert status["down_since"] is None
    assert status["consecutive_failures"] == 0


@pytest.mark.asyncio
async def test_hive_status_provider_silent_when_healthy_or_unconfigured(monkeypatch):
    from types import SimpleNamespace

    from navi.context_providers import hive_status as provider
    from navi import swarm

    # unconfigured: no context at all
    monkeypatch.setattr(provider, "settings", SimpleNamespace(hive_url=""))
    assert await provider.get_context() is None

    # configured + healthy: silent (token economy)
    monkeypatch.setattr(provider, "settings", SimpleNamespace(hive_url="http://hive.test"))

    class FakeAnnouncer:
        def get_status(self):
            return {"reachable": True, "url": "http://hive.test", "down_since": None, "last_error": None}

    monkeypatch.setattr(swarm, "get_announcer", lambda: FakeAnnouncer())
    assert await provider.get_context() is None


@pytest.mark.asyncio
async def test_hive_status_provider_reports_outage(monkeypatch):
    from types import SimpleNamespace

    from navi.context_providers import hive_status as provider
    from navi import swarm

    monkeypatch.setattr(provider, "settings", SimpleNamespace(hive_url="http://hive.test"))

    class FakeAnnouncer:
        def get_status(self):
            return {
                "reachable": False,
                "url": "http://hive.test",
                "down_since": "2026-09-10T12:00:00+00:00",
                "last_error": "ConnectError: connection refused",
            }

    monkeypatch.setattr(swarm, "get_announcer", lambda: FakeAnnouncer())
    text = await provider.get_context()
    assert text is not None
    assert "unreachable" in text
    assert "http://hive.test" in text
    assert "connection refused" in text

# ── central config reporting ────────────────────────────────────────────────


def test_payload_reports_config_state_and_tags(monkeypatch):
    from navi.overlay.state import get_state, reset_state

    reset_state()
    state = get_state()
    state.version = 4
    state.hash = "sha256:abc"
    state.status = "staged"
    state.restart_required = ["openai_api_key"]

    ident = InstanceIdentity(
        name="lab-box",
        instance_id="22222222-2222-3333-4444-555555555555",
        created_at="",
        tags=("lab", "gpu"),
    )
    announcer = HiveAnnouncer(hive_url="http://hive.test", key="k", identity=ident, port=8099)
    payload = announcer.payload

    assert payload["meta"]["config"] == {
        "version": 4,
        "hash": "sha256:abc",
        "status": "staged",
        "restart_required": ["openai_api_key"],
        "hot_applied": [],
        "error": None,
    }
    assert payload["meta"]["tags"] == ["lab", "gpu"]
    reset_state()


async def test_announce_calls_the_config_hint_from_the_reply():
    hints = []

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            200, json={"ok": True, "config": {"fetch": True, "version": 7}}
        )

    client = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url="http://hive.test")
    announcer = HiveAnnouncer(
        hive_url="http://hive.test",
        key="secret",
        identity=IDENT,
        port=8099,
        client=client,
        on_config_hint=hints.append,
    )
    assert await announcer.announce_once() is True
    assert hints == [{"fetch": True, "version": 7}]


async def test_announce_without_a_hint_does_not_refresh():
    hints = []

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={"ok": True, "config": None})

    client = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url="http://hive.test")
    announcer = HiveAnnouncer(
        hive_url="http://hive.test",
        key="secret",
        identity=IDENT,
        port=8099,
        client=client,
        on_config_hint=hints.append,
    )
    await announcer.announce_once()
    assert hints == []


async def test_a_failing_hint_callback_does_not_fail_the_announce():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(200, json={"ok": True, "config": {"fetch": True}})

    def boom(hint):
        raise RuntimeError("nope")

    client = httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url="http://hive.test")
    announcer = HiveAnnouncer(
        hive_url="http://hive.test",
        key="secret",
        identity=IDENT,
        port=8099,
        client=client,
        on_config_hint=boom,
    )
    assert await announcer.announce_once() is True
    assert announcer.get_status()["reachable"] is True
