# stdlib
import sqlite3
import time

# project
import ddtrace
from ddtrace import Pin
from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
from ddtrace.contrib.sqlite3 import connection_factory
from ddtrace.contrib.sqlite3.patch import patch, unpatch, TracedSQLiteCursor
from ddtrace.ext import errors

# testing
from tests.opentracer.utils import init_tracer
from ...base import BaseTracerTestCase
from ...utils import assert_is_measured, assert_is_not_measured


class TestSQLite(BaseTracerTestCase):
    def setUp(self):
        super(TestSQLite, self).setUp()
        patch()

    def tearDown(self):
        unpatch()
        super(TestSQLite, self).tearDown()

    def test_backwards_compat(self):
        # a small test to ensure that if the previous interface is used
        # things still work
        factory = connection_factory(self.tracer, service='my_db_service')
        conn = sqlite3.connect(':memory:', factory=factory)
        q = 'select * from sqlite_master'
        cursor = conn.execute(q)
        self.assertIsInstance(cursor, TracedSQLiteCursor)
        assert not cursor.fetchall()
        assert not self.spans

    def test_service_info(self):
        backup_tracer = ddtrace.tracer
        ddtrace.tracer = self.tracer

        sqlite3.connect(':memory:')

        services = self.tracer.writer.pop_services()
        self.assertEqual(services, {})

        ddtrace.tracer = backup_tracer

    def test_sqlite(self):
        # ensure we can trace multiple services without stomping
        services = ['db', 'another']
        for service in services:
            db = sqlite3.connect(':memory:')
            pin = Pin.get_from(db)
            assert pin
            pin.clone(
                service=service,
                tracer=self.tracer).onto(db)

            # Ensure we can run a query and it's correctly traced
            q = 'select * from sqlite_master'
            start = time.time()
            cursor = db.execute(q)
            self.assertIsInstance(cursor, TracedSQLiteCursor)
            rows = cursor.fetchall()
            end = time.time()
            assert not rows
            self.assert_structure(
                dict(name='sqlite.query', span_type='sql', resource=q, service=service, error=0),
            )
            root = self.get_root_span()
            assert_is_measured(root)
            self.assertIsNone(root.get_tag('sql.query'))
            assert start <= root.start <= end
            assert root.duration <= end - start
            self.reset()

            # run a query with an error and ensure all is well
            q = 'select * from some_non_existant_table'
            try:
                db.execute(q)
            except Exception:
                pass
            else:
                assert 0, 'should have an error'

            self.assert_structure(
                dict(name='sqlite.query', span_type='sql', resource=q, service=service, error=1),
            )
            root = self.get_root_span()
            assert_is_measured(root)
            self.assertIsNone(root.get_tag('sql.query'))
            self.assertIsNotNone(root.get_tag(errors.ERROR_STACK))
            self.assertIn('OperationalError', root.get_tag(errors.ERROR_TYPE))
            self.assertIn('no such table', root.get_tag(errors.ERROR_MSG))
            self.reset()

    def test_sqlite_fetchall_is_traced(self):
        q = 'select * from sqlite_master'

        # Not traced by default
        connection = self._given_a_traced_connection(self.tracer)
        cursor = connection.execute(q)
        cursor.fetchall()
        self.assert_structure(dict(name='sqlite.query', resource=q))
        self.reset()

        with self.override_config('dbapi2', dict(trace_fetch_methods=True)):
            connection = self._given_a_traced_connection(self.tracer)
            cursor = connection.execute(q)
            cursor.fetchall()

            # We have two spans side by side
            query_span, fetchall_span = self.get_root_spans()

            # Assert query
            query_span.assert_structure(dict(name='sqlite.query', resource=q))
            assert_is_measured(query_span)

            # Assert fetchall
            fetchall_span.assert_structure(dict(name='sqlite.query.fetchall', resource=q, span_type='sql', error=0))
            assert_is_not_measured(fetchall_span)
            self.assertIsNone(fetchall_span.get_tag('sql.query'))

    def test_sqlite_fetchone_is_traced(self):
        q = 'select * from sqlite_master'

        # Not traced by default
        connection = self._given_a_traced_connection(self.tracer)
        cursor = connection.execute(q)
        cursor.fetchone()
        self.assert_structure(dict(name='sqlite.query', resource=q))
        self.reset()

        with self.override_config('dbapi2', dict(trace_fetch_methods=True)):
            connection = self._given_a_traced_connection(self.tracer)
            cursor = connection.execute(q)
            cursor.fetchone()

            # We have two spans side by side
            query_span, fetchone_span = self.get_root_spans()

            # Assert query
            assert_is_measured(query_span)
            query_span.assert_structure(dict(name='sqlite.query', resource=q))

            # Assert fetchone
            assert_is_not_measured(fetchone_span)
            fetchone_span.assert_structure(
                dict(
                    name='sqlite.query.fetchone',
                    resource=q,
                    span_type='sql',
                    error=0,
                ),
            )
            self.assertIsNone(fetchone_span.get_tag('sql.query'))

    def test_sqlite_fetchmany_is_traced(self):
        q = 'select * from sqlite_master'

        # Not traced by default
        connection = self._given_a_traced_connection(self.tracer)
        cursor = connection.execute(q)
        cursor.fetchmany(123)
        self.assert_structure(dict(name='sqlite.query', resource=q))
        self.reset()

        with self.override_config('dbapi2', dict(trace_fetch_methods=True)):
            connection = self._given_a_traced_connection(self.tracer)
            cursor = connection.execute(q)
            cursor.fetchmany(123)

            # We have two spans side by side
            query_span, fetchmany_span = self.get_root_spans()

            # Assert query
            assert_is_measured(query_span)
            query_span.assert_structure(dict(name='sqlite.query', resource=q))

            # Assert fetchmany
            assert_is_not_measured(fetchmany_span)
            fetchmany_span.assert_structure(
                dict(
                    name='sqlite.query.fetchmany',
                    resource=q,
                    span_type='sql',
                    error=0,
                    metrics={'db.fetch.size': 123},
                ),
            )
            self.assertIsNone(fetchmany_span.get_tag('sql.query'))

    def test_sqlite_ot(self):
        """Ensure sqlite works with the opentracer."""
        ot_tracer = init_tracer('sqlite_svc', self.tracer)

        # Ensure we can run a query and it's correctly traced
        q = 'select * from sqlite_master'
        with ot_tracer.start_active_span('sqlite_op'):
            db = sqlite3.connect(':memory:')
            pin = Pin.get_from(db)
            assert pin
            pin.clone(tracer=self.tracer).onto(db)
            cursor = db.execute(q)
            rows = cursor.fetchall()
        assert not rows

        self.assert_structure(
            dict(name='sqlite_op', service='sqlite_svc'),
            (
                dict(name='sqlite.query', span_type='sql', resource=q, error=0),
            )
        )
        assert_is_measured(self.get_spans()[1])
        self.reset()

        with self.override_config('dbapi2', dict(trace_fetch_methods=True)):
            with ot_tracer.start_active_span('sqlite_op'):
                db = sqlite3.connect(':memory:')
                pin = Pin.get_from(db)
                assert pin
                pin.clone(tracer=self.tracer).onto(db)
                cursor = db.execute(q)
                rows = cursor.fetchall()
                assert not rows

            self.assert_structure(
                dict(name='sqlite_op', service='sqlite_svc'),
                (
                    dict(name='sqlite.query', span_type='sql', resource=q, error=0),
                    dict(name='sqlite.query.fetchall', span_type='sql', resource=q, error=0),
                ),
            )
            assert_is_measured(self.get_spans()[1])

    def test_commit(self):
        connection = self._given_a_traced_connection(self.tracer)
        connection.commit()
        self.assertEqual(len(self.spans), 1)
        span = self.spans[0]
        self.assertEqual(span.service, 'sqlite')
        self.assertEqual(span.name, 'sqlite.connection.commit')

    def test_rollback(self):
        connection = self._given_a_traced_connection(self.tracer)
        connection.rollback()
        self.assert_structure(
            dict(name='sqlite.connection.rollback', service='sqlite'),
        )

    def test_patch_unpatch(self):
        # Test patch idempotence
        patch()
        patch()

        db = sqlite3.connect(':memory:')
        pin = Pin.get_from(db)
        assert pin
        pin.clone(tracer=self.tracer).onto(db)
        db.cursor().execute('select \'blah\'').fetchall()

        self.assert_structure(
            dict(name='sqlite.query'),
        )
        self.reset()

        # Test unpatch
        unpatch()

        db = sqlite3.connect(':memory:')
        db.cursor().execute('select \'blah\'').fetchall()

        self.assert_has_no_spans()

        # Test patch again
        patch()

        db = sqlite3.connect(':memory:')
        pin = Pin.get_from(db)
        assert pin
        pin.clone(tracer=self.tracer).onto(db)
        db.cursor().execute('select \'blah\'').fetchall()

        self.assert_structure(
            dict(name='sqlite.query'),
        )

    def _given_a_traced_connection(self, tracer):
        db = sqlite3.connect(':memory:')
        Pin.get_from(db).clone(tracer=tracer).onto(db)
        return db

    def test_analytics_default(self):
        q = 'select * from sqlite_master'
        connection = self._given_a_traced_connection(self.tracer)
        cursor = connection.execute(q)
        cursor.fetchall()

        spans = self.get_spans()
        self.assertEqual(len(spans), 1)
        span = spans[0]
        self.assertIsNone(span.get_metric(ANALYTICS_SAMPLE_RATE_KEY))

    def test_analytics_with_rate(self):
        with self.override_config(
                'dbapi2',
                dict(analytics_enabled=True, analytics_sample_rate=0.5)
        ):
            q = 'select * from sqlite_master'
            connection = self._given_a_traced_connection(self.tracer)
            cursor = connection.execute(q)
            cursor.fetchall()

            spans = self.get_spans()
            self.assertEqual(len(spans), 1)
            span = spans[0]
            self.assertEqual(span.get_metric(ANALYTICS_SAMPLE_RATE_KEY), 0.5)

    def test_analytics_without_rate(self):
        with self.override_config(
                'dbapi2',
                dict(analytics_enabled=True)
        ):
            q = 'select * from sqlite_master'
            connection = self._given_a_traced_connection(self.tracer)
            cursor = connection.execute(q)
            cursor.fetchall()

            spans = self.get_spans()

            self.assertEqual(len(spans), 1)
            span = spans[0]
            self.assertEqual(span.get_metric(ANALYTICS_SAMPLE_RATE_KEY), 1.0)

    @BaseTracerTestCase.run_in_subprocess(env_overrides=dict(DD_SERVICE="mysvc"))
    def test_user_specified_service(self):
        """
        When a user specifies a service for the app
            The sqlite3 integration should not use it.
        """
        # Ensure that the service name was configured
        from ddtrace import config
        assert config.service == "mysvc"

        q = "select * from sqlite_master"
        connection = self._given_a_traced_connection(self.tracer)
        cursor = connection.execute(q)
        cursor.fetchall()

        spans = self.get_spans()

        self.assertEqual(len(spans), 1)
        span = spans[0]
        assert span.service != "mysvc"
