from __future__ import annotations

import typing
from datetime import timedelta

import chardet
import pytest

import httpx2

if typing.TYPE_CHECKING:
    from conftest import TestServer


def autodetect(content: bytes) -> str | None:
    return chardet.detect(content).get("encoding")


def test_get(server: TestServer) -> None:
    url = server.url
    with httpx2.Client(http2=True) as http:
        response = http.get(url)
    assert response.status_code == 200
    assert response.url == url
    assert response.content == b"Hello, world!"
    assert response.text == "Hello, world!"
    assert response.http_version == "HTTP/1.1"
    assert response.encoding == "utf-8"
    assert response.request.url == url
    assert response.headers
    assert response.is_redirect is False
    assert repr(response) == "<Response [200 OK]>"
    assert response.elapsed > timedelta(0)


@pytest.mark.parametrize(
    "url",
    [
        pytest.param("invalid://example.org", id="scheme-not-http(s)"),
        pytest.param("://example.org", id="no-scheme"),
        pytest.param("http://", id="no-host"),
    ],
)
def test_get_invalid_url(server: TestServer, url: str) -> None:
    with httpx2.Client() as client:
        with pytest.raises((httpx2.UnsupportedProtocol, httpx2.LocalProtocolError)):
            client.get(url)


def test_build_request(server: TestServer) -> None:
    url = server.url.copy_with(path="/echo_headers")
    headers = {"Custom-header": "value"}

    with httpx2.Client() as client:
        request = client.build_request("GET", url)
        request.headers.update(headers)
        response = client.send(request)

    assert response.status_code == 200
    assert response.url == url

    assert response.json()["Custom-header"] == "value"


def test_build_post_request(server: TestServer) -> None:
    url = server.url.copy_with(path="/echo_headers")
    headers = {"Custom-header": "value"}

    with httpx2.Client() as client:
        request = client.build_request("POST", url)
        request.headers.update(headers)
        response = client.send(request)

    assert response.status_code == 200
    assert response.url == url

    assert response.json()["Content-length"] == "0"
    assert response.json()["Custom-header"] == "value"


def test_post(server: TestServer) -> None:
    with httpx2.Client() as client:
        response = client.post(server.url, content=b"Hello, world!")
    assert response.status_code == 200
    assert response.reason_phrase == "OK"


def test_post_json(server: TestServer) -> None:
    with httpx2.Client() as client:
        response = client.post(server.url, json={"text": "Hello, world!"})
    assert response.status_code == 200
    assert response.reason_phrase == "OK"


def test_stream_response(server: TestServer) -> None:
    with httpx2.Client() as client:
        with client.stream("GET", server.url) as response:
            content = response.read()
    assert response.status_code == 200
    assert content == b"Hello, world!"


def test_stream_iterator(server: TestServer) -> None:
    body = b""

    with httpx2.Client() as client:
        with client.stream("GET", server.url) as response:
            for chunk in response.iter_bytes():
                body += chunk

    assert response.status_code == 200
    assert body == b"Hello, world!"


def test_raw_iterator(server: TestServer) -> None:
    body = b""

    with httpx2.Client() as client:
        with client.stream("GET", server.url) as response:
            for chunk in response.iter_raw():
                body += chunk

    assert response.status_code == 200
    assert body == b"Hello, world!"


def test_cannot_stream_async_request(server: TestServer) -> None:
    async def hello_world() -> typing.AsyncIterator[bytes]:  # pragma: no cover
        yield b"Hello, "
        yield b"world!"

    with httpx2.Client() as client:
        with pytest.raises(RuntimeError):
            client.post(server.url, content=hello_world())


def test_raise_for_status(server: TestServer) -> None:
    with httpx2.Client() as client:
        for status_code in (200, 400, 404, 500, 505):
            response = client.request("GET", server.url.copy_with(path=f"/status/{status_code}"))
            if 400 <= status_code < 600:
                with pytest.raises(httpx2.HTTPStatusError) as exc_info:
                    response.raise_for_status()
                assert exc_info.value.response == response
                assert exc_info.value.request.url.path == f"/status/{status_code}"
            else:
                assert response.raise_for_status() is response


def test_options(server: TestServer) -> None:
    with httpx2.Client() as client:
        response = client.options(server.url)
    assert response.status_code == 200
    assert response.reason_phrase == "OK"


def test_head(server: TestServer) -> None:
    with httpx2.Client() as client:
        response = client.head(server.url)
    assert response.status_code == 200
    assert response.reason_phrase == "OK"


def test_put(server: TestServer) -> None:
    with httpx2.Client() as client:
        response = client.put(server.url, content=b"Hello, world!")
    assert response.status_code == 200
    assert response.reason_phrase == "OK"


def test_patch(server: TestServer) -> None:
    with httpx2.Client() as client:
        response = client.patch(server.url, content=b"Hello, world!")
    assert response.status_code == 200
    assert response.reason_phrase == "OK"


def test_delete(server: TestServer) -> None:
    with httpx2.Client() as client:
        response = client.delete(server.url)
    assert response.status_code == 200
    assert response.reason_phrase == "OK"


def test_base_url(server: TestServer) -> None:
    base_url = server.url
    with httpx2.Client(base_url=base_url) as client:
        response = client.get("/")
    assert response.status_code == 200
    assert response.url == base_url


def test_merge_absolute_url() -> None:
    client = httpx2.Client(base_url="https://www.example.com/")
    request = client.build_request("GET", "http://www.example.com/")
    assert request.url == "http://www.example.com/"


def test_merge_relative_url() -> None:
    client = httpx2.Client(base_url="https://www.example.com/")
    request = client.build_request("GET", "/testing/123")
    assert request.url == "https://www.example.com/testing/123"


def test_merge_relative_url_with_path() -> None:
    client = httpx2.Client(base_url="https://www.example.com/some/path")
    request = client.build_request("GET", "/testing/123")
    assert request.url == "https://www.example.com/some/path/testing/123"


def test_merge_relative_url_with_dotted_path() -> None:
    client = httpx2.Client(base_url="https://www.example.com/some/path")
    request = client.build_request("GET", "../testing/123")
    assert request.url == "https://www.example.com/some/testing/123"


def test_merge_relative_url_with_path_including_colon() -> None:
    client = httpx2.Client(base_url="https://www.example.com/some/path")
    request = client.build_request("GET", "/testing:123")
    assert request.url == "https://www.example.com/some/path/testing:123"


def test_merge_relative_url_with_encoded_slashes() -> None:
    client = httpx2.Client(base_url="https://www.example.com/")
    request = client.build_request("GET", "/testing%2F123")
    assert request.url == "https://www.example.com/testing%2F123"

    client = httpx2.Client(base_url="https://www.example.com/base%2Fpath")
    request = client.build_request("GET", "/testing")
    assert request.url == "https://www.example.com/base%2Fpath/testing"


def test_context_managed_transport() -> None:
    class Transport(httpx2.BaseTransport):
        def __init__(self) -> None:
            self.events: list[str] = []

        def close(self) -> None:
            # The base implementation of httpx2.BaseTransport just
            # calls into `.close`, so simple transport cases can just override
            # this method for any cleanup, where more complex cases
            # might want to additionally override `__enter__`/`__exit__`.
            self.events.append("transport.close")

        def __enter__(self) -> Transport:
            super().__enter__()
            self.events.append("transport.__enter__")
            return self

        def __exit__(self, *args: typing.Any) -> None:
            super().__exit__(*args)
            self.events.append("transport.__exit__")

    transport = Transport()
    with httpx2.Client(transport=transport):
        pass

    assert transport.events == [
        "transport.__enter__",
        "transport.close",
        "transport.__exit__",
    ]


def test_context_managed_transport_and_mount() -> None:
    class Transport(httpx2.BaseTransport):
        def __init__(self, name: str) -> None:
            self.name: str = name
            self.events: list[str] = []

        def close(self) -> None:
            # The base implementation of httpx2.BaseTransport just
            # calls into `.close`, so simple transport cases can just override
            # this method for any cleanup, where more complex cases
            # might want to additionally override `__enter__`/`__exit__`.
            self.events.append(f"{self.name}.close")

        def __enter__(self) -> Transport:
            super().__enter__()
            self.events.append(f"{self.name}.__enter__")
            return self

        def __exit__(self, *args: typing.Any) -> None:
            super().__exit__(*args)
            self.events.append(f"{self.name}.__exit__")

    transport = Transport(name="transport")
    mounted = Transport(name="mounted")
    with httpx2.Client(transport=transport, mounts={"http://www.example.org": mounted}):
        pass

    assert transport.events == [
        "transport.__enter__",
        "transport.close",
        "transport.__exit__",
    ]
    assert mounted.events == [
        "mounted.__enter__",
        "mounted.close",
        "mounted.__exit__",
    ]


def hello_world(request: httpx2.Request) -> httpx2.Response:
    return httpx2.Response(200, text="Hello, world!")


def test_client_closed_state_using_implicit_open() -> None:
    client = httpx2.Client(transport=httpx2.MockTransport(hello_world))

    assert not client.is_closed
    client.get("http://example.com")

    assert not client.is_closed
    client.close()

    assert client.is_closed

    # Once we're close we cannot make any more requests.
    with pytest.raises(RuntimeError):
        client.get("http://example.com")

    # Once we're closed we cannot reopen the client.
    with pytest.raises(RuntimeError):
        with client:
            pass  # pragma: no cover


def test_client_closed_state_using_with_block() -> None:
    with httpx2.Client(transport=httpx2.MockTransport(hello_world)) as client:
        assert not client.is_closed
        client.get("http://example.com")

    assert client.is_closed
    with pytest.raises(RuntimeError):
        client.get("http://example.com")


def echo_raw_headers(request: httpx2.Request) -> httpx2.Response:
    data = [(name.decode("ascii"), value.decode("ascii")) for name, value in request.headers.raw]
    return httpx2.Response(200, json=data)


def test_raw_client_header() -> None:
    """
    Set a header in the Client.
    """
    url = "http://example.org/echo_headers"
    headers = {"Example-Header": "example-value"}

    client = httpx2.Client(transport=httpx2.MockTransport(echo_raw_headers), headers=headers)
    response = client.get(url)

    assert response.status_code == 200
    assert response.json() == [
        ["Host", "example.org"],
        ["Accept", "*/*"],
        ["Accept-Encoding", "gzip, deflate, br, zstd"],
        ["Connection", "keep-alive"],
        ["User-Agent", f"python-httpx2/{httpx2.__version__}"],
        ["Example-Header", "example-value"],
    ]


def unmounted(request: httpx2.Request) -> httpx2.Response:
    data = {"app": "unmounted"}
    return httpx2.Response(200, json=data)


def mounted(request: httpx2.Request) -> httpx2.Response:
    data = {"app": "mounted"}
    return httpx2.Response(200, json=data)


def test_mounted_transport() -> None:
    transport = httpx2.MockTransport(unmounted)
    mounts = {"custom://": httpx2.MockTransport(mounted)}

    client = httpx2.Client(transport=transport, mounts=mounts)

    response = client.get("https://www.example.com")
    assert response.status_code == 200
    assert response.json() == {"app": "unmounted"}

    response = client.get("custom://www.example.com")
    assert response.status_code == 200
    assert response.json() == {"app": "mounted"}


def test_all_mounted_transport() -> None:
    mounts = {"all://": httpx2.MockTransport(mounted)}

    client = httpx2.Client(mounts=mounts)

    response = client.get("https://www.example.com")
    assert response.status_code == 200
    assert response.json() == {"app": "mounted"}


def test_server_extensions(server: TestServer) -> None:
    url = server.url.copy_with(path="/http_version_2")
    with httpx2.Client(http2=True) as client:
        response = client.get(url)
    assert response.status_code == 200
    assert response.extensions["http_version"] == b"HTTP/1.1"


def test_client_decode_text_using_autodetect() -> None:
    # Ensure that a 'default_encoding=autodetect' on the response allows for
    # encoding autodetection to be used when no "Content-Type: text/plain; charset=..."
    # info is present.
    #
    # Here we have some french text encoded with WINDOWS-1252, rather than UTF-8.
    # The curly quotes and em dash occupy bytes 0x80-0x9F, which are control
    # characters in ISO-8859-1, so the encoding is unambiguously WINDOWS-1252.
    text = (
        "Non-seulement Despréaux ne se trompait pas — mais de tous les écrivains "
        "que la France a produits, sans excepter Voltaire lui-même, imprégné de "
        "l’esprit anglais par son séjour à Londres, c’est incontestablement "
        "“Molière” ou Poquelin qui reproduit avec l’exactitude la plus vive et la "
        "plus complète le fond du génie français."
    )

    def cp1252_but_no_content_type(request: httpx2.Request) -> httpx2.Response:
        content = text.encode("WINDOWS-1252")
        return httpx2.Response(200, content=content)

    transport = httpx2.MockTransport(cp1252_but_no_content_type)
    with httpx2.Client(transport=transport, default_encoding=autodetect) as client:
        response = client.get("http://www.example.com")

        assert response.status_code == 200
        assert response.reason_phrase == "OK"
        assert response.encoding == "WINDOWS-1252"
        assert response.text == text


def test_client_decode_text_using_explicit_encoding() -> None:
    # Ensure that a 'default_encoding="..."' on the response is used for text decoding
    # when no "Content-Type: text/plain; charset=..."" info is present.
    #
    # Here we have some french text encoded with WINDOWS-1252, rather than UTF-8.
    # The curly quotes and em dash occupy bytes 0x80-0x9F, which are control
    # characters in ISO-8859-1, so the encoding is unambiguously WINDOWS-1252.
    text = (
        "Non-seulement Despréaux ne se trompait pas — mais de tous les écrivains "
        "que la France a produits, sans excepter Voltaire lui-même, imprégné de "
        "l’esprit anglais par son séjour à Londres, c’est incontestablement "
        "“Molière” ou Poquelin qui reproduit avec l’exactitude la plus vive et la "
        "plus complète le fond du génie français."
    )

    def cp1252_but_no_content_type(request: httpx2.Request) -> httpx2.Response:
        content = text.encode("WINDOWS-1252")
        return httpx2.Response(200, content=content)

    transport = httpx2.MockTransport(cp1252_but_no_content_type)
    with httpx2.Client(transport=transport, default_encoding=autodetect) as client:
        response = client.get("http://www.example.com")

        assert response.status_code == 200
        assert response.reason_phrase == "OK"
        assert response.encoding == "WINDOWS-1252"
        assert response.text == text
