"""Tests for the network topology mapping module."""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path

import pytest

from llmhosts.topology.mapper import TopologyMapper
from llmhosts.topology.models import BackendNode, BackendStatus, LatencyBucket, TopologySnapshot, classify_latency
from llmhosts.topology.store import TopologyStore

# ---------------------------------------------------------------------------
# Model / classification tests
# ---------------------------------------------------------------------------


class TestClassifyLatency:
    """Test the latency bucket classification function."""

    def test_green_bucket(self):
        assert classify_latency(50.0, True) == LatencyBucket.green
        assert classify_latency(0.0, True) == LatencyBucket.green
        assert classify_latency(99.9, True) == LatencyBucket.green

    def test_yellow_bucket(self):
        assert classify_latency(100.0, True) == LatencyBucket.yellow
        assert classify_latency(250.0, True) == LatencyBucket.yellow
        assert classify_latency(500.0, True) == LatencyBucket.yellow

    def test_red_bucket(self):
        assert classify_latency(501.0, True) == LatencyBucket.red
        assert classify_latency(1000.0, True) == LatencyBucket.red

    def test_gray_when_offline(self):
        assert classify_latency(50.0, False) == LatencyBucket.gray
        assert classify_latency(0.0, False) == LatencyBucket.gray


class TestBackendNode:
    """Test BackendNode model."""

    def test_defaults(self):
        node = BackendNode(
            backend_id="test",
            backend_type="ollama",
            name="Test",
            url="http://localhost:11434",
        )
        assert node.status == BackendStatus.offline
        assert node.latency_bucket == LatencyBucket.gray
        assert node.latency_ms == 0.0
        assert node.models == []
        assert node.trend == "stable"

    def test_with_values(self):
        node = BackendNode(
            backend_id="ollama-local",
            backend_type="ollama",
            name="Ollama Local",
            url="http://localhost:11434",
            is_local=True,
            status=BackendStatus.online,
            latency_ms=42.5,
            latency_bucket=LatencyBucket.green,
            models=["llama3.2", "mistral"],
        )
        assert node.is_local is True
        assert node.status == BackendStatus.online
        assert len(node.models) == 2


class TestTopologySnapshot:
    """Test TopologySnapshot model."""

    def test_empty_snapshot(self):
        snap = TopologySnapshot()
        assert snap.total_backends == 0
        assert snap.healthy_backends == 0
        assert snap.nodes == []

    def test_snapshot_with_nodes(self):
        node = BackendNode(
            backend_id="test",
            backend_type="ollama",
            name="Test",
            url="http://localhost:11434",
            status=BackendStatus.online,
        )
        snap = TopologySnapshot(nodes=[node], total_backends=1, healthy_backends=1)
        assert snap.total_backends == 1
        assert snap.healthy_backends == 1


# ---------------------------------------------------------------------------
# TopologyMapper tests
# ---------------------------------------------------------------------------


class TestTopologyMapper:
    """Test the TopologyMapper without network access."""

    def test_register_and_unregister(self):
        mapper = TopologyMapper()
        mapper.register_backend("test-1", "ollama", "Test 1", "http://localhost:11434", is_local=True)
        assert mapper.get_node("test-1") is not None
        assert mapper.get_node("nonexistent") is None

        mapper.unregister_backend("test-1")
        assert mapper.get_node("test-1") is None

    def test_record_latency(self):
        mapper = TopologyMapper()
        mapper.register_backend("test-1", "ollama", "Test 1", "http://localhost:11434")

        mapper.record_latency("test-1", 42.5)
        mapper.record_latency("test-1", 38.0)
        mapper.record_latency("test-1", 45.0)

        node = mapper.get_node("test-1")
        assert node is not None
        assert node.latency_ms > 0
        assert node.status == BackendStatus.online

    def test_record_latency_unknown_backend(self):
        mapper = TopologyMapper()
        # Should not raise
        mapper.record_latency("nonexistent", 100.0)

    def test_consecutive_failures_mark_offline(self):
        mapper = TopologyMapper()
        mapper.register_backend("test-1", "ollama", "Test 1", "http://localhost:11434")

        mapper.record_latency("test-1", 100.0, healthy=False)
        mapper.record_latency("test-1", 100.0, healthy=False)
        mapper.record_latency("test-1", 100.0, healthy=False)

        node = mapper.get_node("test-1")
        assert node is not None
        assert node.status == BackendStatus.offline
        assert node.consecutive_failures == 3

    def test_degraded_status(self):
        mapper = TopologyMapper()
        mapper.register_backend("test-1", "ollama", "Test 1", "http://localhost:11434")

        # First success, then one failure
        mapper.record_latency("test-1", 50.0, healthy=True)
        mapper.record_latency("test-1", 100.0, healthy=False)

        node = mapper.get_node("test-1")
        assert node is not None
        assert node.status == BackendStatus.degraded

    def test_recovery_from_offline(self):
        mapper = TopologyMapper()
        mapper.register_backend("test-1", "ollama", "Test 1", "http://localhost:11434")

        # Go offline
        for _ in range(3):
            mapper.record_latency("test-1", 100.0, healthy=False)

        # Recover
        mapper.record_latency("test-1", 50.0, healthy=True)

        node = mapper.get_node("test-1")
        assert node is not None
        assert node.status == BackendStatus.online
        assert node.consecutive_failures == 0

    def test_get_snapshot(self):
        mapper = TopologyMapper()
        mapper.register_backend("local", "ollama", "Local", "http://localhost:11434", is_local=True)
        mapper.register_backend("cloud", "openai", "OpenAI", "https://api.openai.com/v1")

        mapper.record_latency("local", 30.0, healthy=True)
        mapper.record_latency("cloud", 200.0, healthy=True)

        snapshot = mapper.get_snapshot()
        assert snapshot.total_backends == 2
        assert snapshot.healthy_backends == 2
        assert snapshot.avg_latency_ms > 0
        assert len(snapshot.nodes) == 2

    def test_get_fastest_backend(self):
        mapper = TopologyMapper()
        mapper.register_backend("fast", "ollama", "Fast", "http://localhost:11434")
        mapper.register_backend("slow", "openai", "Slow", "https://api.openai.com/v1")

        for _ in range(5):
            mapper.record_latency("fast", 30.0, healthy=True)
            mapper.record_latency("slow", 200.0, healthy=True)

        fastest = mapper.get_fastest_backend()
        assert fastest == "fast"

    def test_get_fastest_backend_skips_offline(self):
        mapper = TopologyMapper()
        mapper.register_backend("fast", "ollama", "Fast", "http://localhost:11434")
        mapper.register_backend("slow", "openai", "Slow", "https://api.openai.com/v1")

        for _ in range(5):
            mapper.record_latency("fast", 30.0, healthy=True)
            mapper.record_latency("slow", 200.0, healthy=True)

        # Take fast backend offline
        for _ in range(3):
            mapper.record_latency("fast", 100.0, healthy=False)

        fastest = mapper.get_fastest_backend()
        assert fastest == "slow"

    def test_get_fastest_backend_with_filter(self):
        mapper = TopologyMapper()
        mapper.register_backend("a", "ollama", "A", "http://localhost:11434")
        mapper.register_backend("b", "openai", "B", "https://api.openai.com/v1")

        for _ in range(5):
            mapper.record_latency("a", 30.0, healthy=True)
            mapper.record_latency("b", 200.0, healthy=True)

        fastest = mapper.get_fastest_backend(backend_ids=["b"])
        assert fastest == "b"

    def test_get_latency_weight(self):
        mapper = TopologyMapper()
        mapper.register_backend("fast", "ollama", "Fast", "http://localhost:11434")
        mapper.register_backend("slow", "openai", "Slow", "https://api.openai.com/v1")

        for _ in range(5):
            mapper.record_latency("fast", 30.0, healthy=True)
            mapper.record_latency("slow", 800.0, healthy=True)

        fast_weight = mapper.get_latency_weight("fast")
        slow_weight = mapper.get_latency_weight("slow")
        unknown_weight = mapper.get_latency_weight("nonexistent")

        assert fast_weight > slow_weight
        assert fast_weight > 0.9  # <50ms should be near 1.0
        assert unknown_weight == 0.5  # default for unknown

    def test_latency_bucket_classification(self):
        mapper = TopologyMapper()
        mapper.register_backend("fast", "ollama", "Fast", "http://localhost:11434")

        for _ in range(5):
            mapper.record_latency("fast", 30.0, healthy=True)

        node = mapper.get_node("fast")
        assert node is not None
        assert node.latency_bucket == LatencyBucket.green

    def test_trend_computation_stable(self):
        mapper = TopologyMapper()
        mapper.register_backend("test", "ollama", "Test", "http://localhost:11434")

        # All similar latencies -> stable
        for _ in range(20):
            mapper.record_latency("test", 50.0, healthy=True)

        node = mapper.get_node("test")
        assert node is not None
        assert node.trend == "stable"

    def test_trend_computation_degrading(self):
        mapper = TopologyMapper()
        mapper.register_backend("test", "ollama", "Test", "http://localhost:11434")

        # Start fast, get slower
        for i in range(30):
            mapper.record_latency("test", 50.0 + i * 10, healthy=True)

        node = mapper.get_node("test")
        assert node is not None
        assert node.trend == "degrading"

    def test_trend_computation_improving(self):
        mapper = TopologyMapper()
        mapper.register_backend("test", "ollama", "Test", "http://localhost:11434")

        # Start slow, get faster
        for i in range(30):
            mapper.record_latency("test", 500.0 - i * 10, healthy=True)

        node = mapper.get_node("test")
        assert node is not None
        assert node.trend == "improving"

    def test_uptime_tracking(self):
        mapper = TopologyMapper()
        mapper.register_backend("test", "ollama", "Test", "http://localhost:11434")

        # 8 successes, 2 failures = 80% uptime
        for _ in range(8):
            mapper.record_latency("test", 50.0, healthy=True)
        for _ in range(2):
            mapper.record_latency("test", 100.0, healthy=False)

        node = mapper.get_node("test")
        assert node is not None
        assert node.uptime_percent == 80.0

    def test_models_tracked(self):
        mapper = TopologyMapper()
        mapper.register_backend(
            "test",
            "ollama",
            "Test",
            "http://localhost:11434",
            models=["llama3.2", "mistral", "phi3"],
        )

        node = mapper.get_node("test")
        assert node is not None
        assert len(node.models) == 3
        assert "llama3.2" in node.models


# ---------------------------------------------------------------------------
# TopologyStore tests
# ---------------------------------------------------------------------------


class TestTopologyStore:
    """Test SQLite topology persistence."""

    @pytest.fixture()
    def store_path(self, tmp_path: Path) -> Path:
        return tmp_path / "topology.db"

    async def test_initialize_and_close(self, store_path: Path):
        store = TopologyStore(store_path)
        await store.initialize()
        assert store_path.exists()
        await store.close()

    async def test_upsert_and_query(self, store_path: Path):
        store = TopologyStore(store_path)
        await store.initialize()

        await store.upsert_node(
            backend_id="test-1",
            backend_type="ollama",
            name="Test Ollama",
            url="http://localhost:11434",
            is_local=True,
            status="online",
            avg_latency_ms=42.5,
            p50_ms=40.0,
            p95_ms=60.0,
            p99_ms=80.0,
            models_json='["llama3.2"]',
            last_seen=datetime.now(tz=timezone.utc).isoformat(),
            last_check=datetime.now(tz=timezone.utc).isoformat(),
            consecutive_failures=0,
            uptime_percent=99.5,
            trend="stable",
        )

        nodes = await store.get_all_nodes()
        assert len(nodes) == 1
        assert nodes[0]["backend_id"] == "test-1"
        assert nodes[0]["backend_type"] == "ollama"
        assert nodes[0]["is_local"] is True
        assert nodes[0]["avg_latency_ms"] == 42.5

        await store.close()

    async def test_upsert_updates_existing(self, store_path: Path):
        store = TopologyStore(store_path)
        await store.initialize()

        # Insert
        await store.upsert_node(
            backend_id="test-1",
            backend_type="ollama",
            name="Test",
            url="http://localhost:11434",
            is_local=True,
            status="online",
            avg_latency_ms=42.5,
            p50_ms=40.0,
            p95_ms=60.0,
            p99_ms=80.0,
            models_json="[]",
            last_seen=None,
            last_check=None,
            consecutive_failures=0,
            uptime_percent=100.0,
            trend="stable",
        )

        # Update
        await store.upsert_node(
            backend_id="test-1",
            backend_type="ollama",
            name="Test Updated",
            url="http://localhost:11434",
            is_local=True,
            status="offline",
            avg_latency_ms=100.0,
            p50_ms=90.0,
            p95_ms=120.0,
            p99_ms=150.0,
            models_json="[]",
            last_seen=None,
            last_check=None,
            consecutive_failures=3,
            uptime_percent=70.0,
            trend="degrading",
        )

        nodes = await store.get_all_nodes()
        assert len(nodes) == 1
        assert nodes[0]["name"] == "Test Updated"
        assert nodes[0]["status"] == "offline"
        assert nodes[0]["avg_latency_ms"] == 100.0

        await store.close()

    async def test_record_and_prune_samples(self, store_path: Path):
        store = TopologyStore(store_path)
        await store.initialize()

        await store.record_sample("test-1", 42.5, True)
        await store.record_sample("test-1", 38.0, True)
        await store.record_sample("test-1", 100.0, False)

        # Prune samples older than now (should remove all)
        from datetime import timedelta

        future = (datetime.now(tz=timezone.utc) + timedelta(hours=1)).isoformat()
        removed = await store.prune_old_samples(future)
        assert removed == 3

        await store.close()

    async def test_not_initialized_raises(self):
        store = TopologyStore(Path("/tmp/nonexistent.db"))
        with pytest.raises(RuntimeError, match="not initialised"):
            await store.get_all_nodes()


# ---------------------------------------------------------------------------
# Topology API route tests
# ---------------------------------------------------------------------------


class TestTopologyRoutes:
    """Test the /api/topology routes."""

    async def test_get_topology_no_mapper(self, client):
        """Without a mapper attached, returns empty topology."""
        resp = await client.get("/api/topology")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total_backends"] == 0
        assert data["nodes"] == []

    async def test_get_topology_with_mapper(self, app, client):
        """With a mapper attached, returns topology data."""
        mapper = TopologyMapper()
        mapper.register_backend("test-local", "ollama", "Ollama Local", "http://localhost:11434", is_local=True)
        mapper.record_latency("test-local", 42.5, healthy=True)
        app.state.topology_mapper = mapper

        resp = await client.get("/api/topology")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total_backends"] == 1
        assert data["healthy_backends"] == 1
        assert len(data["nodes"]) == 1

        node = data["nodes"][0]
        assert node["backend_id"] == "test-local"
        assert node["backend_type"] == "ollama"
        assert node["is_local"] is True
        assert node["status"] == "online"
        assert node["latency_ms"] == 42.5
        assert node["latency_bucket"] == "green"

    async def test_get_topology_multiple_backends(self, app, client):
        """Multiple backends appear in the topology."""
        mapper = TopologyMapper()
        mapper.register_backend("local", "ollama", "Ollama", "http://localhost:11434", is_local=True)
        mapper.register_backend("cloud", "openai", "OpenAI", "https://api.openai.com/v1")

        mapper.record_latency("local", 30.0, healthy=True)
        mapper.record_latency("cloud", 250.0, healthy=True)
        app.state.topology_mapper = mapper

        resp = await client.get("/api/topology")
        data = resp.json()
        assert data["total_backends"] == 2
        assert data["healthy_backends"] == 2

        ids = {n["backend_id"] for n in data["nodes"]}
        assert "local" in ids
        assert "cloud" in ids

    async def test_ping_topology_no_mapper(self, client):
        """Ping without mapper returns empty."""
        resp = await client.post("/api/topology/ping")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total_backends"] == 0

    async def test_topology_node_offline_status(self, app, client):
        """Offline backends show correct status and gray latency bucket."""
        mapper = TopologyMapper()
        mapper.register_backend("broken", "openai", "Broken", "https://api.openai.com/v1")
        for _ in range(3):
            mapper.record_latency("broken", 1000.0, healthy=False)
        app.state.topology_mapper = mapper

        resp = await client.get("/api/topology")
        data = resp.json()
        node = data["nodes"][0]
        assert node["status"] == "offline"
        assert node["latency_bucket"] == "gray"
        assert node["consecutive_failures"] == 3

    async def test_topology_latency_buckets(self, app, client):
        """Verify correct latency bucket assignment for different backends."""
        mapper = TopologyMapper()
        mapper.register_backend("fast", "ollama", "Fast", "http://localhost:11434", is_local=True)
        mapper.register_backend("medium", "openai", "Medium", "https://api.openai.com/v1")
        mapper.register_backend("slow", "anthropic", "Slow", "https://api.anthropic.com/v1")

        for _ in range(5):
            mapper.record_latency("fast", 30.0, healthy=True)
            mapper.record_latency("medium", 250.0, healthy=True)
            mapper.record_latency("slow", 800.0, healthy=True)
        app.state.topology_mapper = mapper

        resp = await client.get("/api/topology")
        data = resp.json()

        nodes_by_id = {n["backend_id"]: n for n in data["nodes"]}
        assert nodes_by_id["fast"]["latency_bucket"] == "green"
        assert nodes_by_id["medium"]["latency_bucket"] == "yellow"
        assert nodes_by_id["slow"]["latency_bucket"] == "red"
