"""Cmd2 functional testing based on transcript"""

import os
import random
import re
import sys
import tempfile
from typing import NoReturn
from unittest import (
    mock,
)

import pytest

import cmd2
from cmd2 import (
    transcript,
)
from cmd2.utils import (
    Settable,
    StdSim,
)

from .conftest import (
    run_cmd,
    verify_help_text,
)


class CmdLineApp(cmd2.Cmd):
    MUMBLES = ('like', '...', 'um', 'er', 'hmmm', 'ahh')
    MUMBLE_FIRST = ('so', 'like', 'well')
    MUMBLE_LAST = ('right?',)

    def __init__(self, *args, **kwargs) -> None:
        self.maxrepeats = 3

        super().__init__(*args, multiline_commands=['orate'], **kwargs)

        # Make maxrepeats settable at runtime
        self.add_settable(Settable('maxrepeats', int, 'Max number of `--repeat`s allowed', self))

        self.intro = 'This is an intro banner ...'

    speak_parser = cmd2.Cmd2ArgumentParser()
    speak_parser.add_argument('-p', '--piglatin', action="store_true", help="atinLay")
    speak_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE")
    speak_parser.add_argument('-r', '--repeat', type=int, help="output [n] times")

    @cmd2.with_argparser(speak_parser, with_unknown_args=True)
    def do_speak(self, opts, arg) -> None:
        """Repeats what you tell me to."""
        arg = ' '.join(arg)
        if opts.piglatin:
            arg = f'{arg[1:]}{arg[0]}ay'
        if opts.shout:
            arg = arg.upper()
        repetitions = opts.repeat or 1
        for _ in range(min(repetitions, self.maxrepeats)):
            self.poutput(arg)
            # recommend using the poutput function instead of
            # self.stdout.write or "print", because Cmd allows the user
            # to redirect output

    do_say = do_speak  # now "say" is a synonym for "speak"
    do_orate = do_speak  # another synonym, but this one takes multi-line input

    mumble_parser = cmd2.Cmd2ArgumentParser()
    mumble_parser.add_argument('-r', '--repeat', type=int, help="output [n] times")

    @cmd2.with_argparser(mumble_parser, with_unknown_args=True)
    def do_mumble(self, opts, arg) -> None:
        """Mumbles what you tell me to."""
        repetitions = opts.repeat or 1
        for _ in range(min(repetitions, self.maxrepeats)):
            output = []
            if random.random() < 0.33:
                output.append(random.choice(self.MUMBLE_FIRST))
            for word in arg:
                if random.random() < 0.40:
                    output.append(random.choice(self.MUMBLES))
                output.append(word)
            if random.random() < 0.25:
                output.append(random.choice(self.MUMBLE_LAST))
            self.poutput(' '.join(output))

    def do_nothing(self, statement) -> None:
        """Do nothing and output nothing"""

    def do_keyboard_interrupt(self, _) -> NoReturn:
        raise KeyboardInterrupt('Interrupting this command')


def test_commands_at_invocation() -> None:
    testargs = ["prog", "say hello", "say Gracie", "quit"]
    expected = "This is an intro banner ...\nhello\nGracie\n"
    with mock.patch.object(sys, 'argv', testargs):
        app = CmdLineApp()

    app.stdout = StdSim(app.stdout)
    app.cmdloop()
    out = app.stdout.getvalue()
    assert out == expected


@pytest.mark.parametrize(
    ('filename', 'feedback_to_output'),
    [
        ('bol_eol.txt', False),
        ('characterclass.txt', False),
        ('dotstar.txt', False),
        ('extension_notation.txt', False),
        ('from_cmdloop.txt', True),
        ('multiline_no_regex.txt', False),
        ('multiline_regex.txt', False),
        ('no_output.txt', False),
        ('no_output_last.txt', False),
        ('singleslash.txt', False),
        ('slashes_escaped.txt', False),
        ('slashslash.txt', False),
        ('spaces.txt', False),
        ('word_boundaries.txt', False),
    ],
)
def test_transcript(request, capsys, filename, feedback_to_output) -> None:
    # Get location of the transcript
    test_dir = os.path.dirname(request.module.__file__)
    transcript_file = os.path.join(test_dir, 'transcripts', filename)

    # Need to patch sys.argv so cmd2 doesn't think it was called with
    # arguments equal to the py.test args
    testargs = ['prog', '-t', transcript_file]
    with mock.patch.object(sys, 'argv', testargs):
        # Create a cmd2.Cmd() instance and make sure basic settings are
        # like we want for test
        app = CmdLineApp()

    app.feedback_to_output = feedback_to_output

    # Run the command loop
    sys_exit_code = app.cmdloop()
    assert sys_exit_code == 0

    # Check for the unittest "OK" condition for the 1 test which ran
    expected_start = ".\n----------------------------------------------------------------------\nRan 1 test in"
    expected_end = "s\n\nOK\n"
    _, err = capsys.readouterr()
    assert err.startswith(expected_start)
    assert err.endswith(expected_end)


def test_history_transcript() -> None:
    app = CmdLineApp()
    app.stdout = StdSim(app.stdout)
    run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
    run_cmd(app, 'speak /tmp/file.txt is not a regex')

    expected = r"""(Cmd) orate this is
> a /multiline/
> command;
this is a \/multiline\/ command
(Cmd) speak /tmp/file.txt is not a regex
\/tmp\/file.txt is not a regex
"""

    # make a tmp file
    fd, history_fname = tempfile.mkstemp(prefix='', suffix='.txt')
    os.close(fd)

    # tell the history command to create a transcript
    run_cmd(app, f'history -t "{history_fname}"')

    # read in the transcript created by the history command
    with open(history_fname) as f:
        xscript = f.read()

    assert xscript == expected


def test_history_transcript_bad_path(mocker) -> None:
    app = CmdLineApp()
    app.stdout = StdSim(app.stdout)
    run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
    run_cmd(app, 'speak /tmp/file.txt is not a regex')

    # Bad directory
    history_fname = '~/fakedir/this_does_not_exist.txt'
    _out, err = run_cmd(app, f'history -t "{history_fname}"')
    assert "is not a directory" in err[0]

    # Cause os.open to fail and make sure error gets printed
    mock_remove = mocker.patch('builtins.open')
    mock_remove.side_effect = OSError

    history_fname = 'outfile.txt'
    _out, err = run_cmd(app, f'history -t "{history_fname}"')
    assert "Error saving transcript file" in err[0]


def test_run_script_record_transcript(base_app, request) -> None:
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'scripts', 'help.txt')

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # make a tmp file to use as a transcript
    fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn')
    os.close(fd)

    # Execute the run_script command with the -t option to generate a transcript
    run_cmd(base_app, f'run_script {filename} -t {transcript_fname}')

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # read in the transcript created by the history command
    with open(transcript_fname) as f:
        xscript = f.read()

    assert xscript.startswith('(Cmd) help -v\n')
    verify_help_text(base_app, xscript)


def test_generate_transcript_stop(capsys) -> None:
    # Verify transcript generation stops when a command returns True for stop
    app = CmdLineApp()

    # Make a tmp file to use as a transcript
    fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn')
    os.close(fd)

    # This should run all commands
    commands = ['help', 'set']
    app._generate_transcript(commands, transcript_fname)
    _, err = capsys.readouterr()
    assert err.startswith("2 commands")

    # Since quit returns True for stop, only the first 2 commands will run
    commands = ['help', 'quit', 'set']
    app._generate_transcript(commands, transcript_fname)
    _, err = capsys.readouterr()
    assert err.startswith("Command 2 triggered a stop")

    # keyboard_interrupt command should stop the loop and not run the third command
    commands = ['help', 'keyboard_interrupt', 'set']
    app._generate_transcript(commands, transcript_fname)
    _, err = capsys.readouterr()
    assert err.startswith("Interrupting this command\nCommand 2 triggered a stop")


@pytest.mark.parametrize(
    ('expected', 'transformed'),
    [
        # strings with zero or one slash or with escaped slashes means no regular
        # expression present, so the result should just be what re.escape returns.
        # we don't use static strings in these tests because re.escape behaves
        # differently in python 3.7+ than in prior versions
        ('text with no slashes', re.escape('text with no slashes')),
        ('specials .*', re.escape('specials .*')),
        ('use 2/3 cup', re.escape('use 2/3 cup')),
        ('/tmp is nice', re.escape('/tmp is nice')),
        ('slash at end/', re.escape('slash at end/')),
        # escaped slashes
        (r'not this slash\/ or this one\/', re.escape('not this slash/ or this one/')),
        # regexes
        ('/.*/', '.*'),
        ('specials ^ and + /[0-9]+/', re.escape('specials ^ and + ') + '[0-9]+'),
        (r'/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}' + re.escape(' but not /a{6} with ') + '.*?' + re.escape(' more')),
        (r'not \/, use /\|?/, not \/', re.escape('not /, use ') + r'\|?' + re.escape(', not /')),
        # inception: slashes in our regex. backslashed on input, bare on output
        (r'not \/, use /\/?/, not \/', re.escape('not /, use ') + '/?' + re.escape(', not /')),
        (r'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff')),
    ],
)
def test_parse_transcript_expected(expected, transformed) -> None:
    app = CmdLineApp()

    class TestMyAppCase(transcript.Cmd2TestCase):
        cmdapp = app

    testcase = TestMyAppCase()
    assert testcase._transform_transcript_expected(expected) == transformed


def test_transcript_failure(request, capsys) -> None:
    # Get location of the transcript
    test_dir = os.path.dirname(request.module.__file__)
    transcript_file = os.path.join(test_dir, 'transcripts', 'failure.txt')

    # Need to patch sys.argv so cmd2 doesn't think it was called with
    # arguments equal to the py.test args
    testargs = ['prog', '-t', transcript_file]
    with mock.patch.object(sys, 'argv', testargs):
        # Create a cmd2.Cmd() instance and make sure basic settings are
        # like we want for test
        app = CmdLineApp()

    app.feedback_to_output = False

    # Run the command loop
    sys_exit_code = app.cmdloop()
    assert sys_exit_code != 0

    expected_start = "File "
    expected_end = "s\n\nFAILED (failures=1)\n\n"
    _, err = capsys.readouterr()
    assert err.startswith(expected_start)
    assert err.endswith(expected_end)


def test_transcript_no_file(request, capsys) -> None:
    # Need to patch sys.argv so cmd2 doesn't think it was called with
    # arguments equal to the py.test args
    testargs = ['prog', '-t']
    with mock.patch.object(sys, 'argv', testargs):
        app = CmdLineApp()

    app.feedback_to_output = False

    # Run the command loop
    sys_exit_code = app.cmdloop()
    assert sys_exit_code != 0

    # Check for the unittest "OK" condition for the 1 test which ran
    expected = 'No test files found - nothing to test\n'
    _, err = capsys.readouterr()
    assert err == expected
