# -*- coding: utf-8 -*-
"""
Tests for specific subclasses of Data
"""

from aiida.backends.djsite.db.testbase import AiidaTestCase

__copyright__ = u"Copyright (c), This file is part of the AiiDA platform. For further information please visit http://www.aiida.net/. All rights reserved."
__license__ = "MIT license, see LICENSE.txt file."
__version__ = "0.7.0.1"
__authors__ = "The AiiDA team."


### Here comparisons are defined #####################################
### Each comparison has to be a function with name
### _comparison_COMPARISONNAME
### and accepting three parameters:
### - testclass: the testing class, with the proper AssertXXX methods
### - the dbdata value, i.e. the value parsed by the test
### - comparisondata, the values specified by the user for comparison;
###   they typically contain a 'value', and possibly other keys for
###   more advanced keys
def _comparison_AlmostEqual(testclass, dbdata, comparisondata):
    """
    Compare two numbers (or a list of numbers) to check that
    they are all almost equal (within a default precision of 7 digits)
    """
    value = comparisondata['value']
    if isinstance(dbdata, (list, tuple)) and isinstance(value, (list, tuple)):
        testclass.assertEqual(len(dbdata), len(value))
        for i in range(0, len(dbdata)):
            testclass.assertAlmostEqual(dbdata[i], value[i])
    else:
        testclass.assertAlmostEqual(dbdata, value)


def _comparison_Equal(testclass, dbdata, comparisondata):
    """
    Compare two objects to see if they are equal
    """
    testclass.assertEqual(dbdata, comparisondata['value'])


def _comparison_LengthEqual(testclass, dbdata, comparisondata):
    """
    Check if the length of the object is equal to the value specified
    """
    testclass.assertEqual(len(dbdata), comparisondata['value'])


### End of comparison definition #####################################

def output_test(pk, testname, skip_uuids_from_inputs=[]):
    """
    This is the function that should be used to create a new test from an
    existing calculation.

    It is possible to simplify the file removing unwanted nodes.

    :param pk: PK of Calculation, used for test
    :param testname: the name of this test, used to create a new folder.
        The folder name will be of the form test_PLUGIN_TESTNAME,
        with PLUGIN substituted by the plugin name, with dots replaced by
        underscores. Testname can contain only digits, letters and underscores.
    :param skip_uuids_from_inputs: a list of UUIDs of input nodes to be
        skipped
    """
    import os
    import json

    from aiida.common.folders import Folder
    from aiida.orm import JobCalculation
    from aiida.orm.utils import load_node
    from aiida.orm.importexport import export_tree
    c = load_node(pk, parent_class=JobCalculation)
    outfolder = "test_{}_{}".format(
        c.get_parser_name().replace('.', '_'),
        testname)

    if not is_valid_folder_name(outfolder):
        raise ValueError("The testname is invalid; it can contain only "
                         "letters, digits or underscores")

    if os.path.exists(outfolder):
        raise ValueError("Out folder '{}' already exists".format(outfolder))

    inputs = []
    for node in c.get_inputs():
        if node.uuid not in skip_uuids_from_inputs:
            inputs.append(node.dbnode)

    folder = Folder(outfolder)
    to_export = [c.dbnode] + inputs
    try:
        to_export.append(c.out.retrieved.dbnode)
    except AttributeError:
        raise ValueError("No output retrieved node; without it, we cannot "
                         "test the parser!")
    export_tree(to_export, folder=folder,
                also_parents=False,
                also_calc_outputs=False)

    # Create an empty checks file
    with open(os.path.join(outfolder, '_aiida_checks.json'), 'w') as f:
        json.dump({}, f)

    for path, dirlist, filelist in os.walk(outfolder):
        if len(dirlist) == 0 and len(filelist) == 0:
            with open("{}/.gitignore".format(path), 'w') as f:
                f.write("# This is a placeholder file, used to make git "
                        "store an empty folder")
                f.flush()


def read_test(outfolder):
    """
    Read a test folder created by output_test.

    .. note:: This method should only be called in the testing
        environment, because it's importing data in the current
        database.
    """
    import os
    import importlib
    import json

    from aiida.common.exceptions import NotExistent
    from aiida.orm import JobCalculation
    from aiida.orm.utils import load_node
    from aiida.orm.importexport import import_data

    imported = import_data(outfolder,
                           ignore_unknown_nodes=True, silent=True)

    calc = None
    for _, pk in imported['aiida.backends.djsite.db.models.DbNode']['new']:
        c = load_node(pk)
        if issubclass(c.__class__, JobCalculation):
            calc = c
            break

    retrieved = calc.out.retrieved

    try:
        with open(os.path.join(outfolder, '_aiida_checks.json')) as f:
            tests = json.load(f)
    except IOError:
        raise ValueError("This test does not provide a check file!")
    except ValueError:
        raise ValueError("This test does provide a check file, but it cannot "
                         "be JSON-decoded!")

    mod_path = 'aiida.backends.djsite.db.subtests.parser_tests.{}'.format(
        os.path.split(outfolder)[1])

    skip_test = False
    try:
        m = importlib.import_module(mod_path)
        skip_test = m.skip_condition()
    except Exception:
        pass

    if skip_test:
        raise SkipTestException

    return calc, {'retrieved': retrieved}, tests


def is_valid_folder_name(name):
    """
    Return True if the string (that will be the folder name of each subtest)
    is a valid name for a test function: it should start with test_, and
    contain only letters, digits or underscores.
    """
    import string

    if not name.startswith('test_'):
        return False

    # Remove valid characters, see if anything remains
    bad_characters = name.translate(None, string.letters + string.digits + '_')
    if bad_characters:
        return False

    return True


class TestParsers(AiidaTestCase):
    """
    This class dynamically finds all tests in a given subfolder, and loads
    them as different tests.
    """
    # To have both the "default" error message from assertXXX, and the
    # msg specified by us
    longMessage = True

    @classmethod
    def return_base_test(cls, folder):
        from inspect import isfunction

        def base_test(self):
            try:
                calc, retrieved_nodes, tests = read_test(folder)
            except SkipTestException:
                return None
            Parser = calc.get_parserclass()
            if Parser is None:
                raise NotImplementedError
            else:
                parser = Parser(calc)
                successful, new_nodes_tuple = parser.parse_with_retrieved(
                    retrieved_nodes)
                self.assertTrue(successful, msg="The parser did not succeed")
                parsed_output_nodes = dict(new_nodes_tuple)

                # All main keys: name of nodes that should be present
                for test_node_name in tests:
                    try:
                        test_node = parsed_output_nodes[test_node_name]
                    except KeyError:
                        raise AssertionError("Output node '{}' expected but "
                                             "not found".format(test_node_name))

                    # Each subkey: attribute to check
                    # attr_test is the name of the attribute
                    for attr_test in tests[test_node_name]:
                        try:
                            dbdata = test_node.get_attr(attr_test)
                        except AttributeError:
                            raise AssertionError("Attribute '{}' not found in "
                                                 "parsed node '{}'".format(
                                attr_test,
                                test_node_name))
                        # Test data from the JSON
                        attr_test_listtests = tests[test_node_name][attr_test]
                        for test_number, attr_test_data in enumerate(
                                attr_test_listtests, start=1):
                            try:
                                comparison = attr_test_data.pop('comparison')
                            except KeyError as e:
                                raise ValueError(
                                    "Missing '{}' in the '{}' field "
                                    "in '{}' in "
                                    "the test file".format(e.message,
                                                           attr_test,
                                                           test_node_name))

                            try:
                                comparison_test = globals()[
                                    "_comparison_{}".format(comparison)]
                            except KeyError:
                                raise ValueError(
                                    "Unsupported '{}' comparison in "
                                    "the '{}' field in '{}' in "
                                    "the test file".format(comparison,
                                                           attr_test,
                                                           test_node_name))

                            if not isfunction(comparison_test):
                                raise TypeError(
                                    "Internal error: the variable _comparison_{} is not a "
                                    "function!".format(comparison))

                            try:
                                comparison_test(testclass=self, dbdata=dbdata,
                                                comparisondata=attr_test_data)
                            except Exception as e:
                                # I change both the message and the 'args'
                                # (apparently, args[0] is used by str(e))
                                # Probably, a 'better' way should be found to do this!
                                e.message = "Failed test #{} for {}->{}: {}".format(
                                    test_number, test_node_name, attr_test,
                                    e.message)
                                if e.args:
                                    e.args = tuple([
                                                       "Failed test #{} for {}->{}: {}".format(
                                                           test_number,
                                                           test_node_name,
                                                           attr_test,
                                                           e.args[0])]
                                                   + list(e.args[1:]))
                                raise e

        return base_test

    class __metaclass__(type):
        """
        Some python black magic to dynamically create tests
        """

        def __new__(cls, name, bases, attrs):
            import os

            newcls = type.__new__(cls, name, bases, attrs)

            file_folder = os.path.split(__file__)[0]
            parser_test_folder = os.path.join(file_folder, 'parser_tests')
            for f in os.listdir(parser_test_folder):
                absf = os.path.abspath(os.path.join(parser_test_folder, f))
                if is_valid_folder_name(f) and os.path.isdir(absf):
                    function_name = f
                    setattr(newcls, function_name,
                            newcls.return_base_test(absf))

            return newcls


class SkipTestException(Exception):
    pass
