"""Shared Range-honoring loopback server for pm downloader tests.

Both test_downloader.py and test_store_resume.py exercise real downloads
through this one server instead of each re-implementing it. Behaviour is
configured with class attributes on :class:`RangeHandler`; the ``dl_server``
fixture starts one and resets its state.
"""

from __future__ import annotations

import hashlib
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

import pytest


class RangeHandler(BaseHTTPRequestHandler):
    payloads: dict = {}
    ranges_seen: list = []           # (path, start, end) from real Range requests
    abort_after: int | None = None   # refuse bytes beyond this payload offset
    slow_per_chunk: float = 0.0      # sleep per served piece (pause tests)
    no_range: bool = False           # ignore Range, serve 200 full body
    etags: bool = True
    chunk: int = 1 << 20             # serve piece size

    def log_message(self, *args):  # noqa: A002 - silence request logging
        pass

    def end_headers(self):
        payload = self.payloads.get(self.path)
        if payload is not None and self.etags:
            self.send_header("ETag", '"' + hashlib.sha256(payload).hexdigest() + '"')
        super().end_headers()

    def do_GET(self):  # noqa: N802 - http.server API
        payload = self.payloads.get(self.path)
        if payload is None:
            self.send_error(404)
            return
        if self.no_range:
            # A server that ignores Range: 200 with the full body, even
            # when the client asked for a byte range.
            self.send_response(200)
            self.send_header("Content-Length", str(len(payload)))
            self.end_headers()
            served = 0
            while served < len(payload):
                if self.abort_after is not None and served >= self.abort_after:
                    self.connection.close()
                    return
                if self.slow_per_chunk:
                    time.sleep(self.slow_per_chunk)
                piece = payload[served:served + self.chunk]
                self.wfile.write(piece)
                self.wfile.flush()
                served += len(piece)
            return
        rng = self.headers.get("Range")
        if rng:
            spec = rng.removeprefix("bytes=")
            if spec == "0-0":
                # probe: 206 with total from Content-Range, 1 byte body
                total = len(payload)
                self.send_response(206)
                self.send_header("Content-Range", f"bytes 0-0/{total}")
                self.send_header("Content-Length", "1")
                self.end_headers()
                self.wfile.write(payload[:1])
                self.wfile.flush()
                return
            start_s, end_s = spec.split("-", 1)
            start, end = int(start_s), int(end_s)
            self.ranges_seen.append((self.path, start, end))
            body = payload[start:end + 1]
            self.send_response(206)
            self.send_header("Content-Range",
                             f"bytes {start}-{end}/{len(payload)}")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            served = 0
            while served < len(body):
                if self.abort_after is not None and start + served >= self.abort_after:
                    self.connection.close()
                    return
                if self.slow_per_chunk:
                    time.sleep(self.slow_per_chunk)
                end = served + self.chunk
                if self.abort_after is not None:
                    end = min(end, self.abort_after - start)
                piece = body[served:end]
                self.wfile.write(piece)
                self.wfile.flush()
                served += len(piece)
            return
        self.send_response(200)
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)
        self.wfile.flush()


@pytest.fixture
def dl_server():
    RangeHandler.payloads = {}
    RangeHandler.ranges_seen = []
    RangeHandler.abort_after = None
    RangeHandler.slow_per_chunk = 0.0
    RangeHandler.no_range = False
    RangeHandler.etags = True
    RangeHandler.chunk = 1 << 20
    server = HTTPServer(("127.0.0.1", 0), RangeHandler)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    try:
        yield server
    finally:
        server.shutdown()
        server.server_close()


def url(server, path: str) -> str:
    return f"http://127.0.0.1:{server.server_port}{path}"
