"""Tests for probe loop auto-removal of low-reputation marketplace nodes."""

from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch

from llmhosts.marketplace.probes import (
    ProbeResult,
    _check_and_remove_low_reputation,
)


async def test_removes_low_uptime_nodes():
    """Nodes below REPUTATION_REMOVAL_THRESHOLD trigger auto-remove API call."""
    now = datetime.now(timezone.utc)
    results = [
        ProbeResult(device_id="dev-bad", timestamp=now, latency_ms=9999.0, success=False),
        ProbeResult(device_id="dev-good", timestamp=now, latency_ms=50.0, success=True),
    ]

    store = MagicMock()
    # dev-bad has 10% uptime (below 30% threshold), dev-good has 95%
    store.uptime_30d = AsyncMock(side_effect=lambda device_id: 10.0 if device_id == "dev-bad" else 95.0)

    with patch("httpx.AsyncClient") as mock_cls:
        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json = MagicMock(return_value={"disabled": 1})
        mock_client.post = AsyncMock(return_value=mock_response)
        mock_cls.return_value = mock_client

        await _check_and_remove_low_reputation(
            results=results,
            store=store,
            control_plane_url="https://llmhosts.com",
            internal_secret="test-secret",
        )

        mock_client.post.assert_called_once()
        call_args = mock_client.post.call_args
        posted_ids = call_args[1]["json"]["device_ids"]
        assert "dev-bad" in posted_ids
        assert "dev-good" not in posted_ids
        assert call_args[1]["headers"]["x-internal-secret"] == "test-secret"


async def test_skips_api_when_all_nodes_healthy():
    """No API call when all nodes exceed the uptime threshold."""
    now = datetime.now(timezone.utc)
    results = [
        ProbeResult(device_id="dev-good", timestamp=now, latency_ms=50.0, success=True),
    ]

    store = MagicMock()
    store.uptime_30d = AsyncMock(return_value=99.0)

    with patch("httpx.AsyncClient") as mock_cls:
        await _check_and_remove_low_reputation(
            results=results,
            store=store,
            control_plane_url="https://llmhosts.com",
            internal_secret="test-secret",
        )
        mock_cls.assert_not_called()


async def test_skips_when_no_control_plane_url():
    """No API call when control_plane_url is empty."""
    now = datetime.now(timezone.utc)
    results = [
        ProbeResult(device_id="dev-bad", timestamp=now, latency_ms=9999.0, success=False),
    ]
    store = MagicMock()
    store.uptime_30d = AsyncMock(return_value=5.0)

    with patch("httpx.AsyncClient") as mock_cls:
        await _check_and_remove_low_reputation(
            results=results,
            store=store,
            control_plane_url="",
            internal_secret="test-secret",
        )
        mock_cls.assert_not_called()


async def test_skips_when_no_internal_secret():
    """No API call when internal_secret is empty."""
    now = datetime.now(timezone.utc)
    results = [
        ProbeResult(device_id="dev-bad", timestamp=now, latency_ms=9999.0, success=False),
    ]
    store = MagicMock()
    store.uptime_30d = AsyncMock(return_value=5.0)

    with patch("httpx.AsyncClient") as mock_cls:
        await _check_and_remove_low_reputation(
            results=results,
            store=store,
            control_plane_url="https://llmhosts.com",
            internal_secret="",
        )
        mock_cls.assert_not_called()


async def test_probe_loop_calls_auto_remove_when_configured():
    """run_probe_loop with control_plane_url calls auto-remove after each cycle."""
    from llmhosts.marketplace.probes import run_probe_loop

    store = MagicMock()
    store.append = AsyncMock()
    store.uptime_30d = AsyncMock(return_value=5.0)  # all nodes are bad

    stop = asyncio.Event()

    with (
        patch(
            "llmhosts.marketplace.probes.fetch_nodes",
            return_value=[{"id": "dev-bad", "tunnel_url": "http://bad.example.com", "name": "Bad Node"}],
        ),
        patch("llmhosts.marketplace.probes.run_probe") as mock_probe,
        patch("llmhosts.marketplace.probes._check_and_remove_low_reputation") as mock_remove,
    ):
        mock_probe.return_value = ProbeResult(
            device_id="dev-bad",
            timestamp=datetime.now(timezone.utc),
            latency_ms=9999.0,
            success=False,
        )
        mock_remove.return_value = None

        # Run one iteration then stop
        async def stop_after_one():
            await asyncio.sleep(0.05)
            stop.set()

        await asyncio.gather(
            run_probe_loop(
                catalog_url="https://llmhosts.com/api/marketplace/nodes",
                store=store,
                interval_seconds=1,
                stop_event=stop,
                control_plane_url="https://llmhosts.com",
                internal_secret="test-secret",
            ),
            stop_after_one(),
        )

        mock_remove.assert_called_once()
        call_kwargs = mock_remove.call_args[1]
        assert call_kwargs["control_plane_url"] == "https://llmhosts.com"
        assert call_kwargs["internal_secret"] == "test-secret"
