import pickle
import pytest
import socket
import sys
import tests
import time
from unittest import mock
import urllib

import httplib2


def test_convert_byte_str():
    with tests.assert_raises(TypeError):
        httplib2._convert_byte_str(4)
    assert httplib2._convert_byte_str(b"Hello") == "Hello"
    assert httplib2._convert_byte_str("World") == "World"


def test_reflect():
    http = httplib2.Http()
    with tests.server_reflect() as uri:
        response, content = http.request(uri + "?query", "METHOD")
    assert response.status == 200
    host = urllib.parse.urlparse(uri).netloc
    assert content.startswith(
        """\
METHOD /?query HTTP/1.1\r\n\
Host: {host}\r\n""".format(
            host=host
        ).encode()
    ), content


def test_pickle_http():
    http = httplib2.Http(cache=tests.get_cache_path())
    new_http = pickle.loads(pickle.dumps(http))

    assert tuple(sorted(new_http.__dict__)) == tuple(sorted(http.__dict__))
    assert new_http.credentials.credentials == http.credentials.credentials
    assert new_http.certificates.credentials == http.certificates.credentials
    assert new_http.cache.cache == http.cache.cache
    for key in new_http.__dict__:
        if key not in ("cache", "certificates", "credentials"):
            assert getattr(new_http, key) == getattr(http, key)


def test_pickle_http_with_connection():
    http = httplib2.Http()
    http.request("http://random-domain:81/", connection_type=tests.MockHTTPConnection)
    new_http = pickle.loads(pickle.dumps(http))
    assert tuple(http.connections) == ("http:random-domain:81",)
    assert new_http.connections == {}


def test_pickle_custom_request_http():
    http = httplib2.Http()
    http.request = lambda: None
    http.request.dummy_attr = "dummy_value"
    new_http = pickle.loads(pickle.dumps(http))
    assert getattr(new_http.request, "dummy_attr", None) is None


@pytest.mark.xfail(
    sys.version_info >= (3,),
    reason=(
        "FIXME: for unknown reason global timeout test fails in Python3 "
        "with response 200"
    ),
)
def test_timeout_global():
    def handler(request):
        time.sleep(0.5)
        return tests.http_response_bytes()

    try:
        socket.setdefaulttimeout(0.1)
    except Exception:
        pytest.skip("cannot set global socket timeout")
    try:
        http = httplib2.Http()
        http.force_exception_to_status_code = True
        with tests.server_request(handler) as uri:
            response, content = http.request(uri)
            assert response.status == 408
            assert response.reason.startswith("Request Timeout")
    finally:
        socket.setdefaulttimeout(None)


def test_timeout_individual():
    def handler(request):
        time.sleep(0.5)
        return tests.http_response_bytes()

    http = httplib2.Http(timeout=0.1)
    http.force_exception_to_status_code = True

    with tests.server_request(handler) as uri:
        response, content = http.request(uri)
        assert response.status == 408
        assert response.reason.startswith("Request Timeout")


def test_timeout_subsequent():
    class Handler(object):
        number = 0

        @classmethod
        def handle(cls, request):
            # request.number is always 1 because of
            # the new socket connection each time
            cls.number += 1
            if cls.number % 2 != 0:
                time.sleep(0.6)
                return tests.http_response_bytes(status=500)
            return tests.http_response_bytes(status=200)

    http = httplib2.Http(timeout=0.5)
    http.force_exception_to_status_code = True

    with tests.server_request(Handler.handle, request_count=2) as uri:
        response, _ = http.request(uri)
        assert response.status == 408
        assert response.reason.startswith("Request Timeout")

        response, _ = http.request(uri)
        assert response.status == 200


def test_timeout_https():
    c = httplib2.HTTPSConnectionWithTimeout("localhost", 80, timeout=47)
    assert 47 == c.timeout


# @pytest.mark.xfail(
#     sys.version_info >= (3,),
#     reason='[py3] last request should open new connection, but client does not realize socket was closed by server',
# )
def test_connection_close():
    http = httplib2.Http()
    g = []

    def handler(request):
        g.append(request.number)
        return tests.http_response_bytes(proto="HTTP/1.1")

    with tests.server_request(handler, request_count=3) as uri:
        http.request(uri, "GET")  # conn1 req1
        for c in http.connections.values():
            assert c.sock is not None
        http.request(uri, "GET", headers={"connection": "close"})
        time.sleep(0.7)
        http.request(uri, "GET")  # conn2 req1
    assert g == [1, 2, 1]


def test_get_end2end_headers():
    # one end to end header
    response = {"content-type": "application/atom+xml", "te": "deflate"}
    end2end = httplib2._get_end2end_headers(response)
    assert "content-type" in end2end
    assert "te" not in end2end
    assert "connection" not in end2end

    # one end to end header that gets eliminated
    response = {
        "connection": "content-type",
        "content-type": "application/atom+xml",
        "te": "deflate",
    }
    end2end = httplib2._get_end2end_headers(response)
    assert "content-type" not in end2end
    assert "te" not in end2end
    assert "connection" not in end2end

    # Degenerate case of no headers
    response = {}
    end2end = httplib2._get_end2end_headers(response)
    assert len(end2end) == 0

    # Degenerate case of connection referring to a header not passed in
    response = {"connection": "content-type"}
    end2end = httplib2._get_end2end_headers(response)
    assert len(end2end) == 0


@pytest.mark.parametrize("scheme", ("http", "https"))
def test_ipv6(scheme):
    # Even if IPv6 isn't installed on a machine it should just raise socket.error
    uri = "{scheme}://[::1]:1/".format(scheme=scheme)
    try:
        httplib2.Http(timeout=0.1).request(uri)
    except socket.gaierror:
        assert False, "should get the address family right for IPv6"
    except socket.error:
        pass


@pytest.mark.parametrize(
    "conn_type",
    (httplib2.HTTPConnectionWithTimeout, httplib2.HTTPSConnectionWithTimeout),
)
def test_connection_proxy_info_attribute_error(conn_type):
    # HTTPConnectionWithTimeout did not initialize its .proxy_info attribute
    # https://github.com/httplib2/httplib2/pull/97
    # Thanks to Joseph Ryan https://github.com/germanjoey
    conn = conn_type("no-such-hostname.", 80)
    # TODO: replace mock with dummy local server
    with tests.assert_raises(socket.gaierror):
        with mock.patch("socket.socket.connect", side_effect=socket.gaierror):
            conn.request("GET", "/")


def test_http_443_forced_https():
    http = httplib2.Http()
    http.force_exception_to_status_code = True
    uri = "http://localhost:443/"
    # sorry, using internal structure of Http to check chosen scheme
    with mock.patch("httplib2.Http._request") as m:
        http.request(uri)
        assert len(m.call_args) > 0, "expected Http._request() call"
        conn = m.call_args[0][0]
        assert isinstance(conn, httplib2.HTTPConnectionWithTimeout)


def test_close():
    http = httplib2.Http()
    assert len(http.connections) == 0
    with tests.server_const_http() as uri:
        http.request(uri)
        assert len(http.connections) == 1
        http.close()
        assert len(http.connections) == 0


def test_connect_exception_type():
    # This autoformatting PR actually changed the behavior of error handling:
    # https://github.com/httplib2/httplib2/pull/105/files#diff-c6669c781a2dee1b2d2671cab4e21c66L985
    # potentially changing the type of the error raised by connect()
    # https://github.com/httplib2/httplib2/pull/150
    http = httplib2.Http()
    with mock.patch("httplib2.socket.socket.connect", side_effect=socket.timeout("foo")):
        with tests.assert_raises(socket.timeout):
            http.request(tests.DUMMY_URL)
