import unittest
import os
import os.path
import re
import sys
import tempfile

from kogu import Kogu, KoguException
try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

class KoguTest(unittest.TestCase):

    def setUp(self):
        epochs = 44
        Kogu.verbose = True

    def tearDown(self):

        sys.stdout = sys.__stdout__
        sys.stdin = sys.__stdin__

        # clear away any previous parameters.json
        self._parameters_file_name()

    def _error_names(self, missing=True):
        a = ["_a4", "1a", "!a", "b$b", ":F", ",",
             "'", "1", "0.5", "-", "A B", "A-", 1, 0.5]
        b = ["", " ", None]
        if missing:
            a.extend(b)
        return a

    def _clean(self, s):
        s.truncate(0)
        s.seek(0)

    def _assert_warn(self, value, msg=None):
        self._assert_line_format("Warning! Kogu", value, msg)

    def _assert_metric_line(self, value, msg=None):
        regex = r"^#I[-]?\d: .*\n$"
        self.assertRegexpMatches(value, regex, msg)

    def _assert_line_format(self, linestart, value, msg=None):
        regex = r"^" + re.escape(linestart) + r": .*\n$"
        self.assertRegexpMatches(value, regex, msg)

    def test_register_comment(self):
        sys.stdout = stdout = StringIO()
        
        Kogu.comment("")
        self._assert_warn(stdout.getvalue())
        self._clean(stdout)
        
        Kogu.comment("This is comment")
        self._assert_line_format("#COMMENT", stdout.getvalue())
        self.assertTrue("This is comment" in stdout.getvalue())
        self._clean(stdout)

        Kogu.comment("This\r\nis\ncomment\r")
        self._assert_line_format("#COMMENT", stdout.getvalue())
        self.assertTrue("This is comment" in stdout.getvalue(),
                        "Got '%s'" % stdout.getvalue())
        self._clean(stdout)

    def test_register_plot_defaults(self):
        sys.stdout = stdout = StringIO()

        Kogu.plot("", "")
        self._assert_warn(stdout.getvalue())
        self._clean(stdout)

        Kogu.plot("line", "test")
        self._assert_line_format("#PLOT", stdout.getvalue())
        self.assertTrue("type='line' name='line' y_label='Y' metrics=['test']" in stdout.getvalue()
                        , stdout.getvalue())
        self._clean(stdout)

    def test_register_plot_metrics(self):
        sys.stdout = stdout = StringIO()

        # Expected fail cases
        # We allow empty metrics to be passed to the method in this case.
        for name in self._error_names(missing=False):
            Kogu.plot("line", name)
            self._assert_warn(
                stdout.getvalue(), "Metric '%s' should not be allowed" % name)
            self._clean(stdout)

        # Success cases
        # I can pass string
        Kogu.plot("line", "x")
        self._assert_line_format("#PLOT", stdout.getvalue())
        # use better regex to validate the metrics part
        self.assertRegexpMatches(stdout.getvalue(), r".*metrics=\['x'\].*")
        self._clean(stdout)

        # I can pass array
        Kogu.plot("line", ["x"])
        self._assert_line_format("#PLOT", stdout.getvalue())
        # use better regex to validate the metrics part
        self.assertRegexpMatches(stdout.getvalue(), r".*metrics=\['x'\].*")
        self._clean(stdout)

        # Array is reduced to set
        Kogu.plot("line", ["x", "x"])
        self._assert_line_format("#PLOT", stdout.getvalue())
        # use better regex to validate the metrics part
        self.assertRegexpMatches(stdout.getvalue(), r".*metrics=\['x'\].*")
        self._clean(stdout)

        Kogu.plot("line", ["x", "y"])
        self._assert_line_format("#PLOT", stdout.getvalue())
        self.assertRegexpMatches(
            stdout.getvalue(), r".*metrics=\[('x', 'y'|'y', 'x')\].*")
        self._clean(stdout)

        # I can pass set
        Kogu.plot("line", set(["x", "x", "y"]))
        self._assert_line_format("#PLOT", stdout.getvalue())
        # use better regex to validate the metrics part
        self.assertRegexpMatches(
            stdout.getvalue(), r".*metrics=\[('x', 'y'|'y', 'x')\].*")
        self._clean(stdout)

    def test_register_plot_settings(self):
        sys.stdout = stdout = StringIO()

        Kogu.plot("line", ['test'], "Test Plot", "Y-label")
        self._assert_line_format("#PLOT", stdout.getvalue())
        self.assertTrue(
            "type='line' name='Test Plot' y_label='Y-label' metrics=['test']" in stdout.getvalue())
        self._clean(stdout)

    def test_register_plot_settings_double_quotes(self):
        sys.stdout = stdout = StringIO()

        Kogu.plot("line", ['test'], "Test 'Plot", "Y-label")
        self._assert_line_format("#PLOT", stdout.getvalue())
        self.assertTrue(
            "type='line' name=\"Test 'Plot\" y_label='Y-label' metrics=['test']" in stdout.getvalue()
            , stdout.getvalue())
        self._clean(stdout)

    def test_send_file(self):
        sys.stdout = stdout = StringIO()

        Kogu.upload("")
        self._assert_warn(stdout.getvalue(), "parameters not passed")
        self._clean(stdout)

        path = "/non/existing/file"
        Kogu.upload(path)
        # self._assert_warn(stdout.getvalue(), "non-existing-file")
        self._assert_line_format("#UPLOAD", stdout.getvalue())
        self.assertTrue(path in stdout.getvalue())
        self._clean(stdout)

        with tempfile.NamedTemporaryFile() as fp:
            Kogu.upload(fp.name)
            self._assert_line_format("#UPLOAD", stdout.getvalue())
            self.assertTrue(fp.name in stdout.getvalue())

    def test_metrics_none(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics(None)
        self._assert_warn(stdout.getvalue(), "None is not a valid metrics dictionary")
        self._clean(stdout)

    def test_metrics_empty(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics({})
        self.assertFalse(stdout.getvalue(), "an empty metrics dictionary should not generate output")
        self._clean(stdout)

    def test_metrics_none_key_and_value(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics({None: None})
        self._assert_warn(stdout.getvalue(), "None is not a valid metrics key")
        self._clean(stdout)

    def test_metrics_none_key(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics({None: 3})
        self._assert_warn(stdout.getvalue(), "None is not a valid metrics key")
        self._clean(stdout)

    def test_metrics_none_iteration(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics({"x": 3}, None)
        self._assert_warn(stdout.getvalue(), "None is not a valid iteration value")
        self._clean(stdout)

    def test_metrics_string_iteration(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics({"x": 3}, "string")
        self._assert_warn(stdout.getvalue(), "a string is not a valid iteration value")
        self._clean(stdout)

    def test_metrics_float_iteration(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics({"x": 3}, 3.14)
        self._assert_warn(stdout.getvalue(), "a floating point number is not a valid iteration value")
        self._clean(stdout)

    def test_metrics_empty_object_iteration(self):
        sys.stdout = stdout = StringIO()
        Kogu.metrics({"x": 3}, {})
        self._assert_warn(stdout.getvalue(), "a dictionary is not a valid iteration value")
        self._clean(stdout)

    def test_metrics_invalid_key(self):
        sys.stdout = stdout = StringIO()
        for name in self._error_names():
            Kogu.metrics({name: "3"})
            self._assert_warn(stdout.getvalue(),
                "'%s' is not a valid metrics key identifier" % name)
            self._clean(stdout)

    def test_send_metric_validation(self):
        sys.stdout = stdout = StringIO()

        # Default iteration number
        Kogu.metrics({"A": "3"})
        self._assert_metric_line(stdout.getvalue())
        self.assertTrue("#I-1" in stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A='3'.*")
        self._clean(stdout)

        # Set iteration number
        Kogu.metrics({"A": "3"}, 3)
        self._assert_metric_line(stdout.getvalue())
        self.assertTrue("#I3" in stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A='3'.*")
        self._clean(stdout)

        # String values
        Kogu.metrics({"A": "3", "B": "ok"})
        self._assert_metric_line(stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A='3'.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B='ok'.*")
        self._clean(stdout)

        # Numeric values
        Kogu.metrics({"A": 3, "B": 6})
        self._assert_metric_line(stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A=3.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B=6.*")
        self._clean(stdout)

        # Strip whitespace in identifiers
        Kogu.metrics({" A ": 3})
        self._assert_metric_line(stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A=3.*")
        self._clean(stdout)

        # Ignore leading/trailing whitespace in identifiers
        Kogu.metrics({" B": 6, "B ": 9})
        self._assert_metric_line(stdout.getvalue())
        self.assertNotRegexpMatches(stdout.getvalue(), r".*B=6.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B=9.*")
        self._clean(stdout)

        # Case sensitivity
        Kogu.metrics({"b": 6, "B": 9})
        self._assert_metric_line(stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*b=6.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B=9.*")
        self._clean(stdout)

        # Floating point as value
        float_value = 1.0 / 3.0
        Kogu.metrics({"A": float_value})
        match = re.match(r".*A=(\d+.?\d+).*", stdout.getvalue())
        self.assertIsNotNone(match)
        self.assertEqual(1, len(match.groups()))
        self.assertAlmostEqual(float_value, float(match.group(1)))
        self._assert_metric_line(stdout.getvalue())

    def test_load_parameters_to_module_scope(self):

        params = '{"epochs":30}'
        sys.stdin = StringIO(params)
        sys.stdout = stdout = StringIO()

        Kogu.load_parameters()

        self.assertEqual(epochs, 30)

    def _parameters_file_name(self):
        json_path = os.path.join(sys.path[0], "parameters.json")
        try:
            os.remove(json_path)
        except OSError:
            pass
        return json_path

    def test_load_parameters_from_json_file(self):
        with open(self._parameters_file_name(), "w") as json_file:
            json_file.writelines('{"epochs":50}')
        sys.stdout = stdout = StringIO()
        Kogu.load_parameters()
        self.assertEqual(epochs, 50)
        self._clean(stdout)

    # Piped parameters overload parameters passed from
    # parameters json. Parameters available in parameters.json are
    # outputed via param lines
    def test_load_parameters_priority(self):
        with open(self._parameters_file_name(), "w") as json:
            json.writelines(u'{"epochs":50,"spaces":25}')

        params = '{"epochs":10,"spaces": 22,"third":3}'
        sys.stdin = StringIO(params)
        sys.stdout = stdout = StringIO()

        Kogu.load_parameters()

        self.assertEqual(epochs, 10)
        self.assertEqual(spaces, 22)
        self.assertEqual(third, 3)

        self._assert_line_format("#PARAM", stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*epochs=10.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*spaces=22.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*third=.*")

        self._clean(stdout)


    def test_invalid_parameters_json(self):
        with open(self._parameters_file_name(), "w") as json:
            json.writelines(u'epochs:55')

        sys.stdout = stdout = StringIO()

        self.assertRaises(KoguException, Kogu.load_parameters)


    def test_parameters(self):
        sys.stdout = stdout = StringIO()

        for name in self._error_names():
            Kogu.update_parameters({name: "3"})
            self._assert_warn(
                stdout.getvalue(), "%s should not be valid identifier" % name)
            self._clean(stdout)

        # String values
        Kogu.update_parameters({"A": "3", "B": "ok"})
        self._assert_line_format("#PARAM", stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A='3'.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B='ok'.*")
        self._clean(stdout)

        # Numeric values
        Kogu.update_parameters({"A": 3, "B": 6})
        self._assert_line_format("#PARAM", stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A=3.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B=6.*")
        self._clean(stdout)

        # Strip whitespace in identifiers
        Kogu.update_parameters({" A ": 3})
        self._assert_line_format("#PARAM", stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*A=3.*")
        self._clean(stdout)

        # Ignore leading/trailing whitespace in identifiers
        Kogu.update_parameters({" B": 6, "B ": 9})
        self._assert_line_format("#PARAM", stdout.getvalue())
        self.assertNotRegexpMatches(stdout.getvalue(), r".*B=6.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B=9.*")
        self._clean(stdout)

        # Case sensitivity
        Kogu.update_parameters({"b": 6, "B": 9})
        self._assert_line_format("#PARAM", stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*b=6.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*B=9.*")
        self._clean(stdout)

        # Floating point as value
        fl = 1.0 / 3.0
        Kogu.update_parameters({"A": fl})
        match = re.match(r".*A=(\d+.?\d+).*", stdout.getvalue())
        self.assertIsNotNone(match)
        self.assertEqual(1, len(match.groups()))
        self.assertAlmostEqual(fl, float(match.group(1)))
        self._assert_line_format("#PARAM", stdout.getvalue())
        self._clean(stdout)

        # Boolean parameter
        Kogu.update_parameters({"b": True, "a": False})
        self._assert_line_format("#PARAM", stdout.getvalue())
        self.assertRegexpMatches(stdout.getvalue(), r".*b=True.*")
        self.assertRegexpMatches(stdout.getvalue(), r".*a=False.*")
        self._clean(stdout)


if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    unittest.main(testRunner=runner)
