from __future__ import annotations

import gzip
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import List

from wynd_dataingest import (
    CompressionOptions,
    GzipCompressionOptions,
    RetryOptions,
    WyndDataIngestClient,
    WyndDataIngestError,
)


class ScenarioHandler(BaseHTTPRequestHandler):
    statuses: List[int] = [200]
    retry_after: str | None = None
    captured_bodies: List[bytes] = []
    captured_headers: List[dict[str, str]] = []

    def do_POST(self) -> None:  # noqa: N802
        length = int(self.headers.get("content-length", "0"))
        body = self.rfile.read(length)
        ScenarioHandler.captured_bodies.append(body)
        ScenarioHandler.captured_headers.append({k.lower(): v for k, v in self.headers.items()})

        status = ScenarioHandler.statuses.pop(0)
        self.send_response(status)
        if ScenarioHandler.retry_after is not None:
            self.send_header("Retry-After", ScenarioHandler.retry_after)
        self.end_headers()
        self.wfile.write(b"ok" if status < 400 else b"error")

    def log_message(self, format: str, *args: object) -> None:  # noqa: A003
        return


def reset_handler(statuses: List[int], retry_after: str | None = None) -> None:
    ScenarioHandler.statuses = list(statuses)
    ScenarioHandler.retry_after = retry_after
    ScenarioHandler.captured_bodies = []
    ScenarioHandler.captured_headers = []


def serve() -> tuple[ThreadingHTTPServer, str]:
    server = ThreadingHTTPServer(("127.0.0.1", 0), ScenarioHandler)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    host, port = server.server_address
    return server, f"http://{host}:{port}/"


def test_retries_retryable_responses_with_exponential_backoff() -> None:
    reset_handler([503, 503, 202])
    server, url = serve()
    client = WyndDataIngestClient(
        url=url,
        retry=RetryOptions(max_attempts=3, initial_delay_ms=1, max_delay_ms=2, jitter_ratio=0),
    )

    result = client.send([{"message": "hello"}])

    assert result.ok is True
    assert result.status_code == 202
    assert result.attempts == 3
    assert ScenarioHandler.captured_bodies == [
        b'{"message": "hello"}\n',
        b'{"message": "hello"}\n',
        b'{"message": "hello"}\n',
    ]

    client.close()
    server.shutdown()
    server.server_close()


def test_uses_retry_after_when_present() -> None:
    reset_handler([429, 200], retry_after="0.01")
    server, url = serve()
    client = WyndDataIngestClient(
        url=url,
        retry=RetryOptions(max_attempts=2, initial_delay_ms=1, max_delay_ms=100, jitter_ratio=0),
    )

    start = time.time()
    result = client.send([{"message": "hello"}])
    elapsed_ms = int((time.time() - start) * 1000)

    assert result.ok is True
    assert result.attempts == 2
    assert elapsed_ms >= 10

    client.close()
    server.shutdown()
    server.server_close()


def test_throws_on_non_retryable_errors() -> None:
    reset_handler([400])
    server, url = serve()
    client = WyndDataIngestClient(
        url=url,
        retry=RetryOptions(max_attempts=4, initial_delay_ms=1, max_delay_ms=2, jitter_ratio=0),
    )

    try:
        client.send([{"message": "hello"}])
        assert False, "Expected WyndDataIngestError"
    except WyndDataIngestError as exc:
        assert exc.status_code == 400

    client.close()
    server.shutdown()
    server.server_close()


def test_gzips_payloads_above_threshold() -> None:
    reset_handler([202])
    server, url = serve()
    client = WyndDataIngestClient(
        url=url,
        mode="text",
        compression=CompressionOptions(gzip=GzipCompressionOptions(min_bytes=10)),
    )

    payload = "this payload should be compressed"
    client.send(payload)

    assert ScenarioHandler.captured_headers[0]["content-encoding"] == "gzip"
    assert gzip.decompress(ScenarioHandler.captured_bodies[0]).decode("utf-8") == payload

    client.close()
    server.shutdown()
    server.server_close()


def test_skips_gzip_below_threshold() -> None:
    reset_handler([200])
    server, url = serve()
    client = WyndDataIngestClient(
        url=url,
        mode="text",
        compression=CompressionOptions(gzip=GzipCompressionOptions(min_bytes=1_000)),
    )

    payload = "small"
    client.send(payload)

    assert "content-encoding" not in ScenarioHandler.captured_headers[0]
    assert ScenarioHandler.captured_bodies[0].decode("utf-8") == payload

    client.close()
    server.shutdown()
    server.server_close()


def test_supports_gzip_auto_mode_with_default_options() -> None:
    reset_handler([200])
    server, url = serve()
    client = WyndDataIngestClient(
        url=url,
        mode="text",
        compression=CompressionOptions(gzip=True),
    )

    payload = "x" * (110 * 1_024)
    client.send(payload)

    assert ScenarioHandler.captured_headers[0]["content-encoding"] == "gzip"
    assert gzip.decompress(ScenarioHandler.captured_bodies[0]).decode("utf-8") == payload

    client.close()
    server.shutdown()
    server.server_close()


def test_requires_sequence_for_default_ndjson_mode() -> None:
    reset_handler([200])
    server, url = serve()
    client = WyndDataIngestClient(url=url)

    try:
        client.send({"message": "hello"})
        assert False, "Expected TypeError"
    except TypeError as exc:
        assert str(exc) == "NDJSON mode requires a sequence payload."

    client.close()
    server.shutdown()
    server.server_close()


def test_sets_table_name_header_from_table_operation() -> None:
    server, url = serve()
    cases = [
        ("insert", "events"),
        ("new", "events"),
        ("update", "events-updates"),
        ("delete", "events-deletes"),
    ]

    for operation, expected in cases:
        reset_handler([200])
        client = WyndDataIngestClient(url=url, table_name="events", table_operation=operation)  # type: ignore[arg-type]
        client.send([{"message": operation}])
        assert ScenarioHandler.captured_headers[0]["table-name"] == expected
        client.close()

    server.shutdown()
    server.server_close()


def test_json_mode_serializes_single_document() -> None:
    reset_handler([200])
    server, url = serve()
    client = WyndDataIngestClient(url=url, mode="json")

    client.send({"message": "hello"})

    assert json.loads(ScenarioHandler.captured_bodies[0].decode("utf-8")) == {"message": "hello"}

    client.close()
    server.shutdown()
    server.server_close()
