import dogpile
import pytest

from ddtrace import Pin
from ddtrace.contrib.dogpile_cache.patch import patch
from ddtrace.contrib.dogpile_cache.patch import unpatch
from tests.utils import DummyTracer
from tests.utils import TracerSpanContainer
from tests.utils import assert_is_measured


@pytest.fixture
def tracer():
    return DummyTracer()


@pytest.fixture
def test_spans(tracer):
    container = TracerSpanContainer(tracer)
    yield container
    container.reset()


@pytest.fixture
def region(tracer):
    patch()
    # Setup a simple dogpile cache region for testing.
    # The backend is trivial so we can use memory to simplify test setup.
    test_region = dogpile.cache.make_region(name="TestRegion", key_mangler=lambda x: x)
    test_region.configure("dogpile.cache.memory")
    Pin.override(dogpile.cache, tracer=tracer)
    return test_region


@pytest.fixture(autouse=True)
def cleanup():
    yield
    unpatch()


@pytest.fixture
def single_cache(region):
    @region.cache_on_arguments()
    def fn(x):
        return x * 2

    return fn


@pytest.fixture
def multi_cache(region):
    @region.cache_multi_on_arguments()
    def fn(*x):
        return [i * 2 for i in x]

    return fn


def test_doesnt_trace_with_no_pin(tracer, single_cache, multi_cache, test_spans):
    # No pin is set
    unpatch()

    assert single_cache(1) == 2
    assert test_spans.pop_traces() == []

    assert multi_cache(2, 3) == [4, 6]
    assert test_spans.pop_traces() == []


def test_doesnt_trace_with_disabled_pin(tracer, single_cache, multi_cache, test_spans):
    tracer.enabled = False

    assert single_cache(1) == 2
    assert test_spans.pop_traces() == []

    assert multi_cache(2, 3) == [4, 6]
    assert test_spans.pop_traces() == []


def test_traces_get_or_create(tracer, single_cache, test_spans):
    assert single_cache(1) == 2
    traces = test_spans.pop_traces()
    assert len(traces) == 1
    spans = traces[0]
    assert len(spans) == 1
    span = spans[0]

    assert_is_measured(span)
    assert span.name == "dogpile.cache"
    assert span.span_type == "cache"
    assert span.resource == "get_or_create"
    assert span.get_tag("key") == "tests.contrib.dogpile_cache.test_tracing:fn|1"
    assert span.get_tag("hit") == "False"
    assert span.get_tag("expired") == "True"
    assert span.get_tag("backend") == "MemoryBackend"
    assert span.get_tag("region") == "TestRegion"

    # Now the results should be cached.
    assert single_cache(1) == 2
    traces = test_spans.pop_traces()
    assert len(traces) == 1
    spans = traces[0]
    assert len(spans) == 1
    span = spans[0]

    assert_is_measured(span)
    assert span.name == "dogpile.cache"
    assert span.span_type == "cache"
    assert span.resource == "get_or_create"
    assert span.get_tag("key") == "tests.contrib.dogpile_cache.test_tracing:fn|1"
    assert span.get_tag("hit") == "True"
    assert span.get_tag("expired") == "False"
    assert span.get_tag("backend") == "MemoryBackend"
    assert span.get_tag("region") == "TestRegion"


def test_traces_get_or_create_multi(tracer, multi_cache, test_spans):
    assert multi_cache(2, 3) == [4, 6]
    traces = test_spans.pop_traces()
    assert len(traces) == 1
    spans = traces[0]
    assert len(spans) == 1
    span = spans[0]

    assert_is_measured(span)
    assert span.name == "dogpile.cache"
    assert span.span_type == "cache"
    assert span.get_tag("keys") == (
        "['tests.contrib.dogpile_cache.test_tracing:fn|2', " + "'tests.contrib.dogpile_cache.test_tracing:fn|3']"
    )
    assert span.get_tag("hit") == "False"
    assert span.get_tag("expired") == "True"
    assert span.get_tag("backend") == "MemoryBackend"
    assert span.get_tag("region") == "TestRegion"

    # Partial hit
    assert multi_cache(2, 4) == [4, 8]
    traces = test_spans.pop_traces()
    assert len(traces) == 1
    spans = traces[0]
    assert len(spans) == 1
    span = spans[0]
    assert_is_measured(span)
    assert span.name == "dogpile.cache"
    assert span.span_type == "cache"
    assert span.get_tag("keys") == (
        "['tests.contrib.dogpile_cache.test_tracing:fn|2', " + "'tests.contrib.dogpile_cache.test_tracing:fn|4']"
    )
    assert span.get_tag("hit") == "False"
    assert span.get_tag("expired") == "True"
    assert span.get_tag("backend") == "MemoryBackend"
    assert span.get_tag("region") == "TestRegion"

    # Full hit
    assert multi_cache(2, 4) == [4, 8]
    traces = test_spans.pop_traces()
    assert len(traces) == 1
    spans = traces[0]
    assert len(spans) == 1
    span = spans[0]
    assert_is_measured(span)
    assert span.name == "dogpile.cache"
    assert span.span_type == "cache"
    assert span.get_tag("keys") == (
        "['tests.contrib.dogpile_cache.test_tracing:fn|2', " + "'tests.contrib.dogpile_cache.test_tracing:fn|4']"
    )
    assert span.get_tag("hit") == "True"
    assert span.get_tag("expired") == "False"
    assert span.get_tag("backend") == "MemoryBackend"
    assert span.get_tag("region") == "TestRegion"


class TestInnerFunctionCalls(object):
    def single_cache(self, x):
        return x * 2

    def multi_cache(self, *x):
        return [i * 2 for i in x]

    def test_calls_inner_functions_correctly(self, region, mocker):
        """This ensures the get_or_create behavior of dogpile is not altered."""
        spy_single_cache = mocker.spy(self, "single_cache")
        spy_multi_cache = mocker.spy(self, "multi_cache")

        single_cache = region.cache_on_arguments()(self.single_cache)
        multi_cache = region.cache_multi_on_arguments()(self.multi_cache)

        assert 2 == single_cache(1)
        spy_single_cache.assert_called_once_with(1)

        # It's now cached - shouldn't need to call the inner function.
        spy_single_cache.reset_mock()
        assert 2 == single_cache(1)
        assert spy_single_cache.call_count == 0

        assert [6, 8] == multi_cache(3, 4)
        spy_multi_cache.assert_called_once_with(3, 4)

        # Partial hit. Only the "new" key should be passed to the inner function.
        spy_multi_cache.reset_mock()
        assert [6, 10] == multi_cache(3, 5)
        spy_multi_cache.assert_called_once_with(5)

        # Full hit. No call to inner function.
        spy_multi_cache.reset_mock()
        assert [6, 10] == multi_cache(3, 5)
        assert spy_single_cache.call_count == 0


def test_get_or_create_kwarg_only(region):
    """
    When get_or_create is called with only kwargs
        The arguments should be handled correctly
    """
    assert region.get_or_create(key="key", creator=lambda: 3) == 3
    assert region.get_or_create_multi(keys="keys", creator=lambda *args: [1, 2])
