#  BSD 3-Clause License
#
#  Copyright (c) 2019, Elasticsearch BV
#  All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#
#  * Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
#  * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
#  * Neither the name of the copyright holder nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import decimal
import logging
import time
from collections import defaultdict

import mock
import pytest

import elasticapm
from elasticapm.conf import Config, VersionedConfig
from elasticapm.conf.constants import SPAN, TRANSACTION
from elasticapm.traces import Tracer, capture_span, execution_context
from elasticapm.utils.disttracing import TraceParent
from tests.utils import any_record_contains, assert_any_record_contains


@pytest.fixture()
def tracer(elasticapm_client):
    frames = [
        {
            "function": "something_expensive",
            "abs_path": "/var/parent-elasticapm/elasticapm/tests/contrib/django/testapp/views.py",
            "lineno": 52,
            "module": "tests.contrib.django.testapp.views",
            "filename": "tests/contrib/django/testapp/views.py",
        },
        {
            "function": "_resolve_lookup",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/base.py",
            "lineno": 789,
            "module": "django.template.base",
            "filename": "django/template/base.py",
        },
        {
            "function": "resolve",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/base.py",
            "lineno": 735,
            "module": "django.template.base",
            "filename": "django/template/base.py",
        },
        {
            "function": "resolve",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/base.py",
            "lineno": 585,
            "module": "django.template.base",
            "filename": "django/template/base.py",
        },
        {
            "lineno": 4,
            "filename": "/var/parent-elasticapm/elasticapm/tests/contrib/django/testapp/templates/list_fish.html",
        },
        {
            "function": "render",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/defaulttags.py",
            "lineno": 4,
            "module": "django.template.defaulttags",
            "filename": "django/template/defaulttags.py",
        },
        {
            "function": "render_node",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/debug.py",
            "lineno": 78,
            "module": "django.template.debug",
            "filename": "django/template/debug.py",
        },
        {
            "function": "render",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/base.py",
            "lineno": 840,
            "module": "django.template.base",
            "filename": "django/template/base.py",
        },
        {
            "function": "instrumented_test_render",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/test/utils.py",
            "lineno": 85,
            "module": "django.test.utils",
            "filename": "django/test/utils.py",
        },
        {
            "function": "render",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/base.py",
            "lineno": 140,
            "module": "django.template.base",
            "filename": "django/template/base.py",
        },
        {
            "function": "rendered_content",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/response.py",
            "lineno": 82,
            "module": "django.template.response",
            "filename": "django/template/response.py",
        },
        {
            "function": "render",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/template/response.py",
            "lineno": 105,
            "module": "django.template.response",
            "filename": "django/template/response.py",
        },
        {
            "function": "get_response",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/core/handlers/base.py",
            "lineno": 137,
            "module": "django.core.handlers.base",
            "filename": "django/core/handlers/base.py",
        },
        {
            "function": "__call__",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/test/client.py",
            "lineno": 109,
            "module": "django.test.client",
            "filename": "django/test/client.py",
        },
        {
            "function": "request",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/test/client.py",
            "lineno": 426,
            "module": "django.test.client",
            "filename": "django/test/client.py",
        },
        {
            "function": "get",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/test/client.py",
            "lineno": 280,
            "module": "django.test.client",
            "filename": "django/test/client.py",
        },
        {
            "function": "get",
            "abs_path": "/home/ron/.virtualenvs/elasticapm/local/lib/python2.7/site-packages/django/test/client.py",
            "lineno": 473,
            "module": "django.test.client",
            "filename": "django/test/client.py",
        },
        {
            "function": "test_template_name_as_view",
            "abs_path": "/var/parent-elasticapm/elasticapm/tests/contrib/django/django_tests.py",
            "lineno": 710,
            "module": "tests.contrib.django.django_tests",
            "filename": "tests/contrib/django/django_tests.py",
        },
    ]

    events = defaultdict(list)

    def queue(event_type, event, flush=False):
        events[event_type].append(event)

    store = Tracer(lambda: frames, lambda frames: frames, queue, elasticapm_client.config, elasticapm_client)
    store.events = events
    return store


def test_leaf_tracing(tracer):
    tracer.begin_transaction("transaction.test")

    with capture_span("root", "custom"):
        with capture_span("child1-leaf", "custom", leaf=True):
            # These two spans should not show up
            with capture_span("ignored-child1", "custom", leaf=True, duration=0.01):
                pass

            with capture_span("ignored-child2", "custom", leaf=False, duration=0.01):
                pass

    tracer.end_transaction(None, "transaction")

    spans = tracer.events[SPAN]

    assert len(spans) == 2

    signatures = {"root", "child1-leaf"}
    assert {t["name"] for t in spans} == signatures


def test_cancelled_spans_do_not_compress_with_later_spans(elasticapm_client):
    elasticapm_client.config.update(
        "1",
        span_compression_enabled=True,
        span_compression_exact_match_max_duration="50ms",
        span_compression_same_kind_max_duration="50ms",
    )
    elasticapm_client.begin_transaction("transaction.test")

    extra = {
        "message": {"queue": {"name": "topic"}},
        "destination": {"service": {"name": "kafka", "resource": "kafka/topic", "type": "messaging"}},
    }
    with capture_span(
        "Kafka RECEIVE from topic",
        span_type="messaging",
        span_subtype="kafka",
        span_action="receive",
        extra=extra.copy(),
        leaf=True,
        duration=0.01,
    ) as span:
        span.cancel()

    with capture_span(
        "Kafka RECEIVE from topic",
        span_type="messaging",
        span_subtype="kafka",
        span_action="receive",
        extra=extra.copy(),
        leaf=True,
        duration=0.01,
    ):
        pass

    elasticapm_client.end_transaction("transaction")

    spans = elasticapm_client.events[SPAN]
    assert len(spans) == 1
    assert spans[0]["name"] == "Kafka RECEIVE from topic"


def test_get_trace_parent_header(elasticapm_client):
    trace_parent = TraceParent.from_string("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-03")
    transaction = elasticapm_client.begin_transaction("test", trace_parent=trace_parent)
    assert transaction.trace_parent.to_string() == elasticapm.get_trace_parent_header()


def test_get_transaction_id(elasticapm_client):
    transaction = elasticapm_client.begin_transaction("test")
    assert transaction.id == elasticapm.get_transaction_id()


def test_get_trace_id(elasticapm_client):
    trace_parent = TraceParent.from_string("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-03")
    elasticapm_client.begin_transaction("test", trace_parent=trace_parent)
    assert trace_parent.trace_id == elasticapm.get_trace_id()


def test_set_transaction_result(elasticapm_client):
    transaction = elasticapm_client.begin_transaction("test")
    elasticapm.set_transaction_result("success")
    assert "success" == transaction.result


def test_set_transaction_outcome(elasticapm_client):
    transaction = elasticapm_client.begin_transaction("test")
    elasticapm.set_transaction_outcome("failure")
    assert "failure" == transaction.outcome


def test_set_transaction_outcome_with_50X_http_status_code(elasticapm_client):
    transaction = elasticapm_client.begin_transaction("test")
    elasticapm.set_transaction_outcome("anything", http_status_code=500)
    assert "failure" == transaction.outcome


def test_set_transaction_outcome_with_20X_http_status_code(elasticapm_client):
    transaction = elasticapm_client.begin_transaction("test")
    elasticapm.set_transaction_outcome("anything", http_status_code=200)
    assert "success" == transaction.outcome


def test_set_transaction_outcome_with_unknown_http_status_code(elasticapm_client):
    transaction = elasticapm_client.begin_transaction("test")
    elasticapm.set_transaction_outcome("anything", http_status_code="a")
    assert "unknown" == transaction.outcome


def test_set_unknown_transaction_outcome(elasticapm_client):
    transaction = elasticapm_client.begin_transaction("test")
    elasticapm.set_transaction_outcome("anything")
    assert "unknown" == transaction.outcome


def test_get_transaction():
    requests_store = Tracer(lambda: [], lambda: [], lambda *args: None, VersionedConfig(Config(), "1"), None)
    t = requests_store.begin_transaction("test")
    assert t == execution_context.get_transaction()
    requests_store.end_transaction(200, "test")


def test_get_transaction_clear():
    requests_store = Tracer(lambda: [], lambda: [], lambda *args: None, VersionedConfig(Config(), "1"), None)
    t = requests_store.begin_transaction("test")
    assert t == execution_context.get_transaction(clear=True)
    assert execution_context.get_transaction() is None


def test_label_transaction():
    requests_store = Tracer(lambda: [], lambda: [], lambda *args: None, VersionedConfig(Config(), "1"), None)
    transaction = requests_store.begin_transaction("test")
    elasticapm.label(foo="bar")
    transaction.label(baz="bazzinga")
    requests_store.end_transaction(200, "test")

    assert transaction.labels == {"foo": "bar", "baz": "bazzinga"}
    transaction_dict = transaction.to_dict()
    assert transaction_dict["context"]["tags"] == {"foo": "bar", "baz": "bazzinga"}


@pytest.mark.parametrize(
    "elasticapm_client",
    [{"enabled": True}, {"enabled": False}],
    indirect=True,
)
def test_label_while_no_transaction(caplog, elasticapm_client):
    with caplog.at_level(logging.WARNING, "elasticapm.errors"):
        elasticapm.label(foo="bar")
    is_present = any_record_contains(caplog.records, "foo", "elasticapm.errors")
    assert is_present is elasticapm_client.config.enabled


def test_label_with_allowed_non_string_value():
    requests_store = Tracer(lambda: [], lambda: [], lambda *args: None, VersionedConfig(Config(), "1"), None)
    t = requests_store.begin_transaction("test")
    elasticapm.label(foo=1, bar=True, baz=1.1, bazzinga=decimal.Decimal("1.1"))
    requests_store.end_transaction(200, "test")
    assert t.labels == {"foo": 1, "bar": True, "baz": 1.1, "bazzinga": decimal.Decimal("1.1")}


def test_label_with_not_allowed_non_string_value():
    class SomeType(object):
        def __str__(self):
            return "ok"

        def __unicode__(self):
            return "ok"

    requests_store = Tracer(lambda: [], lambda: [], lambda *args: None, VersionedConfig(Config(), "1"), None)
    t = requests_store.begin_transaction("test")
    elasticapm.label(foo=SomeType())
    requests_store.end_transaction(200, "test")
    assert t.labels == {"foo": "ok"}


def test_labels_merge(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    elasticapm.label(foo=1, bar="baz")
    elasticapm.label(bar=3, boo="biz")
    elasticapm_client.end_transaction("test", "OK")
    transactions = elasticapm_client.events[TRANSACTION]

    assert transactions[0]["context"]["tags"] == {"foo": 1, "bar": 3, "boo": "biz"}


def test_labels_dedot(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    elasticapm.label(**{"d.o.t": "dot"})
    elasticapm.label(**{"s*t*a*r": "star"})
    elasticapm.label(**{'q"u"o"t"e': "quote"})

    elasticapm_client.end_transaction("test_name", 200)

    transactions = elasticapm_client.events[TRANSACTION]

    assert transactions[0]["context"]["tags"] == {"d_o_t": "dot", "s_t_a_r": "star", "q_u_o_t_e": "quote"}


@pytest.mark.parametrize(
    "elasticapm_client", [{"server_version": (7, 14, 0)}], indirect=True
)  # unsampled transactions are dropped with server 8.0+
def test_dedot_is_not_run_when_unsampled(elasticapm_client):
    for sampled in (True, False):
        t = elasticapm_client.begin_transaction("test")
        t.is_sampled = sampled
        elasticapm.set_context(lambda: {"a.b": "b"})
        elasticapm_client.end_transaction("x", "OK")
    sampled_transaction, unsampled_transaction = elasticapm_client.events[TRANSACTION]
    assert "a_b" in sampled_transaction["context"]["custom"]
    assert "context" not in unsampled_transaction


@pytest.mark.parametrize(
    "name1,name2,expected1,expected2",
    [
        ("test_name", "another_name", "test_name", "another_name"),
        (["test_name"], {"another_name": "foo"}, "['test_name']", "{'another_name': 'foo'}"),
    ],
)
def test_set_transaction_name(elasticapm_client, name1, name2, expected1, expected2):
    elasticapm_client.begin_transaction("test")
    elasticapm_client.end_transaction(name1, 200)

    elasticapm_client.begin_transaction("test")

    elasticapm.set_transaction_name(name2)

    elasticapm_client.end_transaction(name1, 200)

    transactions = elasticapm_client.events[TRANSACTION]
    assert transactions[0]["name"] == expected1
    assert transactions[1]["name"] == expected2


def test_set_transaction_custom_data(elasticapm_client):
    elasticapm_client.begin_transaction("test")

    elasticapm.set_custom_context({"foo": "bar"})

    elasticapm_client.end_transaction("foo", 200)
    transactions = elasticapm_client.events[TRANSACTION]

    assert transactions[0]["context"]["custom"] == {"foo": "bar"}


def test_set_transaction_custom_data_merge(elasticapm_client):
    elasticapm_client.begin_transaction("test")

    elasticapm.set_custom_context({"foo": "bar", "bar": "baz"})
    elasticapm.set_custom_context({"bar": "bie", "boo": "biz"})

    elasticapm_client.end_transaction("foo", 200)
    transactions = elasticapm_client.events[TRANSACTION]

    assert transactions[0]["context"]["custom"] == {"foo": "bar", "bar": "bie", "boo": "biz"}


def test_set_user_context(elasticapm_client):
    elasticapm_client.begin_transaction("test")

    elasticapm.set_user_context(username="foo", email="foo@example.com", user_id=42)

    elasticapm_client.end_transaction("foo", 200)
    transactions = elasticapm_client.events[TRANSACTION]

    assert transactions[0]["context"]["user"] == {"username": "foo", "email": "foo@example.com", "id": 42}


def test_set_user_context_merge(elasticapm_client):
    elasticapm_client.begin_transaction("test")

    elasticapm.set_user_context(username="foo", email="bar@example.com")
    elasticapm.set_user_context(email="foo@example.com", user_id=42)

    elasticapm_client.end_transaction("foo", 200)
    transactions = elasticapm_client.events[TRANSACTION]

    assert transactions[0]["context"]["user"] == {"username": "foo", "email": "foo@example.com", "id": 42}


@pytest.mark.parametrize(
    "elasticapm_client", [{"server_version": (7, 14, 0)}], indirect=True
)  # unsampled transactions are dropped with server 8.0+
def test_callable_context_ignored_when_not_sampled(elasticapm_client):
    callable_data = mock.Mock()
    callable_data.return_value = {"a": "b"}
    transaction = elasticapm_client.begin_transaction("test")
    transaction.is_sampled = False
    elasticapm.set_context({"c": "d"})
    elasticapm.set_context(callable_data)
    elasticapm_client.end_transaction("test", "OK")
    transaction = elasticapm_client.events[TRANSACTION][0]
    assert callable_data.call_count == 0
    assert "context" not in transaction


def test_dedot_context_keys(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    elasticapm.set_context({"d.o.t": "d_o_t", "s*t*a*r": "s_t_a_r", "q*u*o*t*e": "q_u_o_t_e"})
    elasticapm_client.end_transaction("foo", 200)
    transaction = elasticapm_client.events[TRANSACTION][0]
    assert transaction["context"]["custom"] == {"s_t_a_r": "s_t_a_r", "q_u_o_t_e": "q_u_o_t_e", "d_o_t": "d_o_t"}


def test_transaction_name_none_is_converted_to_empty_string(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    transaction = elasticapm_client.end_transaction(None, 200)
    assert transaction.name == ""


def test_transaction_without_name_result(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    transaction = elasticapm_client.end_transaction()
    assert transaction.name == ""


def test_dotted_span_type_conversion(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    with capture_span("foo", "type"):
        with capture_span("bar", "type.subtype"):
            with capture_span("baz", "type.subtype.action"):
                with capture_span("bazzinga", "type.subtype.action.more"):
                    pass
    elasticapm_client.end_transaction("test", "OK")
    spans = elasticapm_client.events[SPAN]

    assert spans[0]["name"] == "bazzinga"
    assert spans[0]["type"] == "type"
    assert spans[0]["subtype"] == "subtype"
    assert spans[0]["action"] == "action"

    assert spans[1]["name"] == "baz"
    assert spans[1]["type"] == "type"
    assert spans[1]["subtype"] == "subtype"
    assert spans[1]["action"] == "action"

    assert spans[2]["name"] == "bar"
    assert spans[2]["type"] == "type"
    assert spans[2]["subtype"] == "subtype"
    assert spans[2]["action"] is None

    assert spans[3]["name"] == "foo"
    assert spans[3]["type"] == "type"
    assert spans[3]["subtype"] is None
    assert spans[3]["action"] is None


def test_span_labelling(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    with elasticapm.capture_span("test", labels={"foo": "bar", "ba.z": "baz.zinga"}) as span:
        span.label(lorem="ipsum")
    elasticapm_client.end_transaction("test", "OK")
    span = elasticapm_client.events[SPAN][0]
    assert span["context"]["tags"] == {"foo": "bar", "ba_z": "baz.zinga", "lorem": "ipsum"}


def test_span_sync(elasticapm_client):
    elasticapm_client.begin_transaction("test")
    with capture_span("foo", "type", sync=True):
        with capture_span("bar", "type", sync=False):
            with capture_span("baz", "type"):
                pass
    elasticapm_client.end_transaction("test", "OK")
    spans = elasticapm_client.events[SPAN]

    assert spans[0]["name"] == "baz"
    assert "sync" not in spans[0]

    assert spans[1]["name"] == "bar"
    assert not spans[1]["sync"]

    assert spans[2]["name"] == "foo"
    assert spans[2]["sync"]
