# Copyright (c) 2013 Hesky Fisher
# See LICENSE.txt for details.

"""Unit tests for translation.py."""

from collections import namedtuple
import copy
import operator
import sys

from plover.steno_dictionary import StenoDictionary, StenoDictionaryCollection
from plover.translation import Translation, Translator, _State
from plover.translation import escape_translation, unescape_translation
from plover.steno import Stroke, normalize_steno

from plover_build_utils.testing import steno_to_stroke as stroke

from . import parametrize


if sys.platform.startswith('darwin'):
    BACK_STRING = '{#Alt_L(BackSpace)}{^}'
else:
    BACK_STRING = '{#Control_L(BackSpace)}{^}'


def test_no_translation():
    t = Translation([stroke('S'), stroke('T')], None)
    assert t.strokes == [stroke('S'), stroke('T')]
    assert t.rtfcre == ('S', 'T')
    assert t.english is None

def test_translation():
    t = Translation([stroke('S'), stroke('T')], 'translation')
    assert t.strokes == [stroke('S'), stroke('T')]
    assert t.rtfcre == ('S', 'T')
    assert t.english == 'translation'


class TestTranslatorStateSize:

    class FakeState(_State):

        def __init__(self):
            _State.__init__(self)
            self.restrict_calls = []

        def restrict_size(self, n):
            self.restrict_calls.append(n)

    def _check_size_call(self, size):
        assert self.s.restrict_calls[-1] == size

    def _check_no_size_call(self):
        assert self.s.restrict_calls == []

    def clear(self):
        self.s.restrict_calls = []

    def setup_method(self):
        self.t = Translator()
        self.s = type(self).FakeState()
        self.t._state = self.s
        self.d = StenoDictionary()
        self.dc = StenoDictionaryCollection([self.d])
        self.t.set_dictionary(self.dc)

    def test_dictionary_update_grows_size1(self):
        self.d[('S',)] = '1'
        self._check_size_call(1)

    def test_dictionary_update_grows_size4(self):
        self.d[('S', 'PT', '-Z', 'TOP')] = 'hi'
        self._check_size_call(4)

    def test_dictionary_update_no_grow(self):
        self.t.set_min_undo_length(4)
        self._check_size_call(4)
        self.clear()
        self.d[('S', 'T')] = 'nothing'
        self._check_size_call(4)

    def test_dictionary_update_shrink(self):
        self.d[('S', 'T', 'P', '-Z', '-D')] = '1'
        self._check_size_call(5)
        self.clear()
        self.d[('A', 'P')] = '2'
        self._check_no_size_call()
        del self.d[('S', 'T', 'P', '-Z', '-D')]
        self._check_size_call(2)

    def test_dictionary_update_no_shrink(self):
        self.t.set_min_undo_length(7)
        self.d[('S', 'T', 'P', '-Z', '-D')] = '1'
        del self.d[('S', 'T', 'P', '-Z', '-D')]
        self._check_size_call(7)

    def test_translation_calls_restrict(self):
        self.t.translate(stroke('S'))
        self._check_size_call(0)


def test_listeners():
    output1 = []
    def listener1(undo, do, prev):
        output1.append((undo, do, prev))

    output2 = []
    def listener2(undo, do, prev):
        output2.append((undo, do, prev))

    t = Translator()
    s = stroke('S')
    tr = Translation([s], None)
    expected_output = [([], [tr], [tr])]

    t.translate(s)

    t.add_listener(listener1)
    t.translate(s)
    assert output1 == expected_output

    del output1[:]
    t.add_listener(listener2)
    t.translate(s)
    assert output1 == expected_output
    assert output2 == expected_output

    del output1[:]
    del output2[:]
    t.add_listener(listener2)
    t.translate(s)
    assert output1 == expected_output
    assert output2 == expected_output

    del output1[:]
    del output2[:]
    t.remove_listener(listener1)
    t.translate(s)
    assert output1 == []
    assert output2 == expected_output

    del output1[:]
    del output2[:]
    t.remove_listener(listener2)
    t.translate(s)
    assert output1 == []
    assert output2 == []


def test_changing_state():
    output = []
    def listener(undo, do, prev):
        prev = list(prev) if prev else None
        output.append((undo, do, prev))

    d = StenoDictionary()
    d[('S', 'P')] = 'hi'
    dc = StenoDictionaryCollection([d])
    t = Translator()
    t.set_dictionary(dc)
    t.translate(stroke('T'))
    t.translate(stroke('S'))
    s = copy.deepcopy(t.get_state())

    t.add_listener(listener)

    expected = [([Translation([stroke('S')], None)],
                 [Translation([stroke('S'), stroke('P')], 'hi')],
                 [Translation([stroke('T')], None)])]
    t.translate(stroke('P'))
    assert output == expected

    del output[:]
    t.set_state(s)
    t.translate(stroke('P'))
    assert output == expected

    del output[:]
    t.clear_state()
    t.translate(stroke('P'))
    assert output == [([], [Translation([stroke('P')], None)], None)]

    del output[:]
    t.set_state(s)
    t.translate(stroke('P'))
    assert output == [([],
                       [Translation([stroke('P')], None)],
                       [Translation([stroke('S'), stroke('P')], 'hi')])]


def test_translator():

    # It's not clear that this test is needed anymore. There are separate
    # tests for _translate_stroke and test_translate_calls_translate_stroke
    # makes sure that translate calls it properly. But since I already wrote
    # this test I'm going to keep it.

    class Output:
        def __init__(self):
            self._output = []

        def write(self, undo, do, prev):
            for t in undo:
                self._output.pop()
            for t in do:
                if t.english:
                    self._output.append(t.english)
                else:
                    self._output.append('/'.join(t.rtfcre))

        def get(self):
            return ' '.join(self._output)

        def clear(self):
            del self._output[:]

    d = StenoDictionary()
    out = Output()
    t = Translator()
    dc = StenoDictionaryCollection([d])
    t.set_dictionary(dc)
    t.add_listener(out.write)

    t.translate(stroke('S'))
    assert out.get() == 'S'
    t.translate(stroke('T'))
    assert out.get() == 'S T'
    t.translate(stroke('*'))
    assert out.get() == 'S'
    t.translate(stroke('*'))
    # Undo buffer ran out
    assert out.get() == 'S ' + BACK_STRING

    t.set_min_undo_length(3)
    out.clear()
    t.translate(stroke('S'))
    assert out.get() == 'S'
    t.translate(stroke('T'))
    assert out.get() == 'S T'
    t.translate(stroke('*'))
    assert out.get() == 'S'
    t.translate(stroke('*'))
    assert out.get() == ''

    out.clear()
    d[('S',)] = 't1'
    d[('T',)] = 't2'
    d[('S', 'T')] = 't3'

    t.translate(stroke('S'))
    assert out.get() == 't1'
    t.translate(stroke('T'))
    assert out.get() == 't3'
    t.translate(stroke('T'))
    assert out.get() == 't3 t2'
    t.translate(stroke('S'))
    assert out.get() == 't3 t2 t1'
    t.translate(stroke('*'))
    assert out.get() == 't3 t2'
    t.translate(stroke('*'))
    assert out.get() == 't3'
    t.translate(stroke('*'))
    assert out.get() == 't1'
    t.translate(stroke('*'))
    assert out.get() == ''

    t.translate(stroke('S'))
    assert out.get() == 't1'
    t.translate(stroke('T'))
    assert out.get() == 't3'
    t.translate(stroke('T'))
    assert out.get() == 't3 t2'

    d[('S', 'T', 'T')] = 't4'
    d[('S', 'T', 'T', 'S')] = 't5'

    t.translate(stroke('S'))
    assert out.get() == 't5'
    t.translate(stroke('*'))
    assert out.get() == 't3 t2'
    t.translate(stroke('*'))
    assert out.get() == 't3'
    t.translate(stroke('T'))
    assert out.get() == 't4'
    t.translate(stroke('S'))
    assert out.get() == 't5'
    t.translate(stroke('S'))
    assert out.get() == 't5 t1'
    t.translate(stroke('*'))
    assert out.get() == 't5'
    t.translate(stroke('*'))
    assert out.get() == 't4'
    t.translate(stroke('*'))
    assert out.get() == 't3'
    t.translate(stroke('*'))
    assert out.get() == 't1'
    t.translate(stroke('*'))
    assert out.get() == ''

    d.clear()

    s = stroke('S')
    t.translate(s)
    t.translate(s)
    t.translate(s)
    t.translate(s)
    s = stroke('*')
    t.translate(s)
    t.translate(s)
    t.translate(s)
    t.translate(s)
    # Not enough undo to clear output.
    assert out.get() == 'S ' + BACK_STRING

    out.clear()
    t.remove_listener(out.write)
    t.translate(stroke('S'))
    assert out.get() == ''


class TestState:

    def setup_method(self):
        self.a = Translation([stroke('S')], None)
        self.b = Translation([stroke('T'), stroke('-D')], None)
        self.c = Translation([stroke('-Z'), stroke('P'), stroke('T*')], None)

    def test_prev_list0(self):
        s = _State()
        assert s.prev() is None

    def test_prev_list1(self):
        s = _State()
        s.translations = [self.a]
        assert s.prev() == [self.a]

    def test_prev_list2(self):
        s = _State()
        s.translations = [self.a, self.b]
        assert s.prev() == [self.a, self.b]

    def test_prev_tail1(self):
        s = _State()
        s.translations = [self.a]
        s.tail = self.b
        assert s.prev() == [self.a]

    def test_prev_tail0(self):
        s = _State()
        s.tail = self.b
        assert s.prev() == [self.b]

    def test_restrict_size_zero_on_empty(self):
        s = _State()
        s.restrict_size(0)
        assert s.translations == []
        assert s.tail is None

    def test_restrict_size_zero_on_one_stroke(self):
        s = _State()
        s.translations = [self.a]
        s.restrict_size(0)
        assert s.translations == [self.a]
        assert s.tail is None

    def test_restrict_size_to_exactly_one_stroke(self):
        s = _State()
        s.translations = [self.a]
        s.restrict_size(1)
        assert s.translations == [self.a]
        assert s.tail is None

    def test_restrict_size_to_one_on_two_strokes(self):
        s = _State()
        s.translations = [self.b]
        s.restrict_size(1)
        assert s.translations == [self.b]
        assert s.tail is None

    def test_restrict_size_to_one_on_two_translations(self):
        s = _State()
        s.translations = [self.b, self.a]
        s.restrict_size(1)
        assert s.translations == [self.a]
        assert s.tail == self.b

    def test_restrict_size_to_one_on_two_translations_too_big(self):
        s = _State()
        s.translations = [self.a, self.b]
        s.restrict_size(1)
        assert s.translations == [self.b]
        assert s.tail == self.a

    def test_restrict_size_lose_translations(self):
        s = _State()
        s.translations = [self.a, self.b, self.c]
        s.restrict_size(2)
        assert s.translations == [self.c]
        assert s.tail == self.b

    def test_restrict_size_multiple_translations(self):
        s = _State()
        s.translations = [self.a, self.b, self.c]
        s.restrict_size(5)
        assert s.translations == [self.b, self.c]
        assert s.tail == self.a


class TestTranslateStroke:

    class CaptureOutput:
        output = namedtuple('output', 'undo do prev')

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

        def __call__(self, undo, new, prev):
            prev = list(prev) if prev else None
            self.output = type(self).output(undo, new, prev)

    def t(self, strokes):
        """A quick way to make a translation."""
        strokes = [stroke(x) for x in strokes.split('/')]
        key = tuple(s.rtfcre for s in strokes)
        translation = self.dc.lookup(key)
        return Translation(strokes, translation)

    def lt(self, translations):
        """A quick way to make a list of translations."""
        return [self.t(x) for x in translations.split()]

    def define(self, key, value):
        key = normalize_steno(key)
        self.d[key] = value

    def translate(self, steno):
        self.tlor.translate(stroke(steno))

    def _check_translations(self, expected):
        # Hide from traceback on assertions (reduce output size for failed tests).
        __tracebackhide__ = operator.methodcaller('errisinstance', AssertionError)
        msg = '''
        translations:
            results: %s
            expected: %s
        ''' % (self.s.translations, expected)
        assert self.s.translations == expected, msg

    def _check_output(self, undo, do, prev):
        # Hide from traceback on assertions (reduce output size for failed tests).
        __tracebackhide__ = operator.methodcaller('errisinstance', AssertionError)
        msg = '''
        output:
            results: -%s
                     +%s
                     [%s]
            expected: -%s
                      +%s
                      [%s]
        ''' % (self.o.output + (undo, do, prev))
        assert self.o.output == (undo, do, prev), msg

    def setup_method(self):
        self.d = StenoDictionary()
        self.dc = StenoDictionaryCollection([self.d])
        self.s = _State()
        self.o = self.CaptureOutput()
        self.tlor = Translator()
        self.tlor.set_dictionary(self.dc)
        self.tlor.add_listener(self.o)
        self.tlor.set_state(self.s)

    def test_first_stroke(self):
        self.translate('-B')
        self._check_translations(self.lt('-B'))
        self._check_output([], self.lt('-B'), None)

    def test_second_stroke(self):
        self.define('S/P', 'spiders')
        self.s.translations = self.lt('S')
        self.translate('-T')
        self._check_translations(self.lt('S -T'))
        self._check_output([], self.lt('-T'), self.lt('S'))

    def test_second_stroke_tail(self):
        self.s.tail = self.t('T/A/EU/L')
        self.translate('-E')
        self._check_translations(self.lt('E'))
        self._check_output([], self.lt('E'), self.lt('T/A/EU/L'))

    def test_with_translation_1(self):
        self.define('S', 'is')
        self.define('-T', 'that')
        self.s.translations = self.lt('S')
        self.tlor.set_min_undo_length(2)
        self.translate('-T')
        self._check_translations(self.lt('S -T'))
        self._check_output([], self.lt('-T'), self.lt('S'))
        assert self.o.output.do[0].english == 'that'

    def test_with_translation_2(self):
        self.define('S', 'is')
        self.define('-T', 'that')
        self.s.translations = self.lt('S')
        self.tlor.set_min_undo_length(1)
        self.translate('-T')
        self._check_translations(self.lt('-T'))
        self._check_output([], self.lt('-T'), self.lt('S'))
        assert self.o.output.do[0].english == 'that'

    def test_finish_two_translation(self):
        self.define('S/T', 'hello')
        self.s.translations = self.lt('S')
        self.translate('T')
        self._check_translations(self.lt('S/T'))
        self._check_output(self.lt('S'), self.lt('S/T'), None)
        assert self.o.output.do[0].english == 'hello'
        assert self.o.output.do[0].replaced == self.lt('S')

    def test_finish_three_translation(self):
        self.define('S/T/-B', 'bye')
        self.s.translations = self.lt('S T')
        self.translate('-B')
        self._check_translations(self.lt('S/T/-B'))
        self._check_output(self.lt('S T'), self.lt('S/T/-B'), None)
        assert self.o.output.do[0].english == 'bye'
        assert self.o.output.do[0].replaced == self.lt('S T')

    def test_replace_translation(self):
        self.define('S/T/-B', 'longer')
        self.s.translations = self.lt('S/T')
        self.translate('-B')
        self._check_translations(self.lt('S/T/-B'))
        self._check_output(self.lt('S/T'), self.lt('S/T/-B'), None)
        assert self.o.output.do[0].english == 'longer'
        assert self.o.output.do[0].replaced == self.lt('S/T')

    def test_undo(self):
        self.s.translations = self.lt('POP')
        self.translate('*')
        self._check_translations([])
        self._check_output(self.lt('POP'), [], None)

    def test_empty_undo(self):
        self.translate('*')
        self._check_translations([])
        self._check_output([], [Translation([Stroke('*')], BACK_STRING)], None)

    def test_undo_translation(self):
        self.define('P/P', 'pop')
        self.translate('P')
        self.translate('P')
        self.translate('*')
        self._check_translations(self.lt('P'))
        self._check_output(self.lt('P/P'), self.lt('P'), None)

    def test_undo_longer_translation(self):
        self.define('P/P/-D', 'popped')
        self.translate('P')
        self.translate('P')
        self.translate('-D')
        self.translate('*')
        self._check_translations(self.lt('P P'))
        self._check_output(self.lt('P/P/-D'), self.lt('P P'), None)

    def test_undo_tail(self):
        self.s.tail = self.t('T/A/EU/L')
        self.translate('*')
        self._check_translations([])
        self._check_output([], [Translation([Stroke('*')], BACK_STRING)], [self.s.tail])

    def test_suffix_folding(self):
        self.define('K-L', 'look')
        self.define('-G', '{^ing}')
        lt = self.lt('K-LG')
        lt[0].english = 'look {^ing}'
        self.translate('K-LG')
        self._check_translations(lt)

    def test_suffix_folding_multi_stroke(self):
        self.define('E/HR', 'he will')
        self.define('-S', '{^s}')
        self.translate('E')
        self.translate('HR-S')
        output = ' '.join(t.english for t in self.s.translations)
        assert output == 'he will {^s}'

    def test_suffix_folding_doesnt_interfere(self):
        self.define('E/HR', 'he will')
        self.define('-S', '{^s}')
        self.define('E', 'he')
        self.define('HR-S', 'also')
        self.translate('E')
        self.translate('HR-S')
        output = ' '.join(t.english for t in self.s.translations)
        assert output == 'he also'

    def test_suffix_folding_no_suffix(self):
        self.define('K-L', 'look')
        lt = self.lt('K-LG')
        assert lt[0].english is None
        self.translate('K-LG')
        self._check_translations(lt)

    def test_suffix_folding_no_main(self):
        self.define('-G', '{^ing}')
        lt = self.lt('K-LG')
        assert lt[0].english is None
        self.translate('K-LG')
        self._check_translations(lt)

    def test_retrospective_insert_space(self):
        self.define('T/E/S/T', 'a longer key')
        self.define('PER', 'perfect')
        self.define('SWAEUGS', 'situation')
        self.define('PER/SWAEUGS', 'persuasion')
        self.define('SP*', '{*?}')
        self.translate('PER')
        self.translate('SWAEUGS')
        self.translate('SP*')
        lt = self.lt('PER')
        undo = self.lt('PER/SWAEUGS')
        undo[0].replaced = lt
        do = self.lt('SP*')
        do[0].english = 'perfect situation'
        do[0].is_retrospective_command = True
        do[0].replaced = undo
        self._check_translations(do)
        self._check_output(undo, do, None)

    def test_retrospective_insert_space_undefined(self):
        # Should work when beginning or ending strokes aren't defined
        self.define('T/E/S/T', 'a longer key')
        self.define('STWR/STWR', 'test')
        self.define('SP*', '{*?}')
        self.translate('STWR')
        self.translate('STWR')
        self.translate('SP*')
        lt = self.lt('STWR')
        undo = self.lt('STWR/STWR')
        undo[0].replaced = lt
        do = self.lt('SP*')
        do[0].english = 'STWR STWR'
        do[0].is_retrospective_command = True
        do[0].replaced = undo
        self._check_translations(do)
        self._check_output(undo, do, None)

    def test_retrospective_delete_space(self):
        self.define('T/E/S/T', 'a longer key')
        self.define('K', 'kick')
        self.define('PW', 'back')
        self.define('SP*', '{*!}')
        self.translate('K')
        self.translate('PW')
        self.translate('SP*')
        undo = self.lt('K PW')
        do = self.lt('SP*')
        do[0].english = 'kick{^~|^}back'
        do[0].is_retrospective_command = True
        do[0].replaced = undo
        self._check_translations(do)
        self._check_output(undo, do, None)

    def test_retrospective_delete_space_with_number(self):
        self.define('T/E/S/T', 'a longer key')
        self.define('U', 'user')
        self.define('SP*', '{*!}')
        self.translate('U')
        self.translate('1-')
        self.translate('SP*')
        undo = self.lt('U 1-')
        do = self.lt('SP*')
        do[0].english = 'user{^~|^}{&1}'
        do[0].is_retrospective_command = True
        do[0].replaced = undo
        self._check_translations(do)
        self._check_output(undo, do, None)

    def test_retrospective_delete_space_with_period(self):
        self.define('T/E/S/T', 'a longer key')
        self.define('P-P', '{.}')
        self.define('SH*', 'zshrc')
        self.define('SP*', '{*!}')
        self.translate('P-P')
        self.translate('SH*')
        self.translate('SP*')
        undo = self.lt('P-P SH*')
        do = self.lt('SP*')
        do[0].english = '{.}{^~|^}zshrc'
        do[0].is_retrospective_command = True
        do[0].replaced = undo
        self._check_translations(do)
        self._check_output(undo, do, None)

    def test_retrospective_toggle_asterisk(self):
        self.define('T/E/S/T', 'a longer key')
        self.define('S', 'see')
        self.define('S*', 'sea')
        self.define('A*', '{*}')
        self.translate('S')
        self.translate('A*')
        self._check_translations(self.lt('S*'))
        self._check_output(self.lt('S'), self.lt('S*'), None)

    def test_retrospective_toggle_empty(self):
        self.define('A*', '{*}')
        self.translate('A*')
        self._check_translations(self.lt(''))
        assert self.o.output == []

    def test_retrospective_toggle_asterisk_replaced1(self):
        self.define('P-P', '{.}')
        self.define('SKEL', 'cancel')
        self.define('SKEL/TO-PB', 'skeleton')
        self.define('SKEL/TO*PB', 'not skeleton!')
        self.define('A*', '{*}')
        self.translate('P-P')
        self.translate('SKEL')
        self.translate('TO-PB')
        self.translate('A*')
        self._check_translations(self.lt('SKEL/TO*PB'))
        self._check_output(self.lt('SKEL/TO-PB'),
                          self.lt('SKEL/TO*PB'),
                          self.lt('P-P'))

    def test_retrospective_toggle_asterisk_replaced2(self):
        self.define('P-P', '{.}')
        self.define('SKEL', 'cancel')
        self.define('SKEL/TO-PB', 'skeleton')
        self.define('TO*PB', '{^ton}')
        self.define('A*', '{*}')
        self.translate('P-P')
        self.translate('SKEL')
        self.translate('TO-PB')
        self.translate('A*')
        self._check_translations(self.lt('SKEL TO*PB'))
        self._check_output(self.lt('SKEL/TO-PB'),
                          self.lt('SKEL TO*PB'),
                          self.lt('P-P'))

    def test_repeat_last_stroke1(self):
        self.define('T/E/S/T', 'a longer key')
        self.define('TH', 'this')
        self.define('R*', '{*+}')
        self.translate('TH')
        self.translate('R*')
        undo = []
        do = self.lt('TH')
        state = self.lt('TH TH')
        self._check_translations(state)
        self._check_output(undo, do, do)

    def test_repeat_last_stroke2(self):
        self.define('T/E/S/T', 'a longer key')
        self.define('THA', 'that')
        self.define('R*', '{*+}')
        self.translate('THA')
        self.translate('R*')
        undo = []
        do = self.lt('THA')
        state = self.lt('THA THA')
        self._check_translations(state)
        self._check_output(undo, do, do)

    def test_untranslate_translation(self):
        self.tlor.set_min_undo_length(2)
        self.define('TH', 'this')
        self.define('THA', 'that')
        self.translate('TH')
        self.translate('THA')
        self.tlor.untranslate_translation(self.t('THA'))
        self.tlor.untranslate_translation(self.t('TH'))
        self.tlor.flush()
        self._check_output(self.lt('TH THA'), [], None)


ESCAPE_UNESCAPE_TRANSLATION_TESTS = (
    # No change.
    lambda: ('foobar', 'foobar'),
    lambda: (r'\\', r'\\'),
    lambda: ('\\\\\\', '\\\\\\'), # -> \\\
    # Basic support: \n, \r, \t.
    lambda: ('\n', r'\n'),
    lambda: ('\r', r'\r'),
    lambda: ('\t', r'\t'),
    # Allow a literal \n, \r, or \t by doubling the \.
    lambda: (r'\n', r'\\n'),
    lambda: (r'\r', r'\\r'),
    lambda: (r'\t', r'\\t'),
    lambda: (r'\\n', r'\\\n'),
    lambda: (r'\\r', r'\\\r'),
    lambda: (r'\\t', r'\\\t'),
    # A little more complex.
    lambda: ('\tfoo\nbar\r', r'\tfoo\nbar\r'),
    lambda: ('\\tfoo\\nbar\\r', r'\\tfoo\\nbar\\r'),
)

@parametrize(ESCAPE_UNESCAPE_TRANSLATION_TESTS)
def test_escape_unescape_translation(raw, escaped):
    assert unescape_translation(escaped) == raw
    assert escape_translation(raw) == escaped
