import itertools
import subprocess

import django
from django.core.signals import request_started
from django.core.wsgi import get_wsgi_application
from django.db import close_old_connections
from django.test import modify_settings
from django.test import override_settings
from django.test.client import RequestFactory
from django.utils.functional import SimpleLazyObject
from django.views.generic import TemplateView
import mock
import pytest
from six import ensure_text

from ddtrace import config
from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
from ddtrace.constants import SAMPLING_PRIORITY_KEY
from ddtrace.contrib.django.patch import instrument_view
from ddtrace.contrib.django.utils import get_request_uri
from ddtrace.ext import errors
from ddtrace.ext import http
from ddtrace.ext.priority import USER_KEEP
from ddtrace.internal.compat import PY2
from ddtrace.internal.compat import binary_type
from ddtrace.internal.compat import string_type
from ddtrace.propagation.http import HTTP_HEADER_PARENT_ID
from ddtrace.propagation.http import HTTP_HEADER_SAMPLING_PRIORITY
from ddtrace.propagation.http import HTTP_HEADER_TRACE_ID
from ddtrace.propagation.utils import get_wsgi_header
from ddtrace.vendor import wrapt
from tests.opentracer.utils import init_tracer
from tests.utils import assert_dict_issuperset
from tests.utils import override_config
from tests.utils import override_env
from tests.utils import override_global_config
from tests.utils import override_http_config


@pytest.mark.skipif(django.VERSION < (2, 0, 0), reason="")
def test_django_v2XX_request_root_span(client, test_spans):
    """
    When making a request to a Django app
        We properly create the `django.request` root span
    """
    resp = client.get("/")
    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    spans = test_spans.get_spans()
    # Assert the correct number of traces and spans
    assert len(spans) == 26

    # Assert the structure of the root `django.request` span
    root = test_spans.get_root_span()

    if django.VERSION >= (2, 2, 0):
        resource = "GET ^$"
    else:
        resource = "GET tests.contrib.django.views.index"

    meta = {
        "django.request.class": "django.core.handlers.wsgi.WSGIRequest",
        "django.response.class": "django.http.response.HttpResponse",
        "django.user.is_authenticated": "False",
        "django.view": "tests.contrib.django.views.index",
        "http.method": "GET",
        "http.status_code": "200",
        "http.url": "http://testserver/",
    }
    if django.VERSION >= (2, 2, 0):
        meta["http.route"] = "^$"

    assert http.QUERY_STRING not in root.meta
    root.assert_matches(
        name="django.request",
        service="django",
        resource=resource,
        parent_id=None,
        span_type="web",
        error=0,
        meta=meta,
    )


@pytest.mark.skipif(django.VERSION < (2, 0, 0), reason="")
def test_django_v2XX_alter_root_resource(client, test_spans):
    """
    When making a request to a Django app
        We properly create the `django.request` root span
    """
    resp = client.get("/alter-resource/")
    assert resp.status_code == 200
    assert resp.content == b""

    spans = test_spans.get_spans()
    # Assert the correct number of traces and spans
    assert len(spans) == 26

    # Assert the structure of the root `django.request` span
    root = test_spans.get_root_span()

    meta = {
        "django.request.class": "django.core.handlers.wsgi.WSGIRequest",
        "django.response.class": "django.http.response.HttpResponse",
        "django.user.is_authenticated": "False",
        "django.view": "tests.contrib.django.views.alter_resource",
        "http.method": "GET",
        "http.status_code": "200",
        "http.url": "http://testserver/alter-resource/",
    }
    if django.VERSION >= (2, 2, 0):
        meta["http.route"] = "^alter-resource/$"

    assert http.QUERY_STRING not in root.meta
    root.assert_matches(
        name="django.request",
        service="django",
        resource="custom django.request resource",
        parent_id=None,
        span_type="web",
        error=0,
        meta=meta,
    )


@pytest.mark.skipif(django.VERSION >= (2, 0, 0), reason="")
def test_v1XX_middleware(client, test_spans):
    resp = client.get("/")
    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    # Assert the correct number of traces and spans
    if django.VERSION < (1, 11, 0):
        test_spans.assert_span_count(15)
    else:
        test_spans.assert_span_count(16)

    # Get all the `django.middleware` spans in this trace
    middleware_spans = list(test_spans.filter_spans(name="django.middleware"))
    if django.VERSION < (1, 11, 0):
        assert len(middleware_spans) == 13
    else:
        assert len(middleware_spans) == 14

    root = test_spans.get_root_span()
    root.assert_matches(name="django.request")

    # Assert common span structure
    for span in middleware_spans:
        span.assert_matches(
            name="django.middleware",
            service="django",
            error=0,
            span_type=None,
            parent_id=root.span_id,  # They are all children of the root django.request
        )

    # DEV: Order matters here, we want all `process_request` before `process_view`, before `process_response`
    expected_resources = [
        "django.contrib.sessions.middleware.SessionMiddleware.process_request",
        "django.middleware.common.CommonMiddleware.process_request",
        "django.middleware.csrf.CsrfViewMiddleware.process_request",  # Not in < 1.11.0
        "django.contrib.auth.middleware.AuthenticationMiddleware.process_request",
        "django.contrib.auth.middleware.SessionAuthenticationMiddleware.process_request",
        "django.contrib.messages.middleware.MessageMiddleware.process_request",
        "django.middleware.security.SecurityMiddleware.process_request",
        "django.middleware.csrf.CsrfViewMiddleware.process_view",
        "django.middleware.security.SecurityMiddleware.process_response",
        "django.middleware.clickjacking.XFrameOptionsMiddleware.process_response",
        "django.contrib.messages.middleware.MessageMiddleware.process_response",
        "django.middleware.csrf.CsrfViewMiddleware.process_response",
        "django.middleware.common.CommonMiddleware.process_response",
        "django.contrib.sessions.middleware.SessionMiddleware.process_response",
    ]
    if django.VERSION < (1, 11, 0):
        expected_resources.remove("django.middleware.csrf.CsrfViewMiddleware.process_request")
    middleware_spans = sorted(middleware_spans, key=lambda s: s.start)
    span_resources = [s.resource for s in middleware_spans]
    assert span_resources == expected_resources


def test_disallowed_host(client, test_spans):
    with override_settings(ALLOWED_HOSTS="not-testserver"):
        resp = client.get("/")
        assert resp.status_code == 400
        assert b"Bad Request (400)" in resp.content

    root_span = test_spans.get_root_span()
    assert root_span.get_tag("http.status_code") == "400"
    assert root_span.get_tag("http.url") == "http://testserver/"


def test_http_header_tracing_disabled(client, test_spans):
    headers = {
        get_wsgi_header("my-header"): "my_value",
    }
    resp = client.get("/", **headers)

    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    root = test_spans.get_root_span()

    assert root.get_tag("http.request.headers.my-header") is None
    assert root.get_tag("http.response.headers.my-response-header") is None


def test_http_header_tracing_enabled(client, test_spans):
    with override_config("django", {}):
        config.django.http.trace_headers(["my-header", "my-response-header"])

        headers = {
            get_wsgi_header("my-header"): "my_value",
        }
        resp = client.get("/", **headers)

    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    root = test_spans.get_root_span()

    assert root.get_tag("http.request.headers.my-header") == "my_value"
    assert root.get_tag("http.response.headers.my-response-header") == "my_response_value"


"""
Middleware tests
"""


@pytest.mark.skipif(django.VERSION < (2, 0, 0), reason="")
def test_v2XX_middleware(client, test_spans):
    """
    When making a request to a Django app
        We properly create the `django.middleware` spans
    """
    resp = client.get("/")
    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    # Assert the correct number of traces and spans
    test_spans.assert_span_count(26)

    # Get all the `django.middleware` spans in this trace
    middleware_spans = list(test_spans.filter_spans(name="django.middleware"))
    assert len(middleware_spans) == 24

    # Assert common span structure
    for span in middleware_spans:
        span.assert_matches(
            name="django.middleware",
            service="django",
            error=0,
            span_type=None,
        )

    span_resources = {
        "django.contrib.auth.middleware.AuthenticationMiddleware.__call__",
        "django.contrib.auth.middleware.AuthenticationMiddleware.process_request",
        "django.contrib.messages.middleware.MessageMiddleware.__call__",
        "django.contrib.messages.middleware.MessageMiddleware.process_request",
        "django.contrib.messages.middleware.MessageMiddleware.process_response",
        "django.contrib.sessions.middleware.SessionMiddleware.__call__",
        "django.contrib.sessions.middleware.SessionMiddleware.process_request",
        "django.contrib.sessions.middleware.SessionMiddleware.process_response",
        "django.middleware.clickjacking.XFrameOptionsMiddleware.__call__",
        "django.middleware.clickjacking.XFrameOptionsMiddleware.process_response",
        "django.middleware.common.CommonMiddleware.__call__",
        "django.middleware.common.CommonMiddleware.process_request",
        "django.middleware.common.CommonMiddleware.process_response",
        "django.middleware.csrf.CsrfViewMiddleware.__call__",
        "django.middleware.csrf.CsrfViewMiddleware.process_request",
        "django.middleware.csrf.CsrfViewMiddleware.process_response",
        "django.middleware.csrf.CsrfViewMiddleware.process_view",
        "django.middleware.security.SecurityMiddleware.__call__",
        "django.middleware.security.SecurityMiddleware.process_request",
        "django.middleware.security.SecurityMiddleware.process_response",
        "tests.contrib.django.middleware.ClsMiddleware.__call__",
        "tests.contrib.django.middleware.EverythingMiddleware",
        "tests.contrib.django.middleware.EverythingMiddleware.__call__",
        "tests.contrib.django.middleware.EverythingMiddleware.process_view",
    }
    assert set([s.resource for s in middleware_spans]) == span_resources

    # Get middleware spans in reverse order of start time
    middleware_spans = sorted(middleware_spans, key=lambda s: s.start, reverse=True)

    # Assert the first middleware span's parent is the root span (django.request)
    root_span = test_spans.get_root_span()
    assert root_span.name == "django.request"
    first_middleware = middleware_spans[-1]
    assert first_middleware.parent_id == root_span.span_id


def test_django_request_not_found(client, test_spans):
    """
    When making a request to a Django app
        When the endpoint doesn't exist
            We create a 404 span
    """
    resp = client.get("/unknown/endpoint")
    assert resp.status_code == 404

    if django.VERSION >= (3, 0, 0):
        content = (
            b'\n<!doctype html>\n<html lang="en">\n<head>\n  <title>Not Found</title>\n'
            b"</head>\n<body>\n  <h1>Not Found</h1><p>The requested resource was not found "
            b"on this server.</p>\n</body>\n</html>\n"
        )
    elif django.VERSION >= (1, 11, 0):
        content = b"<h1>Not Found</h1><p>The requested resource was not found on this server.</p>"
    else:
        content = b"<h1>Not Found</h1><p>The requested URL /unknown/endpoint was not found on this server.</p>"
    assert resp.content == content

    # Assert the correct number of traces and spans
    if django.VERSION >= (2, 0, 0):
        span_count = 27
    elif django.VERSION >= (1, 11, 0):
        span_count = 18
    else:
        span_count = 16
    test_spans.assert_span_count(span_count)

    # Assert the structure of the root `django.request` span
    root = test_spans.get_root_span()
    root.assert_matches(
        name="django.request",
        service="django",
        resource="GET 404",
        parent_id=None,
        span_type="web",
        error=0,
        meta={
            "django.request.class": "django.core.handlers.wsgi.WSGIRequest",
            "django.response.class": "django.http.response.HttpResponseNotFound",
            "http.method": "GET",
            "http.status_code": "404",
            "http.url": "http://testserver/unknown/endpoint",
        },
    )

    # Assert template render
    render_spans = list(test_spans.filter_spans(name="django.template.render"))
    assert len(render_spans) == 1

    render_span = render_spans[0]
    render_span.assert_matches(
        name="django.template.render",
        resource="django.template.base.Template.render",
        meta={
            "django.template.engine.class": "django.template.engine.Engine",
        },
    )


def test_middleware_trace_error_500(client, test_spans):
    # ensures exceptions generated by views are traced
    with modify_settings(
        **(
            dict(MIDDLEWARE={"append": "tests.contrib.django.middleware.CatchExceptionMiddleware"})
            if django.VERSION >= (2, 0, 0)
            else dict(MIDDLEWARE_CLASSES={"append": "tests.contrib.django.middleware.CatchExceptionMiddleware"})
        )
    ):
        assert client.get("/error-500/").status_code == 500

    error_spans = list(test_spans.filter_spans(error=1))
    # There should be 3 spans flagged as errors
    # 1. The view span which wraps the original error
    # 2. The root span which should just be flagged as an error (no exception info)
    # 3. The middleware span that catches the exception and converts it to a 500
    assert len(error_spans) == 3

    # Test the root span
    span = test_spans.get_root_span()
    assert span.error == 1
    assert span.get_tag("http.status_code") == "500"
    assert span.get_tag(http.URL) == "http://testserver/error-500/"
    if django.VERSION >= (2, 2, 0):
        assert span.resource == "GET ^error-500/$"
    else:
        assert span.resource == "GET tests.contrib.django.views.error_500"
    assert span.get_tag(errors.ERROR_MSG) is None
    assert span.get_tag(errors.ERROR_TYPE) is None
    assert span.get_tag(errors.ERROR_STACK) is None

    # Test the view span (where the exception is generated)
    view_span = list(test_spans.filter_spans(name="django.view"))
    assert len(view_span) == 1
    view_span = view_span[0]
    assert view_span.error == 1
    # Make sure the message is somewhere in the stack trace
    assert "Error 500" in view_span.get_tag(errors.ERROR_STACK)

    # Test the catch exception middleware
    res = "tests.contrib.django.middleware.CatchExceptionMiddleware.process_exception"
    mw_span = list(test_spans.filter_spans(resource=res))[0]
    assert mw_span.error == 1
    # Make sure the message is somewhere in the stack trace
    assert "Error 500" in view_span.get_tag(errors.ERROR_STACK)
    assert mw_span.get_tag(errors.ERROR_MSG) is not None
    assert mw_span.get_tag(errors.ERROR_TYPE) is not None
    assert mw_span.get_tag(errors.ERROR_STACK) is not None


def test_middleware_handled_view_exception_success(client, test_spans):
    """
    When an exception is raised in a view and then handled
        Only the culprit span contains error properties
    """
    with modify_settings(
        **(
            dict(MIDDLEWARE={"append": "tests.contrib.django.middleware.HandleErrorMiddlewareSuccess"})
            if django.VERSION >= (2, 0, 0)
            else dict(MIDDLEWARE_CLASSES={"append": "tests.contrib.django.middleware.HandleErrorMiddlewareSuccess"})
        )
    ):
        assert client.get("/error-500/").status_code == 200

    error_spans = list(test_spans.filter_spans(error=1))
    # There should be 1 span flagged as erroneous:
    # - The view span which wraps the original error
    assert len(error_spans) == 1

    # Test the root span
    root_span = test_spans.get_root_span()
    assert root_span.error == 0
    assert root_span.get_tag(errors.ERROR_STACK) is None
    assert root_span.get_tag(errors.ERROR_MSG) is None
    assert root_span.get_tag(errors.ERROR_TYPE) is None

    # Test the view span (where the exception is generated)
    view_span = list(test_spans.filter_spans(name="django.view"))
    assert len(view_span) == 1
    view_span = view_span[0]
    assert view_span.error == 1
    # Make sure the message is somewhere in the stack trace
    assert "Error 500" in view_span.get_tag("error.stack")


@pytest.mark.skipif(django.VERSION < (1, 10, 0), reason="Middleware functions only implemented since 1.10.0")
def test_empty_middleware_func_is_raised_in_django(client, test_spans):
    # Ensures potential empty middleware function (that returns None) is caught in django's end and not in our tracer
    with override_settings(MIDDLEWARE=["tests.contrib.django.middleware.empty_middleware"]):
        with pytest.raises(django.core.exceptions.ImproperlyConfigured):
            client.get("/")


"""
View tests
"""


def test_request_view(client, test_spans):
    """
    When making a request to a Django app
        A `django.view` span is produced
    """
    resp = client.get("/")
    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    view_spans = list(test_spans.filter_spans(name="django.view"))
    assert len(view_spans) == 1

    # Assert span properties
    view_span = view_spans[0]
    view_span.assert_matches(
        name="django.view",
        service="django",
        resource="tests.contrib.django.views.index",
        error=0,
    )


def test_lambda_based_view(client, test_spans):
    # ensures that the internals are properly traced when using a function view
    assert client.get("/lambda-view/").status_code == 200

    span = test_spans.get_root_span()
    assert span.get_tag("http.status_code") == "200"
    assert span.get_tag(http.URL) == "http://testserver/lambda-view/"
    if django.VERSION >= (2, 2, 0):
        assert span.resource == "GET ^lambda-view/$"
    else:
        assert span.resource == "GET tests.contrib.django.views.<lambda>"


def test_template_view(client, test_spans):
    resp = client.get("/template-view/")
    assert resp.status_code == 200
    assert resp.content == b"some content\n"

    view_spans = list(test_spans.filter_spans(name="django.view"))
    assert len(view_spans) == 1

    # Assert span properties
    view_span = view_spans[0]
    view_span.assert_matches(
        name="django.view",
        service="django",
        resource="tests.contrib.django.views.template_view",
        error=0,
    )

    root_span = test_spans.get_root_span()
    assert root_span.get_tag("django.response.template") == "basic.html"


def test_template_simple_view(client, test_spans):
    resp = client.get("/template-simple-view/")
    assert resp.status_code == 200
    assert resp.content == b"some content\n"

    view_spans = list(test_spans.filter_spans(name="django.view"))
    assert len(view_spans) == 1

    # Assert span properties
    view_span = view_spans[0]
    view_span.assert_matches(
        name="django.view",
        service="django",
        resource="tests.contrib.django.views.template_simple_view",
        error=0,
    )

    root_span = test_spans.get_root_span()
    assert root_span.get_tag("django.response.template") == "basic.html"


def test_template_list_view(client, test_spans):
    resp = client.get("/template-list-view/")
    assert resp.status_code == 200
    assert resp.content == b"some content\n"

    view_spans = list(test_spans.filter_spans(name="django.view"))
    assert len(view_spans) == 1

    # Assert span properties
    view_span = view_spans[0]
    view_span.assert_matches(
        name="django.view",
        service="django",
        resource="tests.contrib.django.views.template_list_view",
        error=0,
    )

    root_span = test_spans.get_root_span()
    assert root_span.get_tag("django.response.template.0") == "doesntexist.html"
    assert root_span.get_tag("django.response.template.1") == "basic.html"


def test_middleware_trace_function_based_view(client, test_spans):
    # ensures that the internals are properly traced when using a function views
    assert client.get("/fn-view/").status_code == 200

    span = test_spans.get_root_span()
    assert span.get_tag("http.status_code") == "200"
    assert span.get_tag(http.URL) == "http://testserver/fn-view/"
    if django.VERSION >= (2, 2, 0):
        assert span.resource == "GET ^fn-view/$"
    else:
        assert span.resource == "GET tests.contrib.django.views.function_view"


def test_middleware_trace_errors(client, test_spans):
    # ensures that the internals are properly traced
    assert client.get("/fail-view/").status_code == 403

    span = test_spans.get_root_span()
    assert span.get_tag("http.status_code") == "403"
    assert span.get_tag(http.URL) == "http://testserver/fail-view/"
    if django.VERSION >= (2, 2, 0):
        assert span.resource == "GET ^fail-view/$"
    else:
        assert span.resource == "GET tests.contrib.django.views.ForbiddenView"


def test_middleware_trace_staticmethod(client, test_spans):
    # ensures that the internals are properly traced
    assert client.get("/static-method-view/").status_code == 200

    span = test_spans.get_root_span()
    assert span.get_tag("http.status_code") == "200"
    assert span.get_tag(http.URL) == "http://testserver/static-method-view/"
    if django.VERSION >= (2, 2, 0):
        assert span.resource == "GET ^static-method-view/$"
    else:
        assert span.resource == "GET tests.contrib.django.views.StaticMethodView"


def test_simple_view_get(client, test_spans):
    # The `get` method of a view should be traced
    assert client.get("/simple/").status_code == 200
    assert len(list(test_spans.filter_spans(name="django.view"))) == 1
    assert len(list(test_spans.filter_spans(name="django.view.dispatch"))) == 1
    spans = list(test_spans.filter_spans(name="django.view.get"))
    assert len(spans) == 1
    span = spans[0]
    span.assert_matches(
        resource="tests.contrib.django.views.BasicView.get",
        error=0,
    )


def test_simple_view_post(client, test_spans):
    # The `post` method of a view should be traced
    assert client.post("/simple/").status_code == 200
    assert len(list(test_spans.filter_spans(name="django.view"))) == 1
    assert len(list(test_spans.filter_spans(name="django.view.dispatch"))) == 1
    assert len(list(test_spans.filter_spans(name="django.view.post"))) == 1
    spans = list(test_spans.filter_spans(name="django.view.post"))
    assert len(spans) == 1
    span = spans[0]
    span.assert_matches(
        resource="tests.contrib.django.views.BasicView.post",
        error=0,
    )


def test_simple_view_delete(client, test_spans):
    # The `delete` method of a view should be traced
    assert client.delete("/simple/").status_code == 200
    assert len(list(test_spans.filter_spans(name="django.view"))) == 1
    assert len(list(test_spans.filter_spans(name="django.view.dispatch"))) == 1
    spans = list(test_spans.filter_spans(name="django.view.delete"))
    assert len(spans) == 1
    span = spans[0]
    span.assert_matches(
        resource="tests.contrib.django.views.BasicView.delete",
        error=0,
    )


def test_simple_view_options(client, test_spans):
    # The `options` method of a view should be traced
    assert client.options("/simple/").status_code == 200
    assert len(list(test_spans.filter_spans(name="django.view"))) == 1
    assert len(list(test_spans.filter_spans(name="django.view.dispatch"))) == 1
    assert len(list(test_spans.filter_spans(name="django.view.options"))) == 1
    spans = list(test_spans.filter_spans(name="django.view.options"))
    assert len(spans) == 1
    span = spans[0]
    span.assert_matches(
        resource="django.views.generic.base.View.options",
        error=0,
    )


def test_simple_view_head(client, test_spans):
    # The `head` method of a view should be traced
    assert client.head("/simple/").status_code == 200
    assert len(list(test_spans.filter_spans(name="django.view"))) == 1
    assert len(list(test_spans.filter_spans(name="django.view.dispatch"))) == 1
    spans = list(test_spans.filter_spans(name="django.view.head"))
    assert len(spans) == 1
    span = spans[0]
    span.assert_matches(
        resource="tests.contrib.django.views.BasicView.head",
        error=0,
    )


"""
Database tests
"""


@pytest.mark.django_db
def test_connection(client, test_spans):
    """
    When database queries are made from Django
        The queries are traced
    """
    from django.contrib.auth.models import User

    users = User.objects.count()
    assert users == 0

    test_spans.assert_span_count(1)
    spans = test_spans.get_spans()

    span = spans[0]
    assert span.name == "sqlite.query"
    assert span.service == "defaultdb"
    assert span.span_type == "sql"
    assert span.get_tag("django.db.vendor") == "sqlite"
    assert span.get_tag("django.db.alias") == "default"


"""
Caching tests
"""


def test_cache_get(test_spans):
    # get the default cache
    cache = django.core.cache.caches["default"]

    cache.get("missing_key")

    spans = test_spans.get_spans()
    assert len(spans) == 1

    span = spans[0]
    assert span.service == "django"
    assert span.resource == "django.core.cache.backends.locmem.get"
    assert span.name == "django.cache"
    assert span.span_type == "cache"
    assert span.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "missing_key",
    }

    assert_dict_issuperset(span.meta, expected_meta)


def test_cache_set(test_spans):
    # get the default cache
    cache = django.core.cache.caches["default"]

    # (trace) the cache miss
    cache.set("a_new_key", 50)

    spans = test_spans.get_spans()
    assert len(spans) == 1

    span = spans[0]
    assert span.service == "django"
    assert span.resource == "django.core.cache.backends.locmem.set"
    assert span.name == "django.cache"
    assert span.span_type == "cache"
    assert span.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "a_new_key",
    }

    assert_dict_issuperset(span.meta, expected_meta)


def test_cache_delete(test_spans):
    # get the default cache
    cache = django.core.cache.caches["default"]

    cache.delete("an_existing_key")

    spans = test_spans.get_spans()
    assert len(spans) == 1

    span = spans[0]
    assert span.service == "django"
    assert span.resource == "django.core.cache.backends.locmem.delete"
    assert span.name == "django.cache"
    assert span.span_type == "cache"
    assert span.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "an_existing_key",
    }

    assert_dict_issuperset(span.meta, expected_meta)


@pytest.mark.skipif(django.VERSION >= (2, 1, 0), reason="")
def test_cache_incr_1XX(test_spans):
    # get the default cache, set the value and reset the spans
    cache = django.core.cache.caches["default"]
    cache.set("value", 0)
    test_spans.reset()

    cache.incr("value")

    spans = test_spans.get_spans()
    assert len(spans) == 2

    span_incr = spans[0]
    span_get = spans[1]

    # LocMemCache doesn't provide an atomic operation
    assert span_get.service == "django"
    assert span_get.resource == "django.core.cache.backends.locmem.get"
    assert span_get.name == "django.cache"
    assert span_get.span_type == "cache"
    assert span_get.error == 0
    assert span_incr.service == "django"
    assert span_incr.resource == "django.core.cache.backends.locmem.incr"
    assert span_incr.name == "django.cache"
    assert span_incr.span_type == "cache"
    assert span_incr.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "value",
    }

    assert_dict_issuperset(span_get.meta, expected_meta)
    assert_dict_issuperset(span_incr.meta, expected_meta)


@pytest.mark.skipif(django.VERSION < (2, 1, 0), reason="")
def test_cache_incr_2XX(test_spans):
    # get the default cache, set the value and reset the spans
    cache = django.core.cache.caches["default"]
    cache.set("value", 0)
    test_spans.reset()

    cache.incr("value")

    spans = test_spans.get_spans()
    assert len(spans) == 1

    span_incr = spans[0]

    # LocMemCache doesn't provide an atomic operation
    assert span_incr.service == "django"
    assert span_incr.resource == "django.core.cache.backends.locmem.incr"
    assert span_incr.name == "django.cache"
    assert span_incr.span_type == "cache"
    assert span_incr.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "value",
    }

    assert_dict_issuperset(span_incr.meta, expected_meta)


@pytest.mark.skipif(django.VERSION >= (2, 1, 0), reason="")
def test_cache_decr_1XX(test_spans):
    # get the default cache, set the value and reset the spans
    cache = django.core.cache.caches["default"]
    cache.set("value", 0)
    test_spans.reset()

    cache.decr("value")

    spans = test_spans.get_spans()
    assert len(spans) == 3

    span_decr = spans[0]
    span_incr = spans[1]
    span_get = spans[2]

    # LocMemCache doesn't provide an atomic operation
    assert span_get.service == "django"
    assert span_get.resource == "django.core.cache.backends.locmem.get"
    assert span_get.name == "django.cache"
    assert span_get.span_type == "cache"
    assert span_get.error == 0
    assert span_incr.service == "django"
    assert span_incr.resource == "django.core.cache.backends.locmem.incr"
    assert span_incr.name == "django.cache"
    assert span_incr.span_type == "cache"
    assert span_incr.error == 0
    assert span_decr.service == "django"
    assert span_decr.resource == "django.core.cache.backends.base.decr"
    assert span_decr.name == "django.cache"
    assert span_decr.span_type == "cache"
    assert span_decr.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "value",
    }

    assert_dict_issuperset(span_get.meta, expected_meta)
    assert_dict_issuperset(span_incr.meta, expected_meta)
    assert_dict_issuperset(span_decr.meta, expected_meta)


@pytest.mark.skipif(django.VERSION < (2, 1, 0), reason="")
def test_cache_decr_2XX(test_spans):
    # get the default cache, set the value and reset the spans
    cache = django.core.cache.caches["default"]
    cache.set("value", 0)
    test_spans.reset()

    cache.decr("value")

    spans = test_spans.get_spans()
    assert len(spans) == 2

    span_decr = spans[0]
    span_incr = spans[1]

    # LocMemCache doesn't provide an atomic operation
    assert span_incr.service == "django"
    assert span_incr.resource == "django.core.cache.backends.locmem.incr"
    assert span_incr.name == "django.cache"
    assert span_incr.span_type == "cache"
    assert span_incr.error == 0
    assert span_decr.service == "django"
    assert span_decr.resource == "django.core.cache.backends.base.decr"
    assert span_decr.name == "django.cache"
    assert span_decr.span_type == "cache"
    assert span_decr.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "value",
    }

    assert_dict_issuperset(span_incr.meta, expected_meta)
    assert_dict_issuperset(span_decr.meta, expected_meta)


def test_cache_get_many(test_spans):
    # get the default cache
    cache = django.core.cache.caches["default"]

    cache.get_many(["missing_key", "another_key"])

    spans = test_spans.get_spans()
    assert len(spans) == 3

    span_get_many = spans[0]
    span_get_first = spans[1]
    span_get_second = spans[2]

    # LocMemCache doesn't provide an atomic operation
    assert span_get_first.service == "django"
    assert span_get_first.resource == "django.core.cache.backends.locmem.get"
    assert span_get_first.name == "django.cache"
    assert span_get_first.span_type == "cache"
    assert span_get_first.error == 0
    assert span_get_second.service == "django"
    assert span_get_second.resource == "django.core.cache.backends.locmem.get"
    assert span_get_second.name == "django.cache"
    assert span_get_second.span_type == "cache"
    assert span_get_second.error == 0
    assert span_get_many.service == "django"
    assert span_get_many.resource == "django.core.cache.backends.base.get_many"
    assert span_get_many.name == "django.cache"
    assert span_get_many.span_type == "cache"
    assert span_get_many.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": str(["missing_key", "another_key"]),
    }

    assert_dict_issuperset(span_get_many.meta, expected_meta)


def test_cache_set_many(test_spans):
    # get the default cache
    cache = django.core.cache.caches["default"]

    cache.set_many({"first_key": 1, "second_key": 2})

    spans = test_spans.get_spans()
    assert len(spans) == 3

    span_set_many = spans[0]
    span_set_first = spans[1]
    span_set_second = spans[2]

    # LocMemCache doesn't provide an atomic operation
    assert span_set_first.service == "django"
    assert span_set_first.resource == "django.core.cache.backends.locmem.set"
    assert span_set_first.name == "django.cache"
    assert span_set_first.span_type == "cache"
    assert span_set_first.error == 0
    assert span_set_second.service == "django"
    assert span_set_second.resource == "django.core.cache.backends.locmem.set"
    assert span_set_second.name == "django.cache"
    assert span_set_second.span_type == "cache"
    assert span_set_second.error == 0
    assert span_set_many.service == "django"
    assert span_set_many.resource == "django.core.cache.backends.base.set_many"
    assert span_set_many.name == "django.cache"
    assert span_set_many.span_type == "cache"
    assert span_set_many.error == 0

    assert span_set_many.meta["django.cache.backend"] == "django.core.cache.backends.locmem.LocMemCache"
    assert "first_key" in span_set_many.meta["django.cache.key"]
    assert "second_key" in span_set_many.meta["django.cache.key"]


def test_cache_delete_many(test_spans):
    # get the default cache
    cache = django.core.cache.caches["default"]

    cache.delete_many(["missing_key", "another_key"])

    spans = test_spans.get_spans()
    assert len(spans) == 3

    span_delete_many = spans[0]
    span_delete_first = spans[1]
    span_delete_second = spans[2]

    # LocMemCache doesn't provide an atomic operation
    assert span_delete_first.service == "django"
    assert span_delete_first.resource == "django.core.cache.backends.locmem.delete"
    assert span_delete_first.name == "django.cache"
    assert span_delete_first.span_type == "cache"
    assert span_delete_first.error == 0
    assert span_delete_second.service == "django"
    assert span_delete_second.resource == "django.core.cache.backends.locmem.delete"
    assert span_delete_second.name == "django.cache"
    assert span_delete_second.span_type == "cache"
    assert span_delete_second.error == 0
    assert span_delete_many.service == "django"
    assert span_delete_many.resource == "django.core.cache.backends.base.delete_many"
    assert span_delete_many.name == "django.cache"
    assert span_delete_many.span_type == "cache"
    assert span_delete_many.error == 0

    assert span_delete_many.meta["django.cache.backend"] == "django.core.cache.backends.locmem.LocMemCache"
    assert "missing_key" in span_delete_many.meta["django.cache.key"]
    assert "another_key" in span_delete_many.meta["django.cache.key"]


@pytest.mark.django_db
def test_cached_view(client, test_spans):
    # make the first request so that the view is cached
    response = client.get("/cached-users/")
    assert response.status_code == 200

    # check the first call for a non-cached view
    spans = list(test_spans.filter_spans(name="django.cache"))
    assert len(spans) == 3
    # the cache miss
    assert spans[0].resource == "django.core.cache.backends.locmem.get"
    # store the result in the cache
    assert spans[1].resource == "django.core.cache.backends.locmem.set"
    assert spans[2].resource == "django.core.cache.backends.locmem.set"

    # check if the cache hit is traced
    response = client.get("/cached-users/")
    assert response.status_code == 200
    spans = list(test_spans.filter_spans(name="django.cache"))
    # There should be two more spans now
    assert len(spans) == 5

    span_header = spans[3]
    span_view = spans[4]
    assert span_view.service == "django"
    assert span_view.resource == "django.core.cache.backends.locmem.get"
    assert span_view.name == "django.cache"
    assert span_view.span_type == "cache"
    assert span_view.error == 0
    assert span_header.service == "django"
    assert span_header.resource == "django.core.cache.backends.locmem.get"
    assert span_header.name == "django.cache"
    assert span_header.span_type == "cache"
    assert span_header.error == 0

    expected_meta_view = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": (
            "views.decorators.cache.cache_page.."
            "GET.03cdc1cc4aab71b038a6764e5fcabb82.d41d8cd98f00b204e9800998ecf8427e.en-us"
        ),
    }

    expected_meta_header = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "views.decorators.cache.cache_header..03cdc1cc4aab71b038a6764e5fcabb82.en-us",
    }

    assert span_view.meta == expected_meta_view
    assert span_header.meta == expected_meta_header


@pytest.mark.django_db
def test_cached_template(client, test_spans):
    # make the first request so that the view is cached
    response = client.get("/cached-template/")
    assert response.status_code == 200

    # check the first call for a non-cached view
    spans = list(test_spans.filter_spans(name="django.cache"))
    assert len(spans) == 2
    # the cache miss
    assert spans[0].resource == "django.core.cache.backends.locmem.get"
    # store the result in the cache
    assert spans[1].resource == "django.core.cache.backends.locmem.set"

    # check if the cache hit is traced
    response = client.get("/cached-template/")
    assert response.status_code == 200
    spans = list(test_spans.filter_spans(name="django.cache"))
    # Should have 1 more span
    assert len(spans) == 3

    span_template_cache = spans[2]
    assert span_template_cache.service == "django"
    assert span_template_cache.resource == "django.core.cache.backends.locmem.get"
    assert span_template_cache.name == "django.cache"
    assert span_template_cache.span_type == "cache"
    assert span_template_cache.error == 0

    expected_meta = {
        "django.cache.backend": "django.core.cache.backends.locmem.LocMemCache",
        "django.cache.key": "template.cache.users_list.d41d8cd98f00b204e9800998ecf8427e",
    }

    assert span_template_cache.meta == expected_meta


"""
Configuration tests
"""


def test_service_can_be_overridden(client, test_spans):
    with override_config("django", dict(service_name="test-service")):
        response = client.get("/")
        assert response.status_code == 200

    spans = test_spans.get_spans()
    assert len(spans) > 0

    span = spans[0]
    assert span.service == "test-service"


@pytest.mark.django_db
def test_database_service_prefix_can_be_overridden(test_spans):
    with override_config("django", dict(database_service_name_prefix="my-")):
        from django.contrib.auth.models import User

        User.objects.count()

    spans = test_spans.get_spans()
    assert len(spans) > 0

    span = spans[0]
    assert span.service == "my-defaultdb"


@pytest.mark.django_db
def test_database_service_can_be_overridden(test_spans):
    with override_config("django", dict(database_service_name="django-db")):
        from django.contrib.auth.models import User

        User.objects.count()

    spans = test_spans.get_spans()
    assert len(spans) > 0

    span = spans[0]
    assert span.service == "django-db"


@pytest.mark.django_db
def test_database_service_prefix_precedence(test_spans):
    with override_config("django", dict(database_service_name="django-db", database_service_name_prefix="my-")):
        from django.contrib.auth.models import User

        User.objects.count()

    spans = test_spans.get_spans()
    assert len(spans) > 0

    span = spans[0]
    assert span.service == "django-db"


def test_cache_service_can_be_overridden(test_spans):
    cache = django.core.cache.caches["default"]

    with override_config("django", dict(cache_service_name="test-cache-service")):
        cache.get("missing_key")

    spans = test_spans.get_spans()
    assert len(spans) == 1

    span = spans[0]
    assert span.service == "test-cache-service"


def test_django_request_distributed(client, test_spans):
    """
    When making a request to a Django app
        With distributed tracing headers
            The django.request span properly inherits from the distributed trace
    """
    headers = {
        get_wsgi_header(HTTP_HEADER_TRACE_ID): "12345",
        get_wsgi_header(HTTP_HEADER_PARENT_ID): "78910",
        get_wsgi_header(HTTP_HEADER_SAMPLING_PRIORITY): USER_KEEP,
    }
    resp = client.get("/", **headers)
    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    # Assert that the trace properly inherits from the distributed headers
    # DEV: Do not use `test_spans.get_root_span()` since that expects `parent_id is None`
    root = test_spans.find_span(name="django.request")
    root.assert_matches(
        name="django.request",
        trace_id=12345,
        parent_id=78910,
        metrics={
            SAMPLING_PRIORITY_KEY: USER_KEEP,
        },
    )

    first_child_span = test_spans.find_span(parent_id=root.span_id)
    assert first_child_span


def test_django_request_distributed_disabled(client, test_spans):
    """
    When making a request to a Django app
        With distributed tracing headers
            When distributed tracing is disabled
                The django.request span doesn't inherit from the distributed trace
    """
    headers = {
        get_wsgi_header(HTTP_HEADER_TRACE_ID): "12345",
        get_wsgi_header(HTTP_HEADER_PARENT_ID): "78910",
        get_wsgi_header(HTTP_HEADER_SAMPLING_PRIORITY): USER_KEEP,
    }
    with override_config("django", dict(distributed_tracing_enabled=False)):
        resp = client.get("/", **headers)
    assert resp.status_code == 200
    assert resp.content == b"Hello, test app."

    # Assert the trace doesn't inherit from the distributed trace
    root = test_spans.find_span(name="django.request")
    assert root.trace_id != 12345
    assert root.parent_id is None


@pytest.mark.django_db
def test_analytics_global_off_integration_default(client, test_spans):
    """
    When making a request
        When an integration trace search is not set and sample rate is set and globally trace search is disabled
            We expect the root span to not include tag
    """
    with override_global_config(dict(analytics_enabled=False)):
        assert client.get("/users/").status_code == 200

    req_span = test_spans.get_root_span()
    assert req_span.name == "django.request"
    assert req_span.get_metric(ANALYTICS_SAMPLE_RATE_KEY) is None


@pytest.mark.django_db
def test_analytics_global_on_integration_default(client, test_spans):
    """
    When making a request
        When an integration trace search is not event sample rate is not set and globally trace search is enabled
            We expect the root span to have the appropriate tag
    """
    with override_global_config(dict(analytics_enabled=True)):
        assert client.get("/users/").status_code == 200

    req_span = test_spans.get_root_span()
    assert req_span.name == "django.request"
    assert req_span.get_metric(ANALYTICS_SAMPLE_RATE_KEY) == 1.0


@pytest.mark.django_db
def test_analytics_global_off_integration_on(client, test_spans):
    """
    When making a request
        When an integration trace search is enabled and sample rate is set and globally trace search is disabled
            We expect the root span to have the appropriate tag
    """
    with override_global_config(dict(analytics_enabled=False)):
        with override_config("django", dict(analytics_enabled=True, analytics_sample_rate=0.5)):
            assert client.get("/users/").status_code == 200

    sp_request = test_spans.get_root_span()
    assert sp_request.name == "django.request"
    assert sp_request.get_metric(ANALYTICS_SAMPLE_RATE_KEY) == 0.5


@pytest.mark.django_db
def test_analytics_global_off_integration_on_and_none(client, test_spans):
    """
    When making a request
        When an integration trace search is enabled
        Sample rate is set to None
        Globally trace search is disabled
            We expect the root span to have the appropriate tag
    """
    with override_global_config(dict(analytics_enabled=False)):
        with override_config("django", dict(analytics_enabled=False, analytics_sample_rate=1.0)):
            assert client.get("/users/").status_code == 200

    sp_request = test_spans.get_root_span()
    assert sp_request.name == "django.request"
    assert sp_request.get_metric(ANALYTICS_SAMPLE_RATE_KEY) is None


def test_trace_query_string_integration_enabled(client, test_spans):
    with override_http_config("django", dict(trace_query_string=True)):
        assert client.get("/?key1=value1&key2=value2").status_code == 200

    sp_request = test_spans.get_root_span()
    assert sp_request.name == "django.request"
    assert sp_request.get_tag(http.QUERY_STRING) == "key1=value1&key2=value2"


def test_disabled_caches(client, test_spans):
    with override_config("django", dict(instrument_caches=False)):
        cache = django.core.cache.caches["default"]
        cache.get("missing_key")

    spans = test_spans.get_spans()
    assert len(spans) == 0


"""
Template tests
"""


def test_template(test_spans):
    # prepare a base template using the default engine
    template = django.template.Template("Hello {{name}}!")
    ctx = django.template.Context({"name": "Django"})

    assert template.render(ctx) == "Hello Django!"

    spans = test_spans.get_spans()
    assert len(spans) == 1

    span = spans[0]
    assert span.span_type == "template"
    assert span.name == "django.template.render"

    template.name = "my-template"
    assert template.render(ctx) == "Hello Django!"
    spans = test_spans.get_spans()
    assert len(spans) == 2

    span = spans[1]
    assert span.get_tag("django.template.name") == "my-template"


@pytest.mark.skipif(PY2, reason="pathlib is not part of the Python 2 stdlib")
def test_template_name(test_spans):
    from pathlib import PosixPath

    # prepare a base template using the default engine
    template = django.template.Template("Hello {{name}}!")

    # DEV: template.name can be an instance of PosixPath (see
    # https://github.com/DataDog/dd-trace-py/issues/2418)
    template.name = PosixPath("/my-template")
    template.render(django.template.Context({"name": "Django"}))

    spans = test_spans.get_spans()
    assert len(spans) == 1

    (span,) = spans
    assert span.get_tag("django.template.name") == "/my-template"
    assert span.resource == "/my-template"


"""
OpenTracing tests
"""


@pytest.mark.django_db
def test_middleware_trace_request_ot(client, test_spans, tracer):
    """OpenTracing version of test_middleware_trace_request."""
    ot_tracer = init_tracer("my_svc", tracer)

    # ensures that the internals are properly traced
    with ot_tracer.start_active_span("ot_span"):
        assert client.get("/users/").status_code == 200

    # check for spans
    spans = test_spans.get_spans()
    ot_span = spans[0]
    sp_request = spans[1]

    # confirm parenting
    assert ot_span.parent_id is None
    assert sp_request.parent_id == ot_span.span_id

    assert ot_span.resource == "ot_span"
    assert ot_span.service == "my_svc"

    assert sp_request.get_tag("http.status_code") == "200"
    assert sp_request.get_tag(http.URL) == "http://testserver/users/"
    assert sp_request.get_tag("django.user.is_authenticated") == "False"
    assert sp_request.get_tag("http.method") == "GET"


def test_collecting_requests_handles_improperly_configured_error(client, test_spans):
    """
    Since it's difficult to reproduce the ImproperlyConfigured error via django (server setup), will instead
    mimic the failure by mocking the user_is_authenticated to raise an error.
    """
    # patch django._patch - django.__init__.py imports patch.py module as _patch
    with mock.patch(
        "ddtrace.contrib.django.utils.user_is_authenticated", side_effect=django.core.exceptions.ImproperlyConfigured
    ):
        # If ImproperlyConfigured error bubbles up, should automatically fail the test.
        resp = client.get("/")
        assert resp.status_code == 200
        assert resp.content == b"Hello, test app."

        # Assert the correct number of traces and spans
        if django.VERSION >= (2, 0, 0):
            test_spans.assert_span_count(26)
        elif django.VERSION < (1, 11, 0):
            test_spans.assert_span_count(15)
        else:
            test_spans.assert_span_count(16)

        root = test_spans.get_root_span()
        root.assert_matches(name="django.request")


"""
urlpatterns tests
There are a variety of ways a user can point to their views.
"""


@pytest.mark.skipif(django.VERSION < (2, 0, 0), reason="path only exists in >=2.0.0")
def test_urlpatterns_path(client, test_spans):
    """
    When a view is specified using `django.urls.path`
        The view is traced
    """
    assert client.get("/path/").status_code == 200

    # Ensure the view was traced
    assert len(list(test_spans.filter_spans(name="django.view"))) == 1


@pytest.mark.skipif(django.VERSION < (2, 0, 0), reason="repath only exists in >=2.0.0")
def test_urlpatterns_repath(client, test_spans):
    """
    When a view is specified using `django.urls.repath`
        The view is traced
    """
    assert client.get("/re-path123/").status_code == 200

    # Ensure the view was traced
    assert len(list(test_spans.filter_spans(name="django.view"))) == 1


@pytest.mark.skipif(django.VERSION < (2, 0, 0), reason="")
@pytest.mark.django_db
def test_user_name_included(client, test_spans):
    """
    When making a request to a Django app with user name included
        We correctly add the `django.user.name` tag to the root span
    """
    resp = client.get("/authenticated/")
    assert resp.status_code == 200

    # user name should be present in root span tags
    root = test_spans.get_root_span()
    assert root.meta.get("django.user.name") == "Jane Doe"
    assert root.meta.get("django.user.is_authenticated") == "True"


@pytest.mark.skipif(django.VERSION < (2, 0, 0), reason="")
@pytest.mark.django_db
def test_user_name_excluded(client, test_spans):
    """
    When making a request to a Django app with user name excluded
        We correctly omit the `django.user.name` tag to the root span
    """
    with override_config("django", dict(include_user_name=False)):
        resp = client.get("/authenticated/")
    assert resp.status_code == 200

    # user name should not be present in root span tags
    root = test_spans.get_root_span()
    assert "django.user.name" not in root.meta
    assert root.meta.get("django.user.is_authenticated") == "True"


def test_django_use_handler_resource_format(client, test_spans):
    """
    Test that the specified format is used over the default.
    """
    with override_config("django", dict(use_handler_resource_format=True)):
        resp = client.get("/")
        assert resp.status_code == 200
        assert resp.content == b"Hello, test app."

        # Assert the structure of the root `django.request` span
        root = test_spans.get_root_span()
        resource = "GET tests.contrib.django.views.index"

        root.assert_matches(resource=resource, parent_id=None, span_type="web")


def test_django_use_handler_resource_format_env(client, test_spans):
    """
    Test that the specified format is used over the default.
    """
    with override_env(dict(DD_DJANGO_USE_HANDLER_RESOURCE_FORMAT="true")):
        out = subprocess.check_output(
            [
                "python",
                "-c",
                (
                    "from ddtrace import config, patch_all; patch_all(); "
                    "assert config.django.use_handler_resource_format; print('Test success')"
                ),
            ]
        )
        assert out.startswith(b"Test success")


def test_django_use_legacy_resource_format(client, test_spans):
    """
    Test that the specified format is used over the default.
    """
    with override_config("django", dict(use_legacy_resource_format=True)):
        resp = client.get("/")
        assert resp.status_code == 200
        assert resp.content == b"Hello, test app."

        # Assert the structure of the root `django.request` span
        root = test_spans.get_root_span()
        resource = "tests.contrib.django.views.index"

        root.assert_matches(resource=resource, parent_id=None, span_type="web")


def test_django_use_legacy_resource_format_env(client, test_spans):
    with override_env(dict(DD_DJANGO_USE_LEGACY_RESOURCE_FORMAT="true")):
        out = subprocess.check_output(
            [
                "python",
                "-c",
                (
                    "from ddtrace import config, patch_all; patch_all(); "
                    "assert config.django.use_legacy_resource_format; print('Test success')"
                ),
            ],
        )
        assert out.startswith(b"Test success")


def test_custom_dispatch_template_view(client, test_spans):
    """
    Test that a template view with a custom dispatch method inherited from a
    mixin is called.
    """
    resp = client.get("/composed-template-view/")
    assert resp.status_code == 200
    assert resp.content.strip() == b"custom dispatch 2"

    spans = test_spans.get_spans()
    assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
        "tests.contrib.django.views.CustomDispatchMixin.dispatch",
        "tests.contrib.django.views.AnotherCustomDispatchMixin.dispatch",
        "django.views.generic.base.View.dispatch",
    ]


def test_custom_dispatch_get_view(client, test_spans):
    """
    Test that a get method on a view with a custom dispatch method inherited
    from a mixin is called.
    """
    resp = client.get("/composed-get-view/")
    assert resp.status_code == 200
    assert resp.content.strip() == b"custom get"

    spans = test_spans.get_spans()
    assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
        "tests.contrib.django.views.CustomDispatchMixin.dispatch",
        "django.views.generic.base.View.dispatch",
    ]
    assert [s.resource for s in spans if s.resource.endswith("get")] == [
        "tests.contrib.django.views.ComposedGetView.get",
        "tests.contrib.django.views.CustomGetView.get",
    ]


def test_view_mixin(client, test_spans):
    from tests.contrib.django import views

    assert views.DISPATCH_CALLED is False
    resp = client.get("/composed-view/")
    assert views.DISPATCH_CALLED is True

    assert resp.status_code == 200
    assert resp.content.strip() == b"custom dispatch"


def test_template_view_patching():
    """
    Test to ensure that patching a view does not give it properties it did not have before
    """
    # DEV: `vars(cls)` will give you only the properties defined on that class,
    #      it will not traverse through __mro__ to find the property from a parent class

    # We are not starting with a "dispatch" property
    assert "dispatch" not in vars(TemplateView)

    # Manually call internal method for patching
    instrument_view(django, TemplateView)
    assert "dispatch" not in vars(TemplateView)

    # Patch via `as_view()`
    TemplateView.as_view()
    assert "dispatch" not in vars(TemplateView)


class _MissingSchemeRequest(django.http.HttpRequest):
    @property
    def scheme(self):
        pass


class _HttpRequest(django.http.HttpRequest):
    @property
    def scheme(self):
        return b"http"


@pytest.mark.parametrize(
    "request_cls,request_path,http_host",
    itertools.product(
        [django.http.HttpRequest, _HttpRequest, _MissingSchemeRequest],
        [u"/;some/?awful/=path/foo:bar/", b"/;some/?awful/=path/foo:bar/"],
        [u"testserver", b"testserver", SimpleLazyObject(lambda: "testserver"), SimpleLazyObject(lambda: object())],
    ),
)
def test_helper_get_request_uri(request_cls, request_path, http_host):
    def eval_lazy(lo):
        return str(lo) if issubclass(lo.__class__, str) else bytes(lo)

    request = request_cls()
    request.path = request_path
    request.META = {"HTTP_HOST": http_host}
    request_uri = get_request_uri(request)
    if (
        isinstance(http_host, SimpleLazyObject) and not (issubclass(http_host.__class__, str))
    ) or request_cls is _MissingSchemeRequest:
        assert request_uri is None
    else:
        assert (
            request_cls == _HttpRequest
            and isinstance(request_path, binary_type)
            and isinstance(http_host, binary_type)
            and isinstance(request_uri, binary_type)
        ) or isinstance(request_uri, string_type)

        host = ensure_text(eval_lazy(http_host)) if isinstance(http_host, SimpleLazyObject) else http_host
        assert request_uri == "".join(map(ensure_text, (request.scheme, "://", host, request_path)))


@pytest.fixture()
def resource():
    # setUp and tearDown for TestWSGI test cases.
    request_started.disconnect(close_old_connections)
    yield
    request_started.connect(close_old_connections)


class TestWSGI:
    request_factory = RequestFactory()

    def test_get_wsgi_application_200_request(self, test_spans, resource):
        application = get_wsgi_application()
        test_response = {}
        environ = self.request_factory._base_environ(
            PATH_INFO="/", CONTENT_TYPE="text/html; charset=utf-8", REQUEST_METHOD="GET"
        )

        def start_response(status, headers):
            test_response["status"] = status
            test_response["headers"] = headers

        response = application(environ, start_response)

        expected_headers = [
            ("my-response-header", "my_response_value"),
            ("Content-Type", "text/html; charset=utf-8"),
        ]
        assert test_response["status"] == "200 OK"
        assert all([header in test_response["headers"] for header in expected_headers])
        assert response.content == b"Hello, test app."

        # Assert the structure of the root `django.request` span
        root = test_spans.get_root_span()

        if django.VERSION >= (2, 2, 0):
            resource = "GET ^$"
        else:
            resource = "GET tests.contrib.django.views.index"

        meta = {
            "django.request.class": "django.core.handlers.wsgi.WSGIRequest",
            "django.response.class": "django.http.response.HttpResponse",
            "django.user.is_authenticated": "False",
            "django.view": "tests.contrib.django.views.index",
            "http.method": "GET",
            "http.status_code": "200",
            "http.url": "http://testserver/",
        }
        if django.VERSION >= (2, 2, 0):
            meta["http.route"] = "^$"

        assert http.QUERY_STRING not in root.meta
        root.assert_matches(
            name="django.request",
            service="django",
            resource=resource,
            parent_id=None,
            span_type="web",
            error=0,
            meta=meta,
        )

    def test_get_wsgi_application_500_request(self, test_spans, resource):
        application = get_wsgi_application()
        test_response = {}
        environ = self.request_factory._base_environ(
            PATH_INFO="/error-500/", CONTENT_TYPE="text/html; charset=utf-8", REQUEST_METHOD="GET"
        )

        def start_response(status, headers):
            test_response["status"] = status
            test_response["headers"] = headers

        response = application(environ, start_response)
        assert test_response["status"].upper() == "500 INTERNAL SERVER ERROR"
        assert "Server Error" in str(response.content)

        # Assert the structure of the root `django.request` span
        root = test_spans.get_root_span()

        assert root.error == 1
        assert root.get_tag("http.status_code") == "500"
        assert root.get_tag(http.URL) == "http://testserver/error-500/"
        assert root.get_tag("django.response.class") == "django.http.response.HttpResponseServerError"
        if django.VERSION >= (2, 2, 0):
            assert root.resource == "GET ^error-500/$"
        else:
            assert root.resource == "GET tests.contrib.django.views.error_500"


@pytest.mark.django_db
def test_connections_patched():
    from django.db import connection
    from django.db import connections

    assert len(connections.all())
    for conn in connections.all():
        assert isinstance(conn.cursor, wrapt.ObjectProxy)

    assert isinstance(connection.cursor, wrapt.ObjectProxy)
