"""Tracker-backed latency experiments and model-speed drill-down."""

import http.server
import json
import threading
import time
import urllib.request

import pytest

from meshnet_tracker.server import TrackerServer


MODELS = {
    "qwen2.5-0.5b-instruct": (24, "Qwen/Qwen2.5-0.5B-Instruct"),
    "qwen3.6-35b-a3b": (40, "unsloth/Qwen3.6-35B-A3B"),
}


def _post_json(url: str, payload: dict) -> dict:
    request = urllib.request.Request(
        url,
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=10.0) as response:
        return json.loads(response.read())


def _get_json(url: str) -> dict:
    with urllib.request.urlopen(url, timeout=10.0) as response:
        return json.loads(response.read())


class _LatencyNode(http.server.BaseHTTPRequestHandler):
    """Synthetic node: every downstream Activation Seam adds deterministic delay."""

    base_delay_seconds = 0.004
    seam_delay_seconds = 0.006

    def log_message(self, *_args):
        pass

    def do_POST(self):
        self.rfile.read(int(self.headers.get("Content-Length", 0)))
        downstream = json.loads(self.headers.get("X-Meshnet-Route", "[]"))
        time.sleep(self.base_delay_seconds + self.seam_delay_seconds * len(downstream))
        body = json.dumps({
            "choices": [{"message": {"role": "assistant", "content": "ok " * 40}}],
            "usage": {"prompt_tokens": 10, "completion_tokens": 40},
        }).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


def _start_latency_nodes(count: int):
    nodes = []
    threads = []
    for _ in range(count):
        node = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _LatencyNode)
        thread = threading.Thread(target=node.serve_forever, daemon=True)
        thread.start()
        nodes.append(node)
        threads.append(thread)
    return nodes, threads


@pytest.mark.parametrize("model", MODELS)
@pytest.mark.parametrize("hardware", ["gpu", "gpu-cpu", "cpu"])
def test_tracker_records_increasing_hop_latency_for_model_and_hardware(model, hardware):
    "One through five hops must preserve a measurable seam penalty in tracker stats.\n\nTags: model, node"

    layer_count, hf_repo = MODELS[model]
    nodes, threads = _start_latency_nodes(5)
    tracker = TrackerServer(enable_billing=False, model_presets={
        model: {
            "layers_start": 0,
            "layers_end": layer_count - 1,
            "hf_repo": hf_repo,
            "aliases": [model],
        }
    })
    tracker_port = tracker.start()
    try:
        registered_ids = []
        for index, node in enumerate(nodes):
            start = (layer_count * index) // 5
            end = (layer_count * (index + 1)) // 5 - 1
            device = "cuda" if hardware == "gpu" or (hardware == "gpu-cpu" and index == 0) else "cpu"
            data = _post_json(
                f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
                {
                    "endpoint": f"http://127.0.0.1:{node.server_address[1]}",
                    "model": model,
                    "hf_repo": hf_repo,
                    "num_layers": layer_count,
                    "shard_start": start,
                    "shard_end": end,
                    "tracker_mode": index == 0,
                    "hardware_profile": {"device": device},
                    "vram_bytes": 8_000_000_000 if device == "cuda" else 0,
                    "ram_bytes": 32_000_000_000,
                    "benchmark_tokens_per_sec": 100.0,
                },
            )
            registered_ids.append(data["node_id"])

        for hops in range(1, 6):
            route = registered_ids[:hops]
            # Widen the final shard to make each pinned prefix a complete route.
            with tracker._server.lock:
                tracker._server.registry[route[-1]].shard_end = layer_count - 1
            _post_json(
                f"http://127.0.0.1:{tracker_port}/v1/chat/completions",
                {
                    "model": model,
                    "messages": [{"role": "user", "content": "measure"}],
                    "route": route,
                },
            )

        report = _get_json(f"http://127.0.0.1:{tracker_port}/v1/model-speed?model={model}")
    finally:
        tracker.stop()
        for node, thread in zip(nodes, threads):
            node.shutdown()
            node.server_close()
            thread.join(timeout=1.0)

    routes = {entry["hop_count"]: entry for entry in report["routes"]}
    assert set(routes) == {1, 2, 3, 4, 5}
    assert routes[5]["latency_ms"] > routes[1]["latency_ms"]
    assert routes[5]["latency_penalty_ms"] > 0
    assert routes[5]["device_mix"] == hardware
    assert report["model"] == model
    assert report["nodes"]


def test_model_speed_dashboard_includes_visualization_and_route_drilldown():
    "Model speed dashboard includes visualization and route drilldown\n\nTags: model, node, routing"
    tracker = TrackerServer(enable_billing=False)
    port = tracker.start()
    try:
        html = urllib.request.urlopen(f"http://127.0.0.1:{port}/dashboard").read().decode()
    finally:
        tracker.stop()

    assert "Model inference speed" in html
    assert "model-speed-chart" in html
    assert "renderModelSpeed" in html
    assert "/v1/model-speed" in html
