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

import textwrap

import pytest

from plover import __version__ as plover_version
from plover.dictionary.rtfcre_dict import RtfDictionary, TranslationFormatter
from plover.dictionary.rtfcre_parse import BadRtfError

from plover_build_utils.testing import dictionary_test, parametrize


@parametrize((
    lambda: ('', ''),
    # Affix handling.
    lambda: ('{^}', r'{\cxds}'),
    lambda: ('{^^}', r'{\cxds}'),
    lambda: ('{^in^}', r'{\cxds in\cxds}'),
    lambda: ('{pre^}', r'{pre\cxds}'),
    lambda: ('{pre^} ', r'{pre\cxds}'),
    lambda: ('{pre^}  ', r'{pre\cxds}'),
    lambda: ('{ pre ^}  ', r'{ pre \cxds}'),
    lambda: ('{^post}', r'{\cxds post}'),
    lambda: (' {^post}', r'{\cxds post}'),
    lambda: ('  {^post}', r'{\cxds post}'),
    lambda: ('{^ post }  ', r'{\cxds  post }'),
    # Escaping special characters.
    lambda: (r'\{', r'\\\{'),
    lambda: (r'\}', r'\\\}'),
    # Hard space.
    lambda: ('{^ ^}', r'\~'),
    # Non-breaking hyphen.
    lambda: ('{^-^}', r'\_'),
    # Handling newlines.
    lambda: ('test\nsomething', r'test\line something'),
    lambda: ('test\n\nsomething', r'test\par something'),
    lambda: ('test\\nsomething', r'test\\nsomething'),
    # Handling tabs.
    lambda: ('test\tsomething', r'test\tab something'),
    lambda: ('test\\tsomething', r'test\\tsomething'),
    # Force Cap.
    lambda: (r'Mr.{-|}', r'Mr.\cxfc '),
    # Force Lower Case.
    lambda: (r'{>}lol', r'\cxfl lol'),
    # Infix with force cap.
    lambda: ('{^\n^}{-|}', r'{\cxds \line \cxds}\cxfc '),
    lambda: ('{^\n\n^}{-|}', r'{\cxds \par \cxds}\cxfc '),
    # Plover custom formatting:
    # - meta: command
    lambda: ('{PLOVER:TOGGLE}', r'{\*\cxplovermeta PLOVER:TOGGLE}'),
    # - meta: key combo
    lambda: ('{#Return}', r'{\*\cxplovermeta #Return}'),
    # - meta: other
    lambda: ('{:retro_case:cap_first_word}', r'{\*\cxplovermeta :retro_case:cap_first_word}'),
    # - macro
    lambda: ('{*}', r'{\*\cxplovermacro retrospective_toggle_asterisk}'),
    lambda: ('{*!}', r'{\*\cxplovermacro retrospective_delete_space}'),
    lambda: ('{*?}', r'{\*\cxplovermacro retrospective_insert_space}'),
    lambda: ('{*+}', r'{\*\cxplovermacro repeat_last_stroke}'),
    lambda: ('=retrospective_delete_space', r'{\*\cxplovermacro retrospective_delete_space}'),
    lambda: ('=macro:with_arg', r'{\*\cxplovermacro macro:with_arg}'),
    lambda: ('=macro:', r'{\*\cxplovermacro macro:}'),
    lambda: ('=undo', r'\cxdstroke'),
    # - not macros
    lambda: ('==', '=='),
    lambda: ('=test()', '=test()'),
    lambda: ("=macro{<-ceci n'est pas une macro}", r"=macro{\*\cxplovermeta <-ceci n'est pas une macro}"),
    lambda: ('{*}something', r'{\*\cxplovermeta *}something'),
))
def test_format_translation(before, expected):
    result = TranslationFormatter().format(before)
    assert result == expected


def rtf_load_test(*spec, xfail=False):
    assert 1 <= len(spec) <= 2
    if len(spec) == 2:
        # Conversion test.
        rtf_entries = r'{\*\cxs S}%s' % spec[0]
        dict_entries = '"S": %r' % spec[1]
    else:
        spec = textwrap.dedent(spec[0]).lstrip()
        if not spec:
            rtf_entries, dict_entries = '', ''
        else:
            rtf_entries, dict_entries = tuple(spec.rsplit('\n\n', 1))
    kwargs = dict(marks=pytest.mark.xfail) if xfail else {}
    return pytest.param(rtf_entries, dict_entries, **kwargs)

RTF_LOAD_TESTS = (

    # Empty dictionary.
    lambda: rtf_load_test(
        '''



        '''),

    # Only one translation.
    lambda: rtf_load_test(
        r'''
        {\*\cxs SP}translation

        'SP': 'translation',
        '''),

    # One translation on multiple lines.
    lambda: rtf_load_test(
        '''
        {\\*\\cxs SP}\r\ntranslation

        'SP': 'translation'
        '''),

    # Steno and translation on multiple lines.
    lambda: rtf_load_test(
        '''
        {\\*\\cxs PHA*-EU/SKWRAO-UPB/SKWR-UL/A-UGT/S-EPT/O-BGT/TPHO*-EF/STK-EPL/SKWRA-PB/TP-EB\r\n
        /PHA-R/A-EUP}May, June, July, August, September, October, November, December, January,\r\n
         February, March, April

        ('PHA*-EU/SKWRAO-UPB/SKWR-UL/A-UGT/'
         'S-EPT/O-BGT/TPHO*-EF/STK-EPL/'
         'SKWRA-PB/TP-EB/PHA-R/A-EUP'): (
         'May, June, July, August, September, '
         'October, November, December, January, '
         'February, March, April')
        '''),

    # Multiple translations no newlines.
    lambda: rtf_load_test(
        r'''
        {\*\cxs SP}translation{\*\cxs S}translation2

        'SP': 'translation',
        'S': 'translation2',
        '''),

    # Multiple translations on separate lines.
    lambda: rtf_load_test(
        '''
        {\\*\\cxs SP}translation\r\n{\\*\\cxs S}translation2

        'SP': 'translation',
        'S': 'translation2',
        '''),
    lambda: rtf_load_test(
        '''
        {\\*\\cxs SP}translation\n{\\*\\cxs S}translation2

        'SP': 'translation',
        'S': 'translation2',
        '''),

    # Group start split on 2 lines.
    lambda: rtf_load_test(
        r'''
        {\*\cxs TEFT}{
        \*\ignored I'm invisible}

        'TEFT': '',
        '''),

    # Mapping to empty string at end.
    lambda: rtf_load_test(
        r'''
        {\*\cxs TEFT}

        'TEFT': '',
        '''),

    # Escaped \r\n handled.
    lambda: rtf_load_test(
        '''
        {\\*\\cxs SP}trans\\\r\n

        'SP': 'trans{^\\n\\n^}',
        '''),

    # Escaped \r\n handled in mid translation.
    lambda: rtf_load_test(
        '''
        {\\*\\cxs SP}trans\\\r\nlation

        'SP': 'trans\\n\\nlation',
        '''),

    # Whitespace is preserved in various situations.
    lambda: rtf_load_test(
        r'''
        {\*\cxs S}t  

        'S': 't{^  ^}',
        '''),
    lambda: rtf_load_test(
        r'''
        {\*\cxs S}  t

        'S': '{^  ^}t',
        '''),
    lambda: rtf_load_test(
        r'''
        {\*\cxs S}t   {\*\cxs T}t    

        'S': 't{^   ^}',
        'T': 't{^    ^}',
        '''),
    lambda: rtf_load_test(
        '''
        {\\*\\cxs S}t   \r\n{\\*\\cxs T}t    

        'S': 't{^   ^}',
        'T': 't{^    ^}',
        '''),
    lambda: rtf_load_test(
        '''
        {\\*\\cxs S}t  \r\n{\\*\\cxs T} t \r\n

        'S': 't{^  ^}',
        'T': ' t ',
        '''),

    # Translations are ignored if converter returns None
    lambda: rtf_load_test(
        r'''
        {\*\cxs S}return_none

        'S': 'return_none',
        '''),

    lambda: rtf_load_test(
        r'''
        {\*\cxs T}t t t  

        'T': 't t t{^  ^}',
        '''),

    # Conflicts result on only the last one kept.
    lambda: rtf_load_test(
        r'''
        {\*\cxs T}t
        {\*\cxs T}g

        'T': 'g',
        '''),
    lambda: rtf_load_test(
        r'''
        {\*\cxs T}t
        {\*\cxs T}return_none

        'T': 'return_none',
        '''),

    # Translation conversion tests.

    # Void...
    lambda: rtf_load_test('', ''),

    # Escaped characters.
    lambda: rtf_load_test(r'\-', '-'),
    lambda: rtf_load_test(r'\\ ', '\\ '),
    lambda: rtf_load_test(r'\\', '\\'),
    lambda: rtf_load_test(r'\{', '{'),
    lambda: rtf_load_test(r'\}', '}'),

    # Hard space.
    lambda: rtf_load_test(r'\~', '{^ ^}'),

    # Non-breaking hyphen.
    lambda: rtf_load_test(r'\_', '{^-^}'),

    # Escaped newline.
    lambda: rtf_load_test('\\\r\n', '{^\n\n^}'),

    # Newline / tabs control words.
    lambda: rtf_load_test(r'test:\line something', 'test:\nsomething'),
    lambda: rtf_load_test(r'\line', '\n'),
    lambda: rtf_load_test(r'\tab', '\t'),
    lambda: rtf_load_test(r'{\line at group start}', '\nat group start'),

    lambda: rtf_load_test('test text', 'test text'),
    lambda: rtf_load_test('test  text', 'test  text'),

    # Delete Spaces.
    lambda: rtf_load_test(r'\cxds', '{^}'),
    lambda: rtf_load_test(r'{\cxds}', '{^}'),
    lambda: rtf_load_test(r'pre\cxds ', '{pre^}'),
    lambda: rtf_load_test(r'{pre\cxds}', '{pre^}'),
    lambda: rtf_load_test(r'pre\cxds  ', '{pre^} '),
    lambda: rtf_load_test(r'pre\cxds', '{pre^}'),
    lambda: rtf_load_test(r'{pre\cxds  }', '{pre^} '),
    lambda: rtf_load_test(r'\cxds post', '{^post}'),
    lambda: rtf_load_test(r'{\cxds post}', '{^post}'),
    lambda: rtf_load_test(r'\cxds in\cxds', '{^in^}'),
    lambda: rtf_load_test(r'\cxds in\cxds ', '{^in^}'),
    lambda: rtf_load_test(r'{\cxds in\cxds}', '{^in^}'),

    # Force Cap.
    lambda: rtf_load_test(r'\cxfc', '{-|}'),

    # Force Lower Case.
    lambda: rtf_load_test(r'\cxfl', '{>}'),
    lambda: rtf_load_test(r'pre\cxfl', 'pre{>}'),

    # New paragraph.
    lambda: rtf_load_test(r'\par', '{^\n\n^}'),
    lambda: rtf_load_test(r'{\par test}', '{^\n\n^}test'),

    # Stenovations extensions...
    lambda: rtf_load_test(r'{\*\cxsvatdictflags N}', '{-|}'),
    lambda: rtf_load_test(r'{\*\cxsvatdictflags LN1}', '{-|}'),

    # Styles.
    # Continuation styles are indented.
    lambda: rtf_load_test(r'\par\s4', '{^\n\n    ^}'),
    # caseCATalyst declares new styles without a preceding \par,
    # so we treat it as an implicit par.
    lambda: rtf_load_test(r'\s1', '{^\n\n^}'),
    # But if the \par is present we don't treat \s as an implicit par.
    lambda: rtf_load_test(r'\par\s1', '{^\n\n^}'),

    # caseCATalyst punctuation.
    lambda: rtf_load_test(r'.', '{.}'),
    lambda: rtf_load_test(r'. ', '{.} '),
    lambda: rtf_load_test(r' . ', ' . '),

    # Automatic Text.
    lambda: rtf_load_test(r'{\cxa Q.}.', 'Q..'),

    # Don't mess with period that is part of a word.
    lambda: rtf_load_test(r'Mr.', 'Mr.'),
    lambda: rtf_load_test(r'.attribute', '.attribute'),

    # Stitching.
    lambda: rtf_load_test(r'{\cxstit contents}', 'contents'),

    # Fingerspelling.
    lambda: rtf_load_test(r'{\cxfing c}', '{&c}'),
    lambda: rtf_load_test(r'\cxfing Z.', '{&Z.}'),

    # Punctuation.
    lambda: rtf_load_test(r'{\cxp.}', '{.}'),
    lambda: rtf_load_test(r'{\cxp .}', '{.}'),
    lambda: rtf_load_test(r'{\cxp . }', '{.}'),
    lambda: rtf_load_test(r'{\cxp .  }', '{.}'),
    lambda: rtf_load_test(r'{\cxp !}', '{!}'),
    lambda: rtf_load_test(r'{\cxp ?}', '{?}'),
    lambda: rtf_load_test(r'{\cxp ,}', '{,}'),
    lambda: rtf_load_test(r'{\cxp ;}', '{;}'),
    lambda: rtf_load_test(r'{\cxp :}', '{:}'),
    lambda: rtf_load_test('{\\cxp \'}', '{^\'}'),
    lambda: rtf_load_test('{\\cxp -}', '{^-^}'),
    lambda: rtf_load_test('{\\cxp /}', '{^/^}'),
    # Why not '{^...^}'?
    lambda: rtf_load_test('{\\cxp...  }', '{^...  ^}'),
    # Why not '{^")^}'?
    lambda: rtf_load_test('{\\cxp ") }', '{^") ^}'),

    # Unsupported, non-ignored group.
    lambda: rtf_load_test(r'{\nonexistent }', ''),
    lambda: rtf_load_test(r'{\nonexistent contents}', 'contents'),
    lambda: rtf_load_test(r'{\nonexistent cont\_ents}', 'cont{^-^}ents'),
    lambda: rtf_load_test(r'{\nonexistent {\cxp .}}', '{.}'),

    # Unsupported, ignored group.
    lambda: rtf_load_test(r'{\*\nonexistent }', ''),
    lambda: rtf_load_test(r'{\*\nonexistent contents}', ''),
    lambda: rtf_load_test(r'{\*\nonexistent {\cxp .}}', ''),
    lambda: rtf_load_test(r'be\cxds{\*\cxsvatdictentrydate\yr2006\mo5\dy10}', '{be^}'),

    # Unresolved conflicts.
    lambda: rtf_load_test(r'{\cxconf [{\cxc abc}]}', '[abc]'),
    lambda: rtf_load_test(r'{\cxconf [{\cxc abc}|{\cxc def}]}', '[abc|def]'),
    lambda: rtf_load_test(r'{\cxconf [{\cxc abc}|{\cxc def}|{\cxc ghi}]}', '[abc|def|ghi]'),
    lambda: rtf_load_test(r'{\cxconf [{\cxc abc}|{\cxc {\cxp... }}]}', '[abc|{^... ^}]'),
    lambda: rtf_load_test(r"{\cxconf [{\cxc their}|{\cxc there}|{\cxc they're}]}", "[their|there|they're]"),
    lambda: rtf_load_test(r'{\cxconf [{\cxc \par\s4}|{\cxc paragraph}]}', '[\n\n    |paragraph]'),

    # Resolved conflicts.
    lambda: rtf_load_test(r"{\cxconf {\cxc their}{\*\deleted {\cxc there}{\cxc they're}}}", 'their'),
    lambda: rtf_load_test(r"{\cxconf {\*\deleted {\cxc their}}{\cxc there}{\*\deleted {\cxc they're}}}", 'there'),
    lambda: rtf_load_test(r'{\cxconf {\cxc \par\s4}{\*\deleted {\cxc paragraph}}}', '{^\n\n    ^}'),
    lambda: rtf_load_test(r'{\cxconf {\*\deleted {\cxc \par\s4}}{\cxc paragraph}}', 'paragraph'),

    # Plover custom formatting:
    # - meta: command
    lambda: rtf_load_test(r'{\*\cxplovermeta plover:focus}', '{plover:focus}'),
    # - meta: key combo
    lambda: rtf_load_test(r'{\*\cxplovermeta #Alt_L(Tab Tab)}', '{#Alt_L(Tab Tab)}'),
    # - meta: other
    lambda: rtf_load_test(r'{\*\cxplovermeta :retro_currency:$c}', '{:retro_currency:$c}'),
    # - macro
    lambda: rtf_load_test(r'{\*\cxplovermacro retrospective_delete_space}', '=retrospective_delete_space'),
    lambda: rtf_load_test(r'\cxdstroke', '=undo'),

    # Unrecoverable content.

    # Bad header.
    lambda: (br' {  \rtf1 test }', BadRtfError),
    lambda: (br'\rtf1 test }', BadRtfError),
    lambda: (br'{\rtf500 ... }', BadRtfError),

    # Recoverable content.

    # Starting new mapping in the middle of previous one.
    lambda: rtf_load_test(
        r'''
        {\*\cxs PWRA*-BGT}{\cxconf [{\cxds arg{\cxc\cxds{\*\cxsvatdictentrydate\yr2016\mo8\da12}
        {\*\cxs #25-UBG}{\cxconf [{\cxc  the talk}|{\cxc\cxsvatds , 1946,}]}{\*\cxsvatdictentrydate
        \yr2016\mo7\da14}
        {\*\cxs #250-UDZ}{\cxconf [{\cxc $25,000}|{\cxc the attitudes}]}{\*\cxsvatdictentrydate
        \yr2016\mo6\da29}

        'PWRA*BGT': '[{^arg}{^}',
        '25UBG': '[ the talk|, 1946,]',
        '250UDZ': '[$25,000|the attitudes]',
        '''),
    # Unescaped group end in the middle of a mapping.
    lambda: rtf_load_test(
        r'''
        {\*\cxs SPWHRA*-RB}\cxds \\\cxds{\*\cxsvatdictentrydate\yr2016\mo5\da20}
        {\*\cxs PWRA*-BGTD}\cxds}]}{\*\cxsvatdictentrydate\yr2016\mo8\da12}
        {\*\cxs SPA*-EUS}\cxds ^\cxds{\*\cxsvatdictentrydate\yr2016\mo8\da22}

        'SPWHRA*RB': '{^}\\{^}',
        'PWRA*BGTD' : '{^}]',
        'SPA*EUS' : '{^^^}',
        '''),
    # Fingerspelling without content.
    lambda: rtf_load_test(r'\cxfing', ''),
    lambda: (
        br'{\rtf1\ansi{\*\cxs TEFT}\cxfing',
        '''
        'TEFT': ''
        '''),

    # RTF/CRE spec allow the number key letter anywhere...
    lambda: rtf_load_test(
        r'''
        {\*\cxs 2#}2

        '2': '2',
        '''),

)

def rtf_save_test(dict_entries, rtf_entries):
    rtf_entries = b'\r\n'.join(((
        br'{\rtf1\ansi{\*\cxrev100}\cxdict'
        br'{\*\cxsystem Plover %s}'
        br'{\stylesheet{\s0 Normal;}}'
    ) % plover_version.encode(),) + rtf_entries + (b'}', b''))
    return dict_entries, rtf_entries

RTF_SAVE_TESTS = (
    lambda: rtf_save_test(
        '''
        'TEFT': 'test',
        'TEFT/-G': 'testing',
        ''',
        (br'{\*\cxs TEFT}test',
         br'{\*\cxs TEFT/-G}testing')
    ),
    lambda: rtf_save_test(
        '''
        'S/T': '{pre^}',
        ''',
        (br'{\*\cxs S/T}{pre\cxds}',)
    ),
    lambda: rtf_save_test(
        r'''
        "PWR-S": "\\{",
        ''',
        (br'{\*\cxs PWR-S}\\\{',)
    ),
    lambda: rtf_save_test(
        r'''
        "TEFT": "test\nsomething",
        ''',
        (br'{\*\cxs TEFT}test\line something',)
    ),
    lambda: rtf_save_test(
        r'''
        "TEFT": "test\\nsomething",
        ''',
        (br'{\*\cxs TEFT}test\\nsomething',)
    ),
    lambda: rtf_save_test(
        '''
        "PHROLG": "{PLOVER:TOGGLE}",
        ''',
        (br'{\*\cxs PHROLG}{\*\cxplovermeta PLOVER:TOGGLE}',)
    ),
)

@dictionary_test
class TestRtfDictionary:

    DICT_CLASS = RtfDictionary
    DICT_EXTENSION = 'rtf'
    DICT_REGISTERED = True
    DICT_LOAD_TESTS = RTF_LOAD_TESTS
    DICT_SAVE_TESTS = RTF_SAVE_TESTS
    DICT_SAMPLE = ''

    @staticmethod
    def make_dict(contents):
        if isinstance(contents, bytes):
            return contents
        rtf_styles = {
            0: 'Normal',
            1: 'Question',
            2: 'Answer',
            3: 'Colloquy',
            4: 'Continuation Q',
            5: 'Continuation A',
            6: 'Continuation Col',
            7: 'Paren',
            8: 'Centered',
        }
        rtf = (
            '\r\n'.join(
                [r'{\rtf1\ansi\cxdict{\*\cxrev100}{\*\cxsystem Fake Software}'] +
                [r'{\stylesheet'] +
                [r'{\s%d %s;}' % (k, v) for k, v in rtf_styles.items()] +
                ['}', contents, '}', '']
            ))
        return rtf.encode('cp1252')
