import threading

from tornado import httpclient
from tornado.testing import gen_test

from ddtrace.contrib.internal.tornado.patch import patch
from ddtrace.contrib.internal.tornado.patch import unpatch
from ddtrace.ext import http

from . import web
from .utils import TornadoTestCase
from .web.app import CustomDefaultHandler


class TestAsyncConcurrency(TornadoTestCase):
    """
    Ensure that application instrumentation doesn't break asynchronous concurrency.
    """

    @gen_test
    def test_concurrent_requests(self):
        REQUESTS_NUMBER = 25
        responses = []

        # the application must handle concurrent calls
        def make_requests():
            # use a blocking HTTP client (we're in another thread)
            http_client = httpclient.HTTPClient()
            url = self.get_url("/nested/")
            response = http_client.fetch(url)
            responses.append(response)
            assert 200 == response.code
            assert "OK" == response.body.decode("utf-8")
            # freeing file descriptors
            http_client.close()

        # blocking call executed in different threads
        threads = [threading.Thread(target=make_requests) for _ in range(REQUESTS_NUMBER)]
        for t in threads:
            t.start()

        while len(responses) < REQUESTS_NUMBER:
            yield web.compat.sleep(0.001)

        for t in threads:
            t.join()

        # the trace is created
        traces = self.pop_traces()
        assert REQUESTS_NUMBER == len(traces)
        assert 2 == len(traces[0])


class TestAppSafety(TornadoTestCase):
    """
    Ensure that the application patch has the proper safety guards.
    """

    def test_trace_unpatch(self):
        # the application must not be traced if unpatch() is called
        patch()
        unpatch()

        response = self.fetch("/success/")
        assert 200 == response.code

        traces = self.pop_traces()
        assert 0 == len(traces)

    def test_trace_unpatch_not_traced(self):
        # the untrace must be safe if the app is not traced
        unpatch()
        unpatch()

        response = self.fetch("/success/")
        assert 200 == response.code

        traces = self.pop_traces()
        assert 0 == len(traces)

    def test_trace_app_twice(self):
        # the application must not be traced multiple times
        patch()
        patch()

        response = self.fetch("/success/")
        assert 200 == response.code

        traces = self.pop_traces()
        assert 1 == len(traces)
        assert 1 == len(traces[0])

    def test_arbitrary_resource_querystring(self):
        # users inputs should not determine `span.resource` field
        full_url = "/success/?magic_number=42"
        response = self.fetch(full_url)
        assert 200 == response.code

        traces = self.pop_traces()
        assert 1 == len(traces)
        assert 1 == len(traces[0])

        request_span = traces[0][0]
        assert "tests.contrib.tornado.web.app.SuccessHandler" == request_span.resource
        assert self.get_url(full_url) == request_span.get_tag(http.URL)

    def test_arbitrary_resource_404(self):
        # users inputs should not determine `span.resource` field
        response = self.fetch("/does_not_exist/")
        assert 404 == response.code

        traces = self.pop_traces()
        assert 1 == len(traces)
        assert 1 == len(traces[0])

        request_span = traces[0][0]
        assert "tornado.web.ErrorHandler" == request_span.resource
        assert self.get_url("/does_not_exist/") == request_span.get_tag(http.URL)

    @gen_test
    def test_futures_without_context(self):
        # ensures that if futures propagation is available, an empty
        # context doesn't crash the system
        from .web.compat import ThreadPoolExecutor

        def job():
            with self.tracer.trace("job"):
                return 42

        executor = ThreadPoolExecutor(max_workers=3)
        yield executor.submit(job)

        traces = self.pop_traces()
        assert 1 == len(traces)
        assert 1 == len(traces[0])

        # this trace yields the execution of the thread
        span = traces[0][0]
        assert "job" == span.name


class TestCustomAppSafety(TornadoTestCase):
    """
    Ensure that the application patch has the proper safety guards,
    even for custom default handlers.
    """

    def get_settings(self):
        return {
            "default_handler_class": CustomDefaultHandler,
            "default_handler_args": dict(status_code=400),
        }

    def test_trace_unpatch(self):
        # the application must not be traced if unpatch() is called
        unpatch()

        response = self.fetch("/custom_handler/")
        assert 400 == response.code

        traces = self.pop_traces()
        assert 0 == len(traces)
