"""
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.base import FunctionWrapper


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
    version = 2

    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._calls = []

        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.patch_redis_interfaces()

    def patch_redis_interfaces(self):
        from noseperf.wrappers.redis import RedisWrapper, RedisPipelineWrapper

        self.add_context(PatchContext('redis.client.StrictRedis.execute_command', RedisWrapper(self._calls)))
        self.add_context(PatchContext('redis.client.BasePipeline.execute', RedisPipelineWrapper(self._calls)))

    def patch_django_interfaces(self):
        import django
        from django.conf import settings
        from noseperf.wrappers.django import DjangoTemplateWrapper, patch_cursor

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

        self.add_context(PatchContext('django.template.Template._render', DjangoTemplateWrapper(self._calls)))

        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), FunctionWrapper('cache', self._calls)))

    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,
            'calls': self._calls,
        }

        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 = {
                'version': self.version,
                'time': self.start_time.isoformat(),
                'tests': self.tests,
                'revision': self.revision,
                'schema': self.schema,
            }
            json.dump(data, f, indent=4)
