import os, sys

import unittest
import tempfile
import yaml
import neahtta
from flask import current_app

from fabric.colors import red, green, cyan, yellow, magenta

tests_module = os.path.join( os.getcwd()
                           , 'tests/'
                           )

projname = os.environ['NDS_CONFIG']\
             .partition('configs/')[2]\
             .partition('.config.yaml')[0]

project_test_file = os.path.join( tests_module
                                , projname + '.yaml'
                                )

class Failures(object):

    def __init__(self):
        self.failure_list = []

    def add(self, test, exception, _input, expect, result, msg=""):
        self.failure_list.append(
            (test, exception, _input, expect, result, msg)
        )

    def summarize(self):
        print self.failure_list

failuretrack = Failures()

class YamlTests(object):

    def __init__(self, path):
        self.filename = path

        with open(self.filename, 'r') as F:
            self.yaml = yaml.load(F)

    @property
    def request_tests(self):
        if not self.yaml.get('RequestTests'):
            return []
        cases = []
        for case_def in self.yaml.get('RequestTests'):
            # a little more processing will happen here
            uri = case_def.get('uri')
            for test in case_def.get('tests'):
                case = []
                case.append(uri)
                case.append(test)
                cases.append(case)
        return cases

    @property
    def morpholexicon_tests(self):
        if not self.yaml.get('MorpholexicalAnalysis'):
            return []
        cases = []
        for case_def in self.yaml.get('MorpholexicalAnalysis'):
            mlex = tuple(case_def.get('morpholexicon'))
            for test in case_def.get('tests'):
                case = []
                case.append(mlex)
                case.append(test)
                cases.append(case)
        return cases

    @property
    def morpholexical_generation_tests(self):
        if not self.yaml.get('MorpholexicalGeneration'):
            return []
        cases = []
        for case_def in self.yaml.get('MorpholexicalGeneration'):
            mlex = tuple(case_def.get('morpholexicon'))
            for test in case_def.get('tests'):
                case = []
                case.append(mlex)
                case.append(test)
                cases.append(case)
        return cases

    @property
    def lexicon_definition_tests(self):
        if not self.yaml.get('Lexicon'):
            return []

        cases = []
        for case_def in self.yaml.get('Lexicon'):
            mlex = tuple(case_def.get('lexicon'))
            for test in case_def.get('tests'):
                case = []
                case.append(mlex)
                case.append(test)
                cases.append(case)
        return cases

class NDSInstance(unittest.TestCase):
    """ Setup and teardown for NDS:
    """

    def context(self):
        return self.current_app.test_request_context('?')

    def setUp(self):
        _app = neahtta.app
        _app.debug = True
        _app.logger.removeHandler(_app.logger.smtp_handler)

        _app.caching_enabled = False
        self.app = _app.test_client()
        self.current_app = _app

        self.yamltests = YamlTests(project_test_file)

    def tearDown(self):
        # failuretrack.summarize()
        pass

# TODO: print useful text about process
class RequestTest(NDSInstance):
    """ These are defined in the .yaml file as:

            RequestTests:
             - etc.
    """

    def test_requests(self):
        from cssselect import GenericTranslator, SelectorError
        from lxml.html.soupparser import fromstring

        print "Running RequestTests tests..."
        print "(Expect to find certain strings in generated HTML)"

        for uri, case in self.yamltests.request_tests:
            uri_formatted = uri % case.get('uri_args')
            expected = case.get('expected_values')
            value_selector = case.get('value_selector')

            print "  uri:   " + cyan(uri_formatted)
            print "  select:  " + cyan(value_selector)
            print "  expect:  " + cyan(' '.join(expected))

            # Run the mock request
            response = self.app.get(uri_formatted)

            # Parse the tree by the test definition, and compare
            tree = fromstring(response.data)
            expr = GenericTranslator().css_to_xpath(value_selector)
            selected = [a.text for a in tree.xpath(expr)]
            print "  result:  " + magenta(' '.join(selected))

            for e in expected:
                passed = True
                try:
                    self.assertIn(e, selected)
                except Exception, exc:
                    passed = False

                if passed:
                    print "    " + green("PASSED") + ' (' + e + ')'
                else:
                    print "    " + red("FAILED") + ': ' + e
                    print "     > " + yellow("Values not found in selector")
                    failuretrack.add(
                        "RequestTest",
                        exc,
                        "%s [%s]" % (uri_formatted, value_selector),
                        ' '.join(expected),
                        ' '.join(selected),
                        "Values not found in selector",
                    )

            print ''

# TODO: print useful text about process
class MorpholexicalAnalysis(NDSInstance):
    """ These are defined in the .yaml file as:

            MorpholexicalAnalysis:
             - etc.
    """

    def test_morpholexical_analyses(self):

        skw = {
            'split_compounds': True,
            'non_compounds_only': False,
            'no_derivations': False,
            'return_raw_data': True,
        }

        # TODO: print useful text
        with self.context():

            print "Running MorpholexicalAnalysis tests..."
            print "(Expect lemmas for input forms)"

            m = self.current_app.morpholexicon
            for (source, target), case in self.yamltests.morpholexicon_tests:
                expect = case.get('expected_lemmas')

                res, raw_out, raw_err = m.lookup(case.get('input'),
                                                 source_lang=source,
                                                 target_lang=target, 
                                                 **skw)

                description = case.get('description', False)
                if description:
                    print "  " + yellow(description)

                print "  input:   " + cyan(case.get('input'))

                lemmas = [r.lemma for r in res.analyses]

                print "  expect:  " + cyan(' '.join(expect))
                print "  result:  " + magenta(' '.join(lemmas))

                for e in expect:
                    passed = True

                    try:
                        self.assertIn(e, lemmas)
                    except Exception, exc:
                        passed = False

                    if passed:
                        print "    " + green("PASSED") + " (" + e + ")"
                    else:
                        print "    " + red("FAILED") + ': ' + e
                        print "     > " + yellow(msg)
                        failuretrack.add(
                            "MorphologicalAnalysis",
                            exc,
                            case.get('input'),
                            ' '.join(expect),
                            ' '.join(lemmas),
                        )


                print ''

# TODO: failure summary ? how best to raise errors
class MorpholexicalGeneration(NDSInstance):
    """ These are defined in the .yaml file as:

            ParadigmGenerationThroughMorpholexicon:
             - etc.
    """

    def test_morpholexical_generation(self):

        skw = {
            'split_compounds': True,
            'non_compounds_only': False,
            'no_derivations': False,
            'return_raw_data': True,
        }

        def test_the_case(case, result, _input=''):

            expect = case.get('expected_forms', False)
            unexpect = case.get('unexpected_forms', False)


            if expect:
                test_func = self.assertIn
                msg =  "Form not generated."
                _in = expect
                print "  expect:  " + cyan(repr(expect))
                print "  result:  " + magenta(' '.join(result))

            if unexpect:
                test_func = self.assertNotIn
                msg =  "Generated form appeared that shouldn't."
                _in = unexpect

                print "  DONT expect: " + cyan(repr(expect))
                print "  result:      " + magenta(' '.join(result))

            if not expect and not unexpect:
                _in = []
                print yellow("  Not expecting any result.")
                print "  result:      " + magenta(' '.join(result))

            for e in _in:
                try:
                    test_func(e, result, msg)
                except Exception, exc:
                    print "    " + red("FAILED") + ': ' + e
                    # TODO: something else with this
                    # print debug
                    print "     > " + yellow(msg)
                    failuretrack.add(
                        "MorphologicalGeneration",
                        exc,
                        _input,
                        ' '.join(expect),
                        ' '.join(result),
                        msg,
                    )
                print "    " + green("PASSED") + " (" + e + ")"

        # TODO: print useful text
        with self.context():

            print "Running MorpholexicalGeneration tests..."
            print "(Expect generated forms for lemma from input forms)"

            m = self.current_app.morpholexicon
            for (source, target), case in self.yamltests.morpholexical_generation_tests:
                expect = case.get('expected_forms', False)
                unexpect = case.get('unexpected_forms', False)

                description = case.get('description', False)
                if description:
                    print "  " + yellow(description)

                print "  input:   " + cyan(case.get('input'))

                res, raw_out, raw_err = m.lookup(case.get('input'),
                                                 source_lang=source,
                                                 target_lang=target,
                                                 **skw)

                for node, morph_analyses in res:
                    if node is not None:
                        paradigm, debug = self.generate_paradigm(source, node, morph_analyses)
                        result = [g.form for g in paradigm]

                        test_the_case(case, result, _input=case.get('input'))
                        # TODO: set a success value? 

                print ''


    def generate_paradigm(self, lang, node, morph_analyses):

        current_app = self.current_app

        debug_text = ''
        
        _str_norm = 'string(normalize-space(%s))'

        morph = current_app.config.morphologies.get(lang, False)
        mlex = current_app.morpholexicon
        
        paradigm_from_file, paradigm_template = \
            mlex.paradigms.get_paradigm(lang, node, morph_analyses,
                                     return_template=True)

        generated_and_formatted = []

        l = node.xpath('./lg/l')[0]
        lemma = l.xpath(_str_norm % './text()')

        if paradigm_from_file:
            extra_log_info = {
                'template_path': paradigm_template,
            }
            form_tags = [_t.split('+')[1::] for _t in paradigm_from_file.splitlines()]
            # TODO: bool not iterable
            _generated, _stdout, _stderr = morph.generate_to_objs(lemma, form_tags, node, extra_log_info=extra_log_info, return_raw_data=True)
        else:
            # For pregenerated things
            _generated, _stdout, _stderr = morph.generate_to_objs(lemma, [], node, return_raw_data=True)

        debug_text += '\n\n' + _stdout + '\n\n'

        return _generated, debug_text

# TODO: failure summary ? how best to raise errors
class LexiconDefinitions(NDSInstance):
    """ These are defined in the .yaml file as:

            LexiconDefinitions:
             - etc.
    """

    def test_lexicon_definitions(self):

        skw = {
            'split_compounds': True,
            'non_compounds_only': False,
            'no_derivations': False,
            'return_raw_data': True,
        }

        _str_norm = 'string(normalize-space(%s))'

        def test_the_case(case, result):
            # test multiple results

            expect_in = case.get('expected_definitions', False)
            unexpect_in = case.get('unexpected_definitions', False)

            if expect_in:
                test_func = self.assertIn
                err_msg = "Could not find definition."
                _in = expect_in
                print "  expect:  " + cyan(repr(_in))
                print "  result:  " + magenta(repr(result))

            if unexpect_in:
                test_func = self.assertNotIn
                err_msg = "Unexpected definition."
                _in = unexpect_in
                print "  DONT expect: " + cyan(repr(_in))
                print "  result:      " + magenta(repr(result))

            if not expect_in and not unexpect_in:
                _in = []
                print yellow("  Not expecting any result.")
                print "  result:      " + magenta(repr(result))
                print "    " + green("PASSED")

            for _i in _in:
                passed = True
                try:
                    test_func(_i, result)
                except Exception, exc:
                    passed = False

                if passed:
                    print "    " + green("PASSED")
                else:
                    print "    " + red("FAILED") + ': ' + repr(_i)
                    if err_msg:
                        print "     > " + yellow(err_msg)
                    failuretrack.add(
                        "LexiconDefinitions",
                        exc,
                        _i,
                        ' '.join(_in),
                        ' '.join(result),
                        err_msg,
                    )

        with self.context():

            m = self.current_app.morpholexicon

            print "Running Lexicon tests..."

            for (source, target), case in self.yamltests.lexicon_definition_tests:
                translation_xpath = case.get('xpath', './mg/tg/t/text()')

                description = case.get('description', False)
                if description:
                    print "  " + yellow(description)

                print "  input:   " + cyan(case.get('input'))
                print "  xpath:   " + cyan(translation_xpath)

                res, raw_out, raw_err = m.lookup(case.get('input'),
                                                 source_lang=source,
                                                 target_lang=target,
                                                 **skw)


                successes = False
                for node, morph_analyses in res:
                    if node is None:
                        continue
                    else:
                        successes = True

                    result_defs = node.xpath(_str_norm % translation_xpath)

                    test_the_case(case, result_defs)
                    # TODO: set a success value? 

                if not successes:
                    err_msg = "No results found for input."

                    print "    " + red("FAILED") + ': ' + repr(case.get('input'))
                    print "     > " + yellow(err_msg)
                    failuretrack.add(
                        "LexiconDefinitions",
                        "",
                        case.get('input'),
                        "",
                        "",
                        err_msg,
                    )

                print ''


