from urllib.parse import parse_qsl, urlsplit

import pytest

from silkworm.http import HttpClient
from silkworm.middlewares import DelayMiddleware, RetryMiddleware
from silkworm.request import Request
from silkworm.response import HTMLResponse, Response
from silkworm import response as response_module
from silkworm.spiders import Spider


def test_request_replace_creates_new_request_with_updates():
    original = Request(
        url="https://example.com",
        method="GET",
        headers={"X-Token": "abc"},
        meta={"foo": 1},
    )

    updated = original.replace(method="POST", meta={"foo": 2})

    assert updated is not original
    assert updated.method == "POST"
    assert updated.meta["foo"] == 2
    assert original.method == "GET"
    assert original.meta["foo"] == 1


def test_response_follow_inherits_callback_and_joins_url():
    def callback(resp: Response) -> None:
        return None

    req = Request(url="http://example.com/dir/page", callback=callback)
    resp = Response(url=req.url, status=200, headers={}, body=b"", request=req)

    next_req = resp.follow("next")

    assert next_req.url == "http://example.com/dir/next"
    assert next_req.callback is callback


def test_htmlresponse_doc_is_cached(monkeypatch: pytest.MonkeyPatch):
    class CountingDocument:
        instances = 0

        def __init__(self, html: str) -> None:
            type(self).instances += 1
            self.html = html

        def select(self, selector: str):
            return [selector]

    monkeypatch.setattr(response_module, "Document", CountingDocument)

    resp = HTMLResponse(
        url="http://example.com",
        status=200,
        headers={},
        body=b"<html></html>",
        request=Request(url="http://example.com"),
    )

    first = resp.doc
    second = resp.doc

    assert first is second
    assert CountingDocument.instances == 1
    assert first.select("body") == ["body"]


def test_httpclient_build_url_merges_params_with_existing_query():
    client = HttpClient()
    req = Request(
        url="https://example.com/search?q=foo&lang=en",
        params={"page": 2, "q": "bar"},
    )

    built = client._build_url(req)
    parsed = dict(parse_qsl(urlsplit(built).query))

    assert parsed == {"q": "bar", "lang": "en", "page": "2"}


def test_httpclient_normalize_headers_handles_multiple_shapes():
    client = HttpClient()
    raw_headers = [
        b"Content-Type: text/html; charset=utf-8",
        ("X-Test", " value "),
        "X-RateLimit: 10",
        "InvalidHeaderWithoutColon",
    ]

    normalized = client._normalize_headers(raw_headers)

    assert normalized["content-type"] == "text/html; charset=utf-8"
    assert normalized["x-test"] == "value"
    assert normalized["x-ratelimit"] == "10"


@pytest.mark.anyio("asyncio")
async def test_retry_middleware_returns_retry_request(monkeypatch: pytest.MonkeyPatch):
    sleep_calls: list[float] = []

    async def fake_sleep(delay: float) -> None:
        sleep_calls.append(delay)

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)

    middleware = RetryMiddleware(max_times=2, backoff_base=0.1)
    request = Request(url="http://example.com")
    response = Response(
        url=request.url, status=500, headers={}, body=b"", request=request
    )

    result = await middleware.process_response(response, Spider())

    assert isinstance(result, Request)
    assert result is not request
    assert result.meta["retry_times"] == 1
    assert sleep_calls == [0.1]


@pytest.mark.anyio("asyncio")
async def test_retry_middleware_sleep_codes_extend_retry(
    monkeypatch: pytest.MonkeyPatch,
):
    sleep_calls: list[float] = []

    async def fake_sleep(delay: float) -> None:
        sleep_calls.append(delay)

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)

    middleware = RetryMiddleware(max_times=1, sleep_http_codes=[403], backoff_base=0.2)
    request = Request(url="http://example.com")
    response = Response(
        url=request.url, status=403, headers={}, body=b"", request=request
    )

    result = await middleware.process_response(response, Spider())

    assert isinstance(result, Request)
    assert result.meta["retry_times"] == 1
    assert sleep_calls == [0.2]


@pytest.mark.anyio("asyncio")
async def test_retry_middleware_retry_without_sleep(monkeypatch: pytest.MonkeyPatch):
    sleep_calls: list[float] = []

    async def fake_sleep(delay: float) -> None:
        sleep_calls.append(delay)

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)

    middleware = RetryMiddleware(
        max_times=1, retry_http_codes=[500], sleep_http_codes=[]
    )
    request = Request(url="http://example.com")
    response = Response(
        url=request.url, status=500, headers={}, body=b"", request=request
    )

    result = await middleware.process_response(response, Spider())

    assert isinstance(result, Request)
    assert result.meta["retry_times"] == 1
    assert sleep_calls == []


@pytest.mark.anyio("asyncio")
async def test_retry_middleware_stops_after_max_times(monkeypatch: pytest.MonkeyPatch):
    async def fake_sleep(_: float) -> None:
        return None

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)
    middleware = RetryMiddleware(max_times=1)
    request = Request(url="http://example.com", meta={"retry_times": 1})
    response = Response(
        url=request.url, status=500, headers={}, body=b"", request=request
    )

    result = await middleware.process_response(response, Spider())

    assert result is response


@pytest.mark.anyio("asyncio")
async def test_delay_middleware_fixed_delay(monkeypatch: pytest.MonkeyPatch):
    sleep_calls: list[float] = []

    async def fake_sleep(delay: float) -> None:
        sleep_calls.append(delay)

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)

    middleware = DelayMiddleware(delay=1.5)
    request = Request(url="http://example.com")

    result = await middleware.process_request(request, Spider())

    assert result is request
    assert sleep_calls == [1.5]


@pytest.mark.anyio("asyncio")
async def test_delay_middleware_random_delay(monkeypatch: pytest.MonkeyPatch):
    sleep_calls: list[float] = []

    async def fake_sleep(delay: float) -> None:
        sleep_calls.append(delay)

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)

    middleware = DelayMiddleware(min_delay=0.5, max_delay=2.0)
    request = Request(url="http://example.com")

    result = await middleware.process_request(request, Spider())

    assert result is request
    assert len(sleep_calls) == 1
    assert 0.5 <= sleep_calls[0] <= 2.0


@pytest.mark.anyio("asyncio")
async def test_delay_middleware_custom_function(monkeypatch: pytest.MonkeyPatch):
    sleep_calls: list[float] = []

    async def fake_sleep(delay: float) -> None:
        sleep_calls.append(delay)

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)

    def custom_delay(request: Request, spider: Spider) -> float:
        return 3.0 if "slow" in request.url else 0.5

    middleware = DelayMiddleware(delay_func=custom_delay)

    slow_request = Request(url="http://slow.example.com")
    result1 = await middleware.process_request(slow_request, Spider())
    assert result1 is slow_request
    assert sleep_calls[-1] == 3.0

    fast_request = Request(url="http://fast.example.com")
    result2 = await middleware.process_request(fast_request, Spider())
    assert result2 is fast_request
    assert sleep_calls[-1] == 0.5


def test_delay_middleware_validation_errors():
    # Must provide at least one configuration
    with pytest.raises(ValueError, match="Must provide one of"):
        DelayMiddleware()

    # Cannot mix delay strategies
    with pytest.raises(ValueError, match="Cannot use both"):
        DelayMiddleware(delay=1.0, min_delay=0.5)

    with pytest.raises(ValueError, match="cannot be used with"):
        DelayMiddleware(delay=1.0, delay_func=lambda r, s: 1.0)

    # min_delay and max_delay must both be provided
    with pytest.raises(ValueError, match="Both min_delay and max_delay"):
        DelayMiddleware(min_delay=0.5)

    with pytest.raises(ValueError, match="Both min_delay and max_delay"):
        DelayMiddleware(max_delay=2.0)

    # Negative values not allowed
    with pytest.raises(ValueError, match="must be non-negative"):
        DelayMiddleware(delay=-1.0)

    with pytest.raises(ValueError, match="must be non-negative"):
        DelayMiddleware(min_delay=-0.5, max_delay=2.0)

    with pytest.raises(ValueError, match="must be non-negative"):
        DelayMiddleware(min_delay=0.5, max_delay=-2.0)

    # min_delay must be <= max_delay
    with pytest.raises(ValueError, match="must be less than or equal to"):
        DelayMiddleware(min_delay=2.0, max_delay=0.5)


@pytest.mark.anyio("asyncio")
async def test_delay_middleware_zero_delay(monkeypatch: pytest.MonkeyPatch):
    sleep_calls: list[float] = []

    async def fake_sleep(delay: float) -> None:
        sleep_calls.append(delay)

    monkeypatch.setattr("silkworm.middlewares.asyncio.sleep", fake_sleep)

    middleware = DelayMiddleware(delay=0.0)
    request = Request(url="http://example.com")

    result = await middleware.process_request(request, Spider())

    assert result is request
    # Zero delay should not call sleep
    assert sleep_calls == []
