import io
import logging
import ssl
import urllib.parse

import pytest
import pytest_httpbin.certs
import yarl

import vcr

asyncio = pytest.importorskip("asyncio")
aiohttp = pytest.importorskip("aiohttp")


from .aiohttp_utils import aiohttp_app, aiohttp_request  # noqa: E402

HTTPBIN_SSL_CONTEXT = ssl.create_default_context(cafile=pytest_httpbin.certs.where())


def run_in_loop(fn):
    async def wrapper():
        return await fn(asyncio.get_running_loop())

    return asyncio.run(wrapper())


def request(method, url, output="text", **kwargs):
    def run(loop):
        return aiohttp_request(loop, method, url, output=output, **kwargs)

    return run_in_loop(run)


def get(url, output="text", **kwargs):
    return request("GET", url, output=output, **kwargs)


def post(url, output="text", **kwargs):
    return request("POST", url, output="text", **kwargs)


@pytest.mark.online
def test_status(tmpdir, httpbin):
    url = httpbin.url

    with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
        response, _ = get(url)

    with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
        cassette_response, _ = get(url)
        assert cassette_response.status == response.status
        assert cassette.play_count == 1


@pytest.mark.online
@pytest.mark.parametrize("auth", [None, aiohttp.BasicAuth("vcrpy", "test")])
def test_headers(tmpdir, auth, httpbin):
    url = httpbin.url
    with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
        response, _ = get(url, auth=auth)

    with vcr.use_cassette(str(tmpdir.join("headers.yaml"))) as cassette:
        if auth is not None:
            request = cassette.requests[0]
            assert "AUTHORIZATION" in request.headers
        cassette_response, _ = get(url, auth=auth)
        assert cassette_response.headers.items() == response.headers.items()
        assert cassette.play_count == 1
        assert "istr" not in cassette.data[0]
        assert "yarl.URL" not in cassette.data[0]


@pytest.mark.online
def test_case_insensitive_headers(tmpdir, httpbin):
    url = httpbin.url

    with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
        _, _ = get(url)

    with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))) as cassette:
        cassette_response, _ = get(url)
        assert "Content-Type" in cassette_response.headers
        assert "content-type" in cassette_response.headers
        assert cassette.play_count == 1


@pytest.mark.online
def test_text(tmpdir, httpbin):
    url = httpbin.url

    with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
        _, response_text = get(url)

    with vcr.use_cassette(str(tmpdir.join("text.yaml"))) as cassette:
        _, cassette_response_text = get(url)
        assert cassette_response_text == response_text
        assert cassette.play_count == 1


@pytest.mark.online
def test_json(tmpdir, httpbin):
    url = httpbin.url + "/json"
    headers = {"Content-Type": "application/json"}

    with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
        _, response_json = get(url, output="json", headers=headers)

    with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
        _, cassette_response_json = get(url, output="json", headers=headers)
        assert cassette_response_json == response_json
        assert cassette.play_count == 1


@pytest.mark.online
def test_binary(tmpdir, httpbin):
    url = httpbin.url + "/image/png"
    with vcr.use_cassette(str(tmpdir.join("binary.yaml"))):
        _, response_binary = get(url, output="raw")

    with vcr.use_cassette(str(tmpdir.join("binary.yaml"))) as cassette:
        _, cassette_response_binary = get(url, output="raw")
        assert cassette_response_binary == response_binary
        assert cassette.play_count == 1


@pytest.mark.online
def test_stream(tmpdir, httpbin):
    url = httpbin.url

    with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
        _, body = get(url, output="raw")  # Do not use stream here, as the stream is exhausted by vcr

    with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
        _, cassette_body = get(url, output="stream")
        assert cassette_body == body
        assert cassette.play_count == 1


@pytest.mark.online
def test_stream_chunked(tmpdir, httpbin):
    # Exercises the async-iteration surface (``content.iter_chunked``) that
    # ``MockStream`` must keep providing across aiohttp versions.
    url = httpbin.url

    with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
        _, body = get(url, output="raw")  # Do not use stream here, as the stream is exhausted by vcr

    with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
        _, cassette_body = get(url, output="stream_chunked")
        assert cassette_body == body
        assert cassette.play_count == 1


POST_DATA = {"key1": "value1", "key2": "value2"}


@pytest.mark.online
@pytest.mark.parametrize(
    "kwargs",
    [
        {"data": POST_DATA},
        {"json": POST_DATA},
        {"data": POST_DATA, "json": None},
        {"data": None, "json": POST_DATA},
    ],
)
def test_post(tmpdir, kwargs, caplog, httpbin):
    caplog.set_level(logging.INFO)
    url = httpbin.url + "/post"
    with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
        _, response_json = post(url, **kwargs)

    with vcr.use_cassette(str(tmpdir.join("post.yaml"))) as cassette:
        request = cassette.requests[0]
        assert request.body == POST_DATA
        _, cassette_response_json = post(url, **kwargs)
        assert cassette_response_json == response_json
        assert cassette.play_count == 1

    assert next(
        (
            log
            for log in caplog.records
            if log.getMessage() == f"<Request (POST) {url}> not in cassette, sending to real server"
        ),
        None,
    ), "Log message not found."


@pytest.mark.online
def test_post_data_plus_json_error(tmpdir, httpbin):
    url = httpbin.url + "/post"
    with (
        vcr.use_cassette(str(tmpdir.join("post.yaml"))) as cassette,
        pytest.raises(ValueError, match="data and json parameters can not be used at the same time"),
    ):
        post(url, data=POST_DATA, json=POST_DATA)
    assert cassette.requests == []


@pytest.mark.online
def test_params(tmpdir, httpbin):
    url = httpbin.url + "/get?d=d"
    headers = {"Content-Type": "application/json"}
    params = {"a": 1, "b": 2, "c": "c"}

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        _, response_json = get(url, output="json", params=params, headers=headers)
        assert response_json["args"] == {"a": "1", "b": "2", "c": "c", "d": "d"}

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        _, cassette_response_json = get(url, output="json", params=params, headers=headers)
        assert cassette_response_json == response_json
        assert cassette.play_count == 1


@pytest.mark.online
def test_params_same_url_distinct_params(tmpdir, httpbin):
    url = httpbin.url + "/json"
    headers = {"Content-Type": "application/json"}
    params = {"a": 1, "b": 2, "c": "c"}

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        _, response_json = get(url, output="json", params=params, headers=headers)

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        _, cassette_response_json = get(url, output="json", params=params, headers=headers)
        assert cassette_response_json == response_json
        assert cassette.play_count == 1

    other_params = {"other": "params"}
    with (
        vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette,
        pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException),
    ):
        get(url, output="text", params=other_params)


@pytest.mark.online
def test_params_on_url(tmpdir, httpbin):
    url = httpbin.url + "/get?a=1&b=foo"
    headers = {"Content-Type": "application/json"}

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        _, response_json = get(url, output="json", headers=headers)
        request = cassette.requests[0]
        assert request.url == url

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        _, cassette_response_json = get(url, output="json", headers=headers)
        request = cassette.requests[0]
        assert request.url == url
        assert cassette_response_json == response_json
        assert cassette.play_count == 1


def test_aiohttp_test_client(aiohttp_client, tmpdir):
    loop = asyncio.get_event_loop()
    app = aiohttp_app()
    url = "/"
    client = loop.run_until_complete(aiohttp_client(app))

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))):
        response = loop.run_until_complete(client.get(url))

    assert response.status == 200
    response_text = loop.run_until_complete(response.text())
    assert response_text == "hello"
    response_text = loop.run_until_complete(response.text(errors="replace"))
    assert response_text == "hello"

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        response = loop.run_until_complete(client.get(url))

    request = cassette.requests[0]
    assert request.url == str(client.make_url(url))
    response_text = loop.run_until_complete(response.text())
    assert response_text == "hello"
    assert cassette.play_count == 1


def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
    loop = asyncio.get_event_loop()
    app = aiohttp_app()
    url = "/json/empty"
    client = loop.run_until_complete(aiohttp_client(app))

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))):
        response = loop.run_until_complete(client.get(url))

    assert response.status == 200
    response_json = loop.run_until_complete(response.json())
    assert response_json is None

    with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
        response = loop.run_until_complete(client.get(url))

    request = cassette.requests[0]
    assert request.url == str(client.make_url(url))
    response_json = loop.run_until_complete(response.json())
    assert response_json is None
    assert cassette.play_count == 1


@pytest.mark.online
def test_redirect(tmpdir, httpbin):
    url = httpbin.url + "/redirect/2"

    with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
        response, _ = get(url)

    with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))) as cassette:
        cassette_response, _ = get(url)

        assert cassette_response.status == response.status
        assert len(cassette_response.history) == len(response.history)
        assert len(cassette) == 3
        assert cassette.play_count == 3

    # Assert that the real response and the cassette response have a similar
    # looking request_info.
    assert cassette_response.request_info.url == response.request_info.url
    assert cassette_response.request_info.method == response.request_info.method
    assert cassette_response.request_info.headers.items() == response.request_info.headers.items()
    assert cassette_response.request_info.real_url == response.request_info.real_url


@pytest.mark.online
def test_not_modified(tmpdir, httpbin):
    """It doesn't try to redirect on 304"""
    url = httpbin.url + "/status/304"

    with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
        response, _ = get(url)

    with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))) as cassette:
        cassette_response, _ = get(url)

        assert cassette_response.status == 304
        assert response.status == 304
        assert len(cassette_response.history) == len(response.history)
        assert len(cassette) == 1
        assert cassette.play_count == 1


@pytest.mark.online
def test_double_requests(tmpdir, httpbin):
    """We should capture, record, and replay all requests and response chains,
    even if there are duplicate ones.

    We should replay in the order we saw them.
    """
    url = httpbin.url

    with vcr.use_cassette(str(tmpdir.join("text.yaml"))):
        _, response_text1 = get(url, output="text")
        _, response_text2 = get(url, output="text")

    with vcr.use_cassette(str(tmpdir.join("text.yaml"))) as cassette:
        resp, cassette_response_text = get(url, output="text")
        assert resp.status == 200
        assert cassette_response_text == response_text1

        # We made only one request, so we should only play 1 recording.
        assert cassette.play_count == 1

        # Now make the second test to url
        resp, cassette_response_text = get(url, output="text")

        assert resp.status == 200

        assert cassette_response_text == response_text2

        # Now that we made both requests, we should have played both.
        assert cassette.play_count == 2


def test_cookies(httpbin_both, tmpdir):
    async def run(loop):
        cookies_url = httpbin_both.url + (
            "/response-headers?"
            "set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
            "Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
        )
        home_url = httpbin_both.url + "/"
        tmp = str(tmpdir.join("cookies.yaml"))
        req_cookies = {"Cookie_3": "Val_3"}
        req_headers = {"Cookie": "Cookie_4=Val_4"}

        # ------------------------- Record -------------------------- #
        with vcr.use_cassette(tmp) as cassette:
            async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
                cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
                home_resp = await session.get(
                    home_url,
                    cookies=req_cookies,
                    headers=req_headers,
                    ssl=HTTPBIN_SSL_CONTEXT,
                )
                assert cassette.play_count == 0
        assert_responses(cookies_resp, home_resp)

        # -------------------------- Play --------------------------- #
        with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
            async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
                cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
                home_resp = await session.get(
                    home_url,
                    cookies=req_cookies,
                    headers=req_headers,
                    ssl=HTTPBIN_SSL_CONTEXT,
                )
                assert cassette.play_count == 2
        assert_responses(cookies_resp, home_resp)

    def assert_responses(cookies_resp, home_resp):
        assert cookies_resp.cookies.get("cookie_1").value == "val_1"
        assert cookies_resp.cookies.get("Cookie_2").value == "Val_2"
        request_cookies = home_resp.request_info.headers["cookie"]
        assert "cookie_1=val_1" in request_cookies
        assert "Cookie_2=Val_2" in request_cookies
        assert "Cookie_3=Val_3" in request_cookies
        assert "Cookie_4=Val_4" in request_cookies

    run_in_loop(run)


def test_cookies_redirect(httpbin_both, tmpdir):
    async def run(loop):
        # Sets cookie as provided by the query string and redirects
        cookies_url = httpbin_both.url + "/cookies/set?Cookie_1=Val_1"
        tmp = str(tmpdir.join("cookies.yaml"))

        # ------------------------- Record -------------------------- #
        with vcr.use_cassette(tmp) as cassette:
            async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
                cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
                assert not cookies_resp.cookies
                cookies = session.cookie_jar.filter_cookies(yarl.URL(cookies_url))
                assert cookies["Cookie_1"].value == "Val_1"
                assert cassette.play_count == 0

            assert cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"

        # -------------------------- Play --------------------------- #
        with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
            async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
                cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
                assert not cookies_resp.cookies
                cookies = session.cookie_jar.filter_cookies(yarl.URL(cookies_url))
                assert cookies["Cookie_1"].value == "Val_1"
                assert cassette.play_count == 2

            assert cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"

        # Assert that it's ignoring expiration date
        with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
            cassette.responses[0]["headers"]["set-cookie"] = [
                "Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
            ]
            async with aiohttp.ClientSession(loop=loop, cookie_jar=aiohttp.CookieJar(unsafe=True)) as session:
                cookies_resp = await session.get(cookies_url, ssl=HTTPBIN_SSL_CONTEXT)
                assert not cookies_resp.cookies
                cookies = session.cookie_jar.filter_cookies(yarl.URL(cookies_url))
                assert cookies["Cookie_1"].value == "Val_1"

    run_in_loop(run)


@pytest.mark.online
def test_not_allow_redirects(tmpdir, httpbin):
    url = httpbin + "/redirect-to?url=.%2F&status_code=308"
    path = str(tmpdir.join("redirects.yaml"))

    with vcr.use_cassette(path):
        response, _ = get(url, allow_redirects=False)
        assert response.url.path == "/redirect-to"
        assert response.status == 308

    with vcr.use_cassette(path) as cassette:
        response, _ = get(url, allow_redirects=False)
        assert response.url.path == "/redirect-to"
        assert response.status == 308
        assert cassette.play_count == 1


def test_filter_query_parameters(tmpdir, httpbin):
    url = httpbin + "?password=secret"
    path = str(tmpdir.join("query_param_filter.yaml"))

    with vcr.use_cassette(path, filter_query_parameters=["password"]) as cassette:
        get(url)

    assert "password" not in cassette.requests[0].url
    assert "secret" not in cassette.requests[0].url

    with open(path) as f:
        cassette_content = f.read()
        assert "password" not in cassette_content
        assert "secret" not in cassette_content


@pytest.mark.online
def test_use_cassette_with_io(tmpdir, caplog, httpbin):
    url = httpbin.url + "/post"

    # test without cassettes
    data = io.BytesIO(b"hello")
    _, response_json = request("POST", url, output="json", data=data)
    assert response_json["data"] == "hello"

    # test with cassettes
    data = io.BytesIO(b"hello")
    with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
        _, response_json = request("POST", url, output="json", data=data)
        assert response_json["data"] == "hello"
