# coding: utf-8
"""
Tests for traitlets.config.application.Application
"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

import json
import logging
import os
from io import StringIO
from unittest import TestCase

try:
    from unittest import mock
except ImportError:
    import mock

pjoin = os.path.join

from pytest import mark

from traitlets.config.configurable import Configurable
from traitlets.config.loader import Config
from traitlets.tests.utils import check_help_output, check_help_all_output

from traitlets.config.application import (
    Application
)

from ipython_genutils.tempdir import TemporaryDirectory
from traitlets.traitlets import (
    Bool, Unicode, Integer, List, Dict
)


class Foo(Configurable):

    i = Integer(0, help="The integer i.").tag(config=True)
    j = Integer(1, help="The integer j.").tag(config=True)
    name = Unicode(u'Brian', help="First name.").tag(config=True)


class Bar(Configurable):

    b = Integer(0, help="The integer b.").tag(config=True)
    enabled = Bool(True, help="Enable bar.").tag(config=True)


class MyApp(Application):

    name = Unicode(u'myapp')
    running = Bool(False, help="Is the app running?").tag(config=True)
    classes = List([Bar, Foo])
    config_file = Unicode(u'', help="Load this config file").tag(config=True)

    warn_tpyo = Unicode(u"yes the name is wrong on purpose", config=True,
            help="Should print a warning if `MyApp.warn-typo=...` command is passed")

    aliases = Dict({
                    'i' : 'Foo.i',
                    'j' : 'Foo.j',
                    'name' : 'Foo.name',
                    'enabled' : 'Bar.enabled',
                    'log-level' : 'Application.log_level',
                })

    flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"),
                  disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"),
                  crit=({'Application' : {'log_level' : logging.CRITICAL}},
                        "set level=CRITICAL"),
            ))

    def init_foo(self):
        self.foo = Foo(parent=self)

    def init_bar(self):
        self.bar = Bar(parent=self)


class TestApplication(TestCase):

    def test_log(self):
        stream = StringIO()
        app = MyApp(log_level=logging.INFO)
        handler = logging.StreamHandler(stream)
        # trigger reconstruction of the log formatter
        app.log.handlers = [handler]
        app.log_format = "%(message)s"
        app.log_datefmt = "%Y-%m-%d %H:%M"
        app.log.info("hello")
        assert "hello" in stream.getvalue()

    def test_basic(self):
        app = MyApp()
        self.assertEqual(app.name, u'myapp')
        self.assertEqual(app.running, False)
        self.assertEqual(app.classes, [MyApp,Bar,Foo])
        self.assertEqual(app.config_file, u'')

    def test_config(self):
        app = MyApp()
        app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
        config = app.config
        self.assertEqual(config.Foo.i, 10)
        self.assertEqual(config.Foo.j, 10)
        self.assertEqual(config.Bar.enabled, False)
        self.assertEqual(config.MyApp.log_level,50)

    def test_config_propagation(self):
        app = MyApp()
        app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
        app.init_foo()
        app.init_bar()
        self.assertEqual(app.foo.i, 10)
        self.assertEqual(app.foo.j, 10)
        self.assertEqual(app.bar.enabled, False)

    def test_cli_priority(self):
        """Test that loading config files does not override CLI options"""
        name = 'config.py'
        class TestApp(Application):
            value = Unicode().tag(config=True)
            config_file_loaded = Bool().tag(config=True)
            aliases = {'v': 'TestApp.value'}
        app = TestApp()
        with TemporaryDirectory() as td:
            config_file = pjoin(td, name)
            with open(config_file, 'w') as f:
                f.writelines([
                    "c.TestApp.value = 'config file'\n",
                    "c.TestApp.config_file_loaded = True\n"
                ])

            app.parse_command_line(['--v=cli'])
            assert 'value' in app.config.TestApp
            assert app.config.TestApp.value == 'cli'
            assert app.value == 'cli'

            app.load_config_file(name, path=[td])
            assert app.config_file_loaded
            assert app.config.TestApp.value == 'cli'
            assert app.value == 'cli'

    def test_ipython_cli_priority(self):
        # this test is almost entirely redundant with above,
        # but we can keep it around in case of subtle issues creeping into
        # the exact sequence IPython follows.
        name = 'config.py'
        class TestApp(Application):
            value = Unicode().tag(config=True)
            config_file_loaded = Bool().tag(config=True)
            aliases = {'v': 'TestApp.value'}
        app = TestApp()
        with TemporaryDirectory() as td:
            config_file = pjoin(td, name)
            with open(config_file, 'w') as f:
                f.writelines([
                    "c.TestApp.value = 'config file'\n",
                    "c.TestApp.config_file_loaded = True\n"
                ])
            # follow IPython's config-loading sequence to ensure CLI priority is preserved
            app.parse_command_line(['--v=cli'])
            # this is where IPython makes a mistake:
            # it assumes app.config will not be modified,
            # and storing a reference is storing a copy
            cli_config = app.config
            assert 'value' in app.config.TestApp
            assert app.config.TestApp.value == 'cli'
            assert app.value == 'cli'
            app.load_config_file(name, path=[td])
            assert app.config_file_loaded
            # enforce cl-opts override config file opts:
            # this is where IPython makes a mistake: it assumes
            # that cl_config is a different object, but it isn't.
            app.update_config(cli_config)
            assert app.config.TestApp.value == 'cli'
            assert app.value == 'cli'

    def test_flags(self):
        app = MyApp()
        app.parse_command_line(["--disable"])
        app.init_bar()
        self.assertEqual(app.bar.enabled, False)
        app.parse_command_line(["--enable"])
        app.init_bar()
        self.assertEqual(app.bar.enabled, True)

    def test_aliases(self):
        app = MyApp()
        app.parse_command_line(["--i=5", "--j=10"])
        app.init_foo()
        self.assertEqual(app.foo.i, 5)
        app.init_foo()
        self.assertEqual(app.foo.j, 10)

    def test_flag_clobber(self):
        """test that setting flags doesn't clobber existing settings"""
        app = MyApp()
        app.parse_command_line(["--Bar.b=5", "--disable"])
        app.init_bar()
        self.assertEqual(app.bar.enabled, False)
        self.assertEqual(app.bar.b, 5)
        app.parse_command_line(["--enable", "--Bar.b=10"])
        app.init_bar()
        self.assertEqual(app.bar.enabled, True)
        self.assertEqual(app.bar.b, 10)

    def test_warn_autocorrect(self):
        stream = StringIO()
        app = MyApp(log_level=logging.INFO)
        app.log.handlers = [logging.StreamHandler(stream)]

        cfg = Config()
        cfg.MyApp.warn_typo = "WOOOO"
        app.config = cfg

        self.assertIn("warn_typo", stream.getvalue())
        self.assertIn("warn_tpyo", stream.getvalue())


    def test_flatten_flags(self):
        cfg = Config()
        cfg.MyApp.log_level = logging.WARN
        app = MyApp()
        app.update_config(cfg)
        self.assertEqual(app.log_level, logging.WARN)
        self.assertEqual(app.config.MyApp.log_level, logging.WARN)
        app.initialize(["--crit"])
        self.assertEqual(app.log_level, logging.CRITICAL)
        # this would be app.config.Application.log_level if it failed:
        self.assertEqual(app.config.MyApp.log_level, logging.CRITICAL)

    def test_flatten_aliases(self):
        cfg = Config()
        cfg.MyApp.log_level = logging.WARN
        app = MyApp()
        app.update_config(cfg)
        self.assertEqual(app.log_level, logging.WARN)
        self.assertEqual(app.config.MyApp.log_level, logging.WARN)
        app.initialize(["--log-level", "CRITICAL"])
        self.assertEqual(app.log_level, logging.CRITICAL)
        # this would be app.config.Application.log_level if it failed:
        self.assertEqual(app.config.MyApp.log_level, "CRITICAL")

    def test_extra_args(self):
        app = MyApp()
        app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args'])
        app.init_bar()
        self.assertEqual(app.bar.enabled, False)
        self.assertEqual(app.bar.b, 5)
        self.assertEqual(app.extra_args, ['extra', 'args'])
        app = MyApp()
        app.parse_command_line(["--Bar.b=5", '--', 'extra', "--disable", 'args'])
        app.init_bar()
        self.assertEqual(app.bar.enabled, True)
        self.assertEqual(app.bar.b, 5)
        self.assertEqual(app.extra_args, ['extra', '--disable', 'args'])

    def test_unicode_argv(self):
        app = MyApp()
        app.parse_command_line(['ünîcødé'])

    def test_document_config_option(self):
        app = MyApp()
        app.document_config_options()

    def test_generate_config_file(self):
        app = MyApp()
        assert 'The integer b.' in app.generate_config_file()

    def test_generate_config_file_classes_to_include(self):
        class NoTraits(Foo, Bar):
            pass

        app = MyApp()
        app.classes.append(NoTraits)
        conf_txt = app.generate_config_file()
        self.assertIn('The integer b.', conf_txt)
        self.assertIn('# Bar(Configurable)', conf_txt)
        self.assertIn('# Foo(Configurable)', conf_txt)
        self.assertNotIn('# Configurable', conf_txt)
        self.assertIn('# NoTraits(Foo,Bar)', conf_txt)

    def test_multi_file(self):
        app = MyApp()
        app.log = logging.getLogger()
        name = 'config.py'
        with TemporaryDirectory('_1') as td1:
            with open(pjoin(td1, name), 'w') as f1:
                f1.write("get_config().MyApp.Bar.b = 1")
            with TemporaryDirectory('_2') as td2:
                with open(pjoin(td2, name), 'w') as f2:
                    f2.write("get_config().MyApp.Bar.b = 2")
                app.load_config_file(name, path=[td2, td1])
                app.init_bar()
                self.assertEqual(app.bar.b, 2)
                app.load_config_file(name, path=[td1, td2])
                app.init_bar()
                self.assertEqual(app.bar.b, 1)

    @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
    def test_log_collisions(self):
        app = MyApp()
        app.log = logging.getLogger()
        app.log.setLevel(logging.INFO)
        name = 'config'
        with TemporaryDirectory('_1') as td:
            with open(pjoin(td, name + '.py'), 'w') as f:
                f.write("get_config().Bar.b = 1")
            with open(pjoin(td, name + '.json'), 'w') as f:
                json.dump({
                    'Bar': {
                        'b': 2
                    }
                }, f)
            with self.assertLogs(app.log, logging.WARNING) as captured:
                app.load_config_file(name, path=[td])
                app.init_bar()
        assert app.bar.b == 2
        output = '\n'.join(captured.output)
        assert 'Collision' in output
        assert '1 ignored, using 2' in output
        assert pjoin(td, name + '.py') in output
        assert pjoin(td, name + '.json') in output

    @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
    def test_log_bad_config(self):
        app = MyApp()
        app.log = logging.getLogger()
        name = 'config.py'
        with TemporaryDirectory() as td:
            with open(pjoin(td, name), 'w') as f:
                f.write("syntax error()")
            with self.assertLogs(app.log, logging.ERROR) as captured:
                app.load_config_file(name, path=[td])
        output = '\n'.join(captured.output)
        self.assertIn('SyntaxError', output)

    def test_raise_on_bad_config(self):
        app = MyApp()
        app.raise_config_file_errors = True
        app.log = logging.getLogger()
        name = 'config.py'
        with TemporaryDirectory() as td:
            with open(pjoin(td, name), 'w') as f:
                f.write("syntax error()")
            with self.assertRaises(SyntaxError):
                app.load_config_file(name, path=[td])

    def test_loaded_config_files(self):
        app = MyApp()
        app.log = logging.getLogger()
        name = 'config.py'
        with TemporaryDirectory('_1') as td1:
            config_file = pjoin(td1, name)
            with open(config_file, 'w') as f:
                f.writelines([
                    "c.MyApp.running = True\n"
                ])

            app.load_config_file(name, path=[td1])
            self.assertEqual(len(app.loaded_config_files), 1)
            self.assertEquals(app.loaded_config_files[0], config_file)

            app.start()
            self.assertEqual(app.running, True)

            # emulate an app that allows dynamic updates and update config file
            with open(config_file, 'w') as f:
                f.writelines([
                    "c.MyApp.running = False\n"
                ])

            # reload and verify update, and that loaded_configs was not increased
            app.load_config_file(name, path=[td1])
            self.assertEqual(len(app.loaded_config_files), 1)
            self.assertEqual(app.running, False)

            # Attempt to update, ensure error...
            with self.assertRaises(AttributeError):
                app.loaded_config_files = "/foo"

            # ensure it can't be udpated via append
            app.loaded_config_files.append("/bar")
            self.assertEqual(len(app.loaded_config_files), 1)

            # repeat to ensure no unexpected changes occurred
            app.load_config_file(name, path=[td1])
            self.assertEqual(len(app.loaded_config_files), 1)
            self.assertEqual(app.running, False)


class DeprecatedApp(Application):
    override_called = False
    parent_called = False
    def _config_changed(self, name, old, new):
        self.override_called = True
        def _capture(*args):
            self.parent_called = True
        with mock.patch.object(self.log, 'debug', _capture):
            super(DeprecatedApp, self)._config_changed(name, old, new)


def test_deprecated_notifier():
    app = DeprecatedApp()
    assert not app.override_called
    assert not app.parent_called
    app.config = Config({'A': {'b': 'c'}})
    assert app.override_called
    assert app.parent_called


def test_help_output():
    check_help_output(__name__)
    check_help_all_output(__name__)

if __name__ == '__main__':
    # for test_help_output:
    MyApp.launch_instance()