"""
Test Selection
--------------

Test selection is handled by a Selector. The test loader calls the
appropriate selector method for each object it encounters that it
thinks may be a test.
"""
import logging
import os
import unittest
from nose.config import Config
from nose.util import split_test_name, src, getfilename, getpackage, ispackage

log = logging.getLogger(__name__)

__all__ = ['Selector', 'defaultSelector', 'TestAddress']


# for efficiency and easier mocking
op_join = os.path.join
op_basename = os.path.basename
op_exists = os.path.exists
op_splitext = os.path.splitext
op_isabs = os.path.isabs
op_abspath = os.path.abspath


class Selector(object):
    """Core test selector. Examines test candidates and determines whether,
    given the specified configuration, the test candidate should be selected
    as a test.
    """
    def __init__(self, config):
        if config is None:
            config = Config()
        self.configure(config)

    def configure(self, config):
        self.config = config
        self.exclude = config.exclude
        self.ignoreFiles = config.ignoreFiles
        self.include = config.include
        self.plugins = config.plugins
        self.match = config.testMatch
        
    def matches(self, name):
        """Does the name match my requirements?

        To match, a name must match config.testMatch OR config.include
        and it must not match config.exclude
        """
        return ((self.match.search(name)
                 or (self.include and
                     [_f for _f in [inc.search(name) for inc in self.include] if _f]))
                and ((not self.exclude)
                     or not [_f for _f in [exc.search(name) for exc in self.exclude] if _f]
                 ))
    
    def wantClass(self, cls):
        """Is the class a wanted test class?

        A class must be a unittest.TestCase subclass, or match test name
        requirements. Classes that start with _ are always excluded.
        """
        declared = getattr(cls, '__test__', None)
        if declared is not None:
            wanted = declared
        else:
            wanted = (not cls.__name__.startswith('_')
                      and (issubclass(cls, unittest.TestCase)
                           or self.matches(cls.__name__)))
        
        plug_wants = self.plugins.wantClass(cls)        
        if plug_wants is not None:
            log.debug("Plugin setting selection of %s to %s", cls, plug_wants)
            wanted = plug_wants
        log.debug("wantClass %s? %s", cls, wanted)
        return wanted

    def wantDirectory(self, dirname):
        """Is the directory a wanted test directory?

        All package directories match, so long as they do not match exclude. 
        All other directories must match test requirements.
        """
        tail = op_basename(dirname)
        if ispackage(dirname):
            wanted = (not self.exclude
                      or not [_f for _f in [exc.search(tail) for exc in self.exclude] if _f])
        else:
            wanted = (self.matches(tail)
                      or (self.config.srcDirs
                          and tail in self.config.srcDirs))
        plug_wants = self.plugins.wantDirectory(dirname)
        if plug_wants is not None:
            log.debug("Plugin setting selection of %s to %s",
                      dirname, plug_wants)
            wanted = plug_wants
        log.debug("wantDirectory %s? %s", dirname, wanted)
        return wanted
    
    def wantFile(self, file):
        """Is the file a wanted test file?

        The file must be a python source file and match testMatch or
        include, and not match exclude. Files that match ignore are *never*
        wanted, regardless of plugin, testMatch, include or exclude settings.
        """
        # never, ever load files that match anything in ignore
        # (.* _* and *setup*.py by default)
        base = op_basename(file)
        ignore_matches = [ ignore_this for ignore_this in self.ignoreFiles
                           if ignore_this.search(base) ]
        if ignore_matches:
            log.debug('%s matches ignoreFiles pattern; skipped',
                      base) 
            return False
        if not self.config.includeExe and os.access(file, os.X_OK):
            log.info('%s is executable; skipped', file)
            return False
        dummy, ext = op_splitext(base)
        pysrc = ext == '.py'

        wanted = pysrc and self.matches(base) 
        plug_wants = self.plugins.wantFile(file)
        if plug_wants is not None:
            log.debug("plugin setting want %s to %s", file, plug_wants)
            wanted = plug_wants