# encoding: utf-8

from itertools import islice
from six.moves import range
import collections
import inspect
import os
import shutil
import socket
import sys
import tempfile

from bpython._py3compat import py3
from bpython import config, repl, cli, autocomplete
from bpython.test import MagicIterMock, mock, FixLanguageTestCase as TestCase
from bpython.test import unittest, TEST_CONFIG


pypy = 'PyPy' in sys.version


def setup_config(conf):
    config_struct = config.Struct()
    config.loadini(config_struct, TEST_CONFIG)
    if conf is not None and 'autocomplete_mode' in conf:
        config_struct.autocomplete_mode = conf['autocomplete_mode']
    return config_struct


class FakeHistory(repl.History):

    def __init__(self):
        pass

    def reset(self):
        pass


class FakeRepl(repl.Repl):
    def __init__(self, conf=None):
        repl.Repl.__init__(self, repl.Interpreter(), setup_config(conf))
        self.current_line = ""
        self.cursor_offset = 0


class FakeCliRepl(cli.CLIRepl, FakeRepl):
    def __init__(self):
        self.s = ''
        self.cpos = 0
        self.rl_history = FakeHistory()


class TestMatchesIterator(unittest.TestCase):

    def setUp(self):
        self.matches = ['bobby', 'bobbies', 'bobberina']
        self.matches_iterator = repl.MatchesIterator()
        self.matches_iterator.current_word = 'bob'
        self.matches_iterator.orig_line = 'bob'
        self.matches_iterator.orig_cursor_offset = len('bob')
        self.matches_iterator.matches = self.matches

    def test_next(self):
        self.assertEqual(next(self.matches_iterator), self.matches[0])

        for x in range(len(self.matches) - 1):
            next(self.matches_iterator)

        self.assertEqual(next(self.matches_iterator), self.matches[0])
        self.assertEqual(next(self.matches_iterator), self.matches[1])
        self.assertNotEqual(next(self.matches_iterator), self.matches[1])

    def test_previous(self):
        self.assertEqual(self.matches_iterator.previous(), self.matches[2])

        for x in range(len(self.matches) - 1):
            self.matches_iterator.previous()

        self.assertNotEqual(self.matches_iterator.previous(), self.matches[0])
        self.assertEqual(self.matches_iterator.previous(), self.matches[1])
        self.assertEqual(self.matches_iterator.previous(), self.matches[0])

    def test_nonzero(self):
        """self.matches_iterator should be False at start,
        then True once we active a match.
        """
        self.assertFalse(self.matches_iterator)
        next(self.matches_iterator)
        self.assertTrue(self.matches_iterator)

    def test_iter(self):
        slice = islice(self.matches_iterator, 0, 9)
        self.assertEqual(list(slice), self.matches * 3)

    def test_current(self):
        with self.assertRaises(ValueError):
            self.matches_iterator.current()
        next(self.matches_iterator)
        self.assertEqual(self.matches_iterator.current(), self.matches[0])

    def test_update(self):
        slice = islice(self.matches_iterator, 0, 3)
        self.assertEqual(list(slice), self.matches)

        newmatches = ['string', 'str', 'set']
        completer = mock.Mock()
        completer.locate.return_value = (0, 1, 's')
        self.matches_iterator.update(1, 's', newmatches, completer)

        newslice = islice(newmatches, 0, 3)
        self.assertNotEqual(list(slice), self.matches)
        self.assertEqual(list(newslice), newmatches)

    def test_cur_line(self):
        completer = mock.Mock()
        completer.locate.return_value = (
            0,
            self.matches_iterator.orig_cursor_offset,
            self.matches_iterator.orig_line)
        self.matches_iterator.completer = completer

        with self.assertRaises(ValueError):
            self.matches_iterator.cur_line()

        self.assertEqual(next(self.matches_iterator), self.matches[0])
        self.assertEqual(self.matches_iterator.cur_line(),
                         (len(self.matches[0]), self.matches[0]))

    def test_is_cseq(self):
        self.assertTrue(self.matches_iterator.is_cseq())


class TestArgspec(unittest.TestCase):
    def setUp(self):
        self.repl = FakeRepl()
        self.repl.push("def spam(a, b, c):\n", False)
        self.repl.push("    pass\n", False)
        self.repl.push("\n", False)
        self.repl.push("class Spam(object):\n", False)
        self.repl.push("    def spam(self, a, b, c):\n", False)
        self.repl.push("        pass\n", False)
        self.repl.push("\n", False)
        self.repl.push("class SpammitySpam(object):\n", False)
        self.repl.push("    def __init__(self, a, b, c):\n", False)
        self.repl.push("        pass\n", False)
        self.repl.push("\n", False)
        self.repl.push("class WonderfulSpam(object):\n", False)
        self.repl.push("    def __new__(self, a, b, c):\n", False)
        self.repl.push("        pass\n", False)
        self.repl.push("\n", False)
        self.repl.push("o = Spam()\n", False)
        self.repl.push("\n", False)

    def set_input_line(self, line):
        """Set current input line of the test REPL."""
        self.repl.current_line = line
        self.repl.cursor_offset = len(line)

    def test_func_name(self):
        for (line, expected_name) in [("spam(", "spam"),
                                      ("spam(map([]", "map"),
                                      ("spam((), ", "spam")]:
            self.set_input_line(line)
            self.assertTrue(self.repl.get_args())
            self.assertEqual(self.repl.current_func.__name__, expected_name)

    def test_func_name_method_issue_479(self):
        for (line, expected_name) in [("o.spam(", "spam"),
                                      ("o.spam(map([]", "map"),
                                      ("o.spam((), ", "spam")]:
            self.set_input_line(line)
            self.assertTrue(self.repl.get_args())
            self.assertEqual(self.repl.current_func.__name__, expected_name)

    def test_syntax_error_parens(self):
        for line in ["spam(]", "spam([)", "spam())"]:
            self.set_input_line(line)
            # Should not explode
            self.repl.get_args()

    def test_kw_arg_position(self):
        self.set_input_line("spam(a=0")
        self.assertTrue(self.repl.get_args())
        self.assertEqual(self.repl.arg_pos, "a")

        self.set_input_line("spam(1, b=1")
        self.assertTrue(self.repl.get_args())
        self.assertEqual(self.repl.arg_pos, "b")

        self.set_input_line("spam(1, c=2")
        self.assertTrue(self.repl.get_args())
        self.assertEqual(self.repl.arg_pos, "c")

    def test_lambda_position(self):
        self.set_input_line("spam(lambda a, b: 1, ")
        self.assertTrue(self.repl.get_args())
        self.assertTrue(self.repl.funcprops)
        # Argument position
        self.assertEqual(self.repl.arg_pos, 1)

    def test_issue127(self):
        self.set_input_line("x=range(")
        self.assertTrue(self.repl.get_args())
        self.assertEqual(self.repl.current_func.__name__, "range")

        self.set_input_line("{x:range(")
        self.assertTrue(self.repl.get_args())
        self.assertEqual(self.repl.current_func.__name__, "range")

        self.set_input_line("foo(1, 2, x,range(")
        self.assertEqual(self.repl.current_func.__name__, "range")

        self.set_input_line("(x,range(")
        self.assertEqual(self.repl.current_func.__name__, "range")

    def test_nonexistent_name(self):
        self.set_input_line("spamspamspam(")
        self.assertFalse(self.repl.get_args())

    def test_issue572(self):
        self.set_input_line("SpammitySpam(")
        self.assertTrue(self.repl.get_args())

        self.set_input_line("WonderfulSpam(")
        self.assertTrue(self.repl.get_args())

    @unittest.skipIf(pypy, 'pypy pydoc doesn\'t have this')
    def test_issue583(self):
        self.repl = FakeRepl()
        self.repl.push("a = 1.2\n", False)
        self.set_input_line("a.is_integer(")
        self.repl.set_docstring()
        self.assertIsNot(self.repl.docstring, None)

    def test_methods_of_expressions(self):
        self.set_input_line("'a'.capitalize(")
        self.assertTrue(self.repl.get_args())

        self.set_input_line("(1 + 1.1).as_integer_ratio(")
        self.assertTrue(self.repl.get_args())


class TestArgspecInternal(unittest.TestCase):
    def test_function_expressions(self):
        te = self.assertTupleEqual
        fa = lambda line: repl.Repl._funcname_and_argnum(line)
        for line, (func, argnum) in [
            ('spam(', ('spam', 0)),
            ('spam((), ', ('spam', 1)),
            ('spam.eggs((), ', ('spam.eggs', 1)),
            ('spam[abc].eggs((), ', ('spam[abc].eggs', 1)),
            ('spam[0].eggs((), ', ('spam[0].eggs', 1)),
            ('spam[a + b]eggs((), ', ('spam[a + b]eggs', 1)),
            ('spam().eggs((), ', ('spam().eggs', 1)),
            ('spam(1, 2).eggs((), ', ('spam(1, 2).eggs', 1)),
            ('spam(1, f(1)).eggs((), ', ('spam(1, f(1)).eggs', 1)),
            ('[0].eggs((), ', ('[0].eggs', 1)),
            ('[0][0]((), {}).eggs((), ', ('[0][0]((), {}).eggs', 1)),
            ('a + spam[0].eggs((), ', ('spam[0].eggs', 1)),
            ("spam(", ("spam", 0)),
            ("spam(map([]", ("map", 0)),
            ("spam((), ", ("spam", 1))
                ]:
            te(fa(line), (func, argnum))


class TestGetSource(unittest.TestCase):
    def setUp(self):
        self.repl = FakeRepl()

    def set_input_line(self, line):
        """Set current input line of the test REPL."""
        self.repl.current_line = line
        self.repl.cursor_offset = len(line)

    def assert_get_source_error_for_current_function(self, func, msg):
        self.repl.current_func = func
        with self.assertRaises(repl.SourceNotFound):
            self.repl.get_source_of_current_name()
        try:
            self.repl.get_source_of_current_name()
        except repl.SourceNotFound as e:
            self.assertEqual(e.args[0], msg)
        else:
            self.fail("Should have raised SourceNotFound")

    def test_current_function(self):
        self.set_input_line('INPUTLINE')
        self.repl.current_func = inspect.getsource
        self.assertIn("text of the source code",
                      self.repl.get_source_of_current_name())

        self.assert_get_source_error_for_current_function(
            [], "No source code found for INPUTLINE")

        self.assert_get_source_error_for_current_function(
            list.pop, "No source code found for INPUTLINE")

    @unittest.skipIf(pypy, 'different errors for PyPy')
    def test_current_function_cpython(self):
        self.set_input_line('INPUTLINE')
        self.assert_get_source_error_for_current_function(
            collections.defaultdict.copy, "No source code found for INPUTLINE")
        self.assert_get_source_error_for_current_function(
            collections.defaultdict, "could not find class definition")

    def test_current_line(self):
        self.repl.interp.locals['a'] = socket.socket
        self.set_input_line('a')
        self.assertIn('dup(self)', self.repl.get_source_of_current_name())

# TODO add tests for various failures without using current function


class TestEditConfig(TestCase):
    def setUp(self):
        self.repl = FakeRepl()
        self.repl.interact.confirm = lambda msg: True
        self.repl.interact.notify = lambda msg: None
        self.repl.config.editor = 'true'

    def test_create_config(self):
        tmp_dir = tempfile.mkdtemp()
        try:
            config_path = os.path.join(tmp_dir, 'newdir', 'config')
            self.repl.config.config_path = config_path
            self.repl.edit_config()
            self.assertTrue(os.path.exists(config_path))
        finally:
            shutil.rmtree(tmp_dir)
            self.assertFalse(os.path.exists(config_path))


class TestRepl(unittest.TestCase):

    def set_input_line(self, line):
        """Set current input line of the test REPL."""
        self.repl.current_line = line
        self.repl.cursor_offset = len(line)

    def setUp(self):
        self.repl = FakeRepl()

    def test_current_string(self):
        self.set_input_line('a = "2"')
        # TODO factor cpos out of repl.Repl
        self.repl.cpos = 0
        self.assertEqual(self.repl.current_string(), '"2"')

        self.set_input_line('a = "2" + 2')
        self.assertEqual(self.repl.current_string(), '')

    def test_push(self):
        self.repl = FakeRepl()
        self.repl.push("foobar = 2")
        self.assertEqual(self.repl.interp.locals['foobar'], 2)

    # COMPLETE TESTS
    # 1. Global tests
    def test_simple_global_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.SIMPLE})
        self.set_input_line("d")

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches,
                         ['def', 'del', 'delattr(', 'dict(', 'dir(',
                          'divmod('])

    def test_substring_global_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.SUBSTRING})
        self.set_input_line("time")

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches,
                         ['RuntimeError(', 'RuntimeWarning('])

    def test_fuzzy_global_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.FUZZY})
        self.set_input_line("doc")

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches,
                         ['UnboundLocalError(', '__doc__'] if not py3 else
                         ['ChildProcessError(', 'UnboundLocalError(',
                          '__doc__'])

    # 2. Attribute tests
    def test_simple_attribute_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.SIMPLE})
        self.set_input_line("Foo.b")

        code = "class Foo():\n\tdef bar(self):\n\t\tpass\n"
        for line in code.split("\n"):
            self.repl.push(line)

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches, ['Foo.bar'])

    def test_substring_attribute_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.SUBSTRING})
        self.set_input_line("Foo.az")

        code = "class Foo():\n\tdef baz(self):\n\t\tpass\n"
        for line in code.split("\n"):
            self.repl.push(line)

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches, ['Foo.baz'])

    def test_fuzzy_attribute_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.FUZZY})
        self.set_input_line("Foo.br")

        code = "class Foo():\n\tdef bar(self):\n\t\tpass\n"
        for line in code.split("\n"):
            self.repl.push(line)

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches, ['Foo.bar'])

    # 3. Edge cases
    def test_updating_namespace_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.SIMPLE})
        self.set_input_line("foo")
        self.repl.push("foobar = 2")

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches, ['foobar'])

    def test_file_should_not_appear_in_complete(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.SIMPLE})
        self.set_input_line("_")
        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertNotIn('__file__', self.repl.matches_iter.matches)

    # 4. Parameter names
    def test_paremeter_name_completion(self):
        self.repl = FakeRepl({'autocomplete_mode': autocomplete.SIMPLE})
        self.set_input_line("foo(ab")

        code = "def foo(abc=1, abd=2, xyz=3):\n\tpass\n"
        for line in code.split("\n"):
            self.repl.push(line)

        self.assertTrue(self.repl.complete())
        self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
        self.assertEqual(self.repl.matches_iter.matches,
                         ['abc=', 'abd=', 'abs('])


class TestCliRepl(unittest.TestCase):

    def setUp(self):
        self.repl = FakeCliRepl()

    def test_atbol(self):
        self.assertTrue(self.repl.atbol())

        self.repl.s = "\t\t"
        self.assertTrue(self.repl.atbol())

        self.repl.s = "\t\tnot an empty line"
        self.assertFalse(self.repl.atbol())

    def test_addstr(self):
        self.repl.complete = mock.Mock(True)

        self.repl.s = "foo"
        self.repl.addstr("bar")
        self.assertEqual(self.repl.s, "foobar")

        self.repl.cpos = 3
        self.repl.addstr('buzz')
        self.assertEqual(self.repl.s, "foobuzzbar")


class TestCliReplTab(unittest.TestCase):

    def setUp(self):
        self.repl = FakeCliRepl()

    # 3 Types of tab complete
    def test_simple_tab_complete(self):
        self.repl.matches_iter = MagicIterMock()
        if py3:
            self.repl.matches_iter.__bool__.return_value = False
        else:
            self.repl.matches_iter.__nonzero__.return_value = False
        self.repl.complete = mock.Mock()
        self.repl.print_line = mock.Mock()
        self.repl.matches_iter.is_cseq.return_value = False
        self.repl.show_list = mock.Mock()
        self.repl.funcprops = mock.Mock()
        self.repl.arg_pos = mock.Mock()
        self.repl.matches_iter.cur_line.return_value = (None, "foobar")

        self.repl.s = "foo"
        self.repl.tab()
        self.assertTrue(self.repl.complete.called)
        self.repl.complete.assert_called_with(tab=True)
        self.assertEqual(self.repl.s, "foobar")

    @unittest.skip("disabled while non-simple completion is disabled")
    def test_substring_tab_complete(self):
        self.repl.s = "bar"
        self.repl.config.autocomplete_mode = autocomplete.FUZZY
        self.repl.tab()
        self.assertEqual(self.repl.s, "foobar")
        self.repl.tab()
        self.assertEqual(self.repl.s, "foofoobar")

    @unittest.skip("disabled while non-simple completion is disabled")
    def test_fuzzy_tab_complete(self):
        self.repl.s = "br"
        self.repl.config.autocomplete_mode = autocomplete.FUZZY
        self.repl.tab()
        self.assertEqual(self.repl.s, "foobar")

    # Edge Cases
    def test_normal_tab(self):
        """make sure pressing the tab key will
           still in some cases add a tab"""
        self.repl.s = ""
        self.repl.config = mock.Mock()
        self.repl.config.tab_length = 4
        self.repl.complete = mock.Mock()
        self.repl.print_line = mock.Mock()
        self.repl.tab()
        self.assertEqual(self.repl.s, "    ")

    def test_back_parameter(self):
        self.repl.matches_iter = mock.Mock()
        self.repl.matches_iter.matches = True
        self.repl.matches_iter.previous.return_value = "previtem"
        self.repl.matches_iter.is_cseq.return_value = False
        self.repl.show_list = mock.Mock()
        self.repl.funcprops = mock.Mock()
        self.repl.arg_pos = mock.Mock()
        self.repl.matches_iter.cur_line.return_value = (None, "previtem")
        self.repl.print_line = mock.Mock()
        self.repl.s = "foo"
        self.repl.cpos = 0
        self.repl.tab(back=True)
        self.assertTrue(self.repl.matches_iter.previous.called)
        self.assertTrue(self.repl.s, "previtem")

    # Attribute Tests
    @unittest.skip("disabled while non-simple completion is disabled")
    def test_fuzzy_attribute_tab_complete(self):
        """Test fuzzy attribute with no text"""
        self.repl.s = "Foo."
        self.repl.config.autocomplete_mode = autocomplete.FUZZY

        self.repl.tab()
        self.assertEqual(self.repl.s, "Foo.foobar")

    @unittest.skip("disabled while non-simple completion is disabled")
    def test_fuzzy_attribute_tab_complete2(self):
        """Test fuzzy attribute with some text"""
        self.repl.s = "Foo.br"
        self.repl.config.autocomplete_mode = autocomplete.FUZZY

        self.repl.tab()
        self.assertEqual(self.repl.s, "Foo.foobar")

    # Expand Tests
    def test_simple_expand(self):
        self.repl.s = "f"
        self.cpos = 0
        self.repl.matches_iter = mock.Mock()
        self.repl.matches_iter.is_cseq.return_value = True
        self.repl.matches_iter.substitute_cseq.return_value = (3, "foo")
        self.repl.print_line = mock.Mock()
        self.repl.tab()
        self.assertEqual(self.repl.s, "foo")

    @unittest.skip("disabled while non-simple completion is disabled")
    def test_substring_expand_forward(self):
        self.repl.config.autocomplete_mode = autocomplete.SUBSTRING
        self.repl.s = "ba"
        self.repl.tab()
        self.assertEqual(self.repl.s, "bar")

    @unittest.skip("disabled while non-simple completion is disabled")
    def test_fuzzy_expand(self):
        pass


if __name__ == '__main__':
    unittest.main()
