"""
noseperf.plugin
~~~~~~~~~~~~~~~

:copyright: (c) 2012 DISQUS
:license: Apache License 2.0, see LICENSE for more details.
"""
from __future__ import absolute_import

from datetime import datetime
import json
import os
import sys
import time

from nose.plugins.base import Plugin

from noseperf.testcases import PerformanceTest
from noseperf.util import PatchContext
from noseperf.wrappers import patch_cursor, PerformanceCacheWrapper, PerformanceRedisWrapper, \
  RedisPipelineHook


class PerformancePlugin(Plugin):
    """
    Nose performance plugin.

    This plugin is used with the PerformanceTest class. This plugin will
    profile each test and dump all of the raw data into a json file.

    For this test to be super helpful, pass in a fixture set generated by the
    ``loaddata`` command. Then run the tests as follows::

        nosetests /tests/performance/ --with-performance

    """
    score = -sys.maxint - 1  # Should be the absolute last thing ever to run since it transparently monitors the code.
    enabled = False

    def options(self, parser, env):
        parser.add_option("--with-performance", dest="enable_performance",
            default=False, action="store_true",
            help="Enable the performance plugin.")

        parser.add_option("--performance-json-file", dest="performance_file",
            default="performance.json", metavar="FILE",
            help="Save Results in file. Defaults to performance.json")

        parser.add_option("--performance-revision", dest="performance_revision",
            default=None, help="Revision name to identify this build")

        parser.add_option("--performance-schema", dest="performance_schema",
            default=None, help="Schema to identify this build with")

        parser.add_option("--disable-performance-selector", dest="disable_performance_selector",
            action='store_true', help="If disabled, will allow any test to run", default=False)

    def configure(self, options, conf):
        if not options.enable_performance:
            return

        self.enabled = True
        self.json_file = options.performance_file
        self.revision = options.performance_revision
        self.schema = options.performance_schema
        self.use_selector = not options.disable_performance_selector

    def wantClass(self, cls):
        """
        Only profile performance tests.
        """
        if not self.use_selector:
            return None
        return issubclass(cls, PerformanceTest)

    def begin(self):
        self.start_time = datetime.now()
        self.tests = []
        self.context_stack = []

    def patch_interfaces(self):
        self._sql_data = []
        self._cache_data = []
        self._redis_data = []

        try:
            from django.conf import settings
            if not settings.configured:
                raise ImportError
        except ImportError:
            pass
        else:
            self.patch_django_interfaces()

        try:
            __import__('redis')
        except ImportError:
            pass
        else:
            self.add_context(PatchContext('redis.client.StrictRedis.execute_command', PerformanceRedisWrapper(self._redis_data)))
            self.add_context(PatchContext('redis.client.BasePipeline.execute', RedisPipelineHook(self._redis_data)))

    def patch_django_interfaces(self):
        import django
        from django.conf import settings

        self.add_context(PatchContext('django.db.backends.BaseDatabaseWrapper.cursor', patch_cursor(self._sql_data)))

        cache_backends = set()
        if django.VERSION < (1, 3):
            backend = settings.CACHE_BACKEND.split(':', 1)[0]
            if '.' not in backend:
                backend = 'django.core.cache.backends.%s' % backend
            path = '%s.CacheClass' % backend
            cache_backends.add(path)

        else:
            for cache_config in settings.CACHES.itervalues():
                cache_backends.add(cache_config['BACKEND'])

        for path in cache_backends:
            for cmd in ('get', 'set', 'add', 'delete', 'get_many'):
                self.add_context(PatchContext('%s.%s' % (path, cmd), PerformanceCacheWrapper(self._cache_data, cmd)))

    def add_context(self, ctx):
        ctx.__enter__()
        self.context_stack.append(ctx)

    def clear_context(self):
        while self.context_stack:
            self.context_stack.pop().__exit__(None, None, None)

    def beforeTest(self, test):
        """
        Monkey patch the test to ensure setUp and tearDown happen outside
        of the scope of our monitoring hooks.
        """
        plugin = self

        cls = type(test.test)

        cls.__orig_setup = cls.setUp
        cls.__orig_teardown = cls.tearDown

        def setUp(self):
            self.__orig_setup()
            plugin.beginTracing(self)

        def tearDown(self):
            plugin.endTracing(self)
            self.__orig_teardown()

        setUp.__is_patched = True
        tearDown.__is_patched = True

        cls.setUp = setUp
        cls.tearDown = tearDown

        return test

    def afterTest(self, test):
        """
        Remove our monkey patches.
        """
        cls = type(test.test)

        cls.setUp = cls.__orig_setup
        cls.tearDown = cls.__orig_teardown

    def beginTracing(self, test):
        self.patch_interfaces()
        self.start = time.time()

    def endTracing(self, test):
        self.end = time.time()
        self.clear_context()

        if not hasattr(self, 'end'):
            return

        interfaces = {}

        data = {
            'id': test.id(),
            'doc': test._testMethodDoc,
            'duration': self.end - self.start,
            'interfaces': interfaces,
        }

        interfaces.update({
            'sql': self._sql_data,
            'cache': self._cache_data,
            'redis': self._redis_data,
        })

        self.tests.append(data)

    def report(self, stream):
        json_file = os.path.realpath(self.json_file)
        base_dir = os.path.dirname(json_file)
        if not os.path.exists(base_dir):
            os.makedirs(base_dir)

        # Dump the raw data to json
        with open(json_file, "w+") as f:
            data = {
                'time': self.start_time.isoformat(),
                'tests': self.tests,
                'revision': self.revision,
                'schema': self.schema,
            }
            json.dump(data, f, indent=4)
