"""Test plugin infrastructure and hooks."""

import argparse
import sys
from typing import NoReturn
from unittest import (
    mock,
)

import pytest

import cmd2
from cmd2 import (
    Cmd2ArgumentParser,
    exceptions,
    plugin,
    with_argparser,
)


class Plugin:
    """A mixin class for testing hook registration and calling"""

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.reset_counters()

    def reset_counters(self) -> None:
        self.called_preparse = 0
        self.called_postparsing = 0
        self.called_precmd = 0
        self.called_postcmd = 0
        self.called_cmdfinalization = 0

    ###
    #
    # preloop and postloop hooks
    # which share the same signature and are thus interchangeable
    #
    ###
    def prepost_hook_one(self) -> None:
        """Method used for preloop or postloop hooks"""
        self.poutput("one")

    def prepost_hook_two(self) -> None:
        """Another method used for preloop or postloop hooks"""
        self.poutput("two")

    def prepost_hook_too_many_parameters(self, param) -> None:
        """A preloop or postloop hook with too many parameters"""

    def prepost_hook_with_wrong_return_annotation(self) -> bool:
        """A preloop or postloop hook with incorrect return type"""

    ###
    #
    # preparse hook
    #
    ###
    def preparse(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
        """Preparsing hook"""
        self.called_preparse += 1
        return data

    ###
    #
    # Postparsing hooks
    #
    ###
    def postparse_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
        """A postparsing hook"""
        self.called_postparsing += 1
        return data

    def postparse_hook_stop(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
        """A postparsing hook with requests application exit"""
        self.called_postparsing += 1
        data.stop = True
        return data

    def postparse_hook_emptystatement(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
        """A postparsing hook with raises an EmptyStatement exception"""
        self.called_postparsing += 1
        raise exceptions.EmptyStatement

    def postparse_hook_exception(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
        """A postparsing hook which raises an exception"""
        self.called_postparsing += 1
        raise ValueError

    def postparse_hook_too_many_parameters(self, data1, data2) -> cmd2.plugin.PostparsingData:
        """A postparsing hook with too many parameters"""

    def postparse_hook_undeclared_parameter_annotation(self, data) -> cmd2.plugin.PostparsingData:
        """A postparsing hook with an undeclared parameter type"""

    def postparse_hook_wrong_parameter_annotation(self, data: str) -> cmd2.plugin.PostparsingData:
        """A postparsing hook with the wrong parameter type"""

    def postparse_hook_undeclared_return_annotation(self, data: cmd2.plugin.PostparsingData) -> None:
        """A postparsing hook with an undeclared return type"""

    def postparse_hook_wrong_return_annotation(self, data: cmd2.plugin.PostparsingData) -> str:
        """A postparsing hook with the wrong return type"""

    ###
    #
    # precommand hooks, some valid, some invalid
    #
    ###
    def precmd(self, statement: cmd2.Statement) -> cmd2.Statement:
        """Override cmd.Cmd method"""
        self.called_precmd += 1
        return statement

    def precmd_hook(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
        """A precommand hook"""
        self.called_precmd += 1
        return data

    def precmd_hook_emptystatement(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
        """A precommand hook which raises an EmptyStatement exception"""
        self.called_precmd += 1
        raise exceptions.EmptyStatement

    def precmd_hook_exception(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
        """A precommand hook which raises an exception"""
        self.called_precmd += 1
        raise ValueError

    def precmd_hook_not_enough_parameters(self) -> plugin.PrecommandData:
        """A precommand hook with no parameters"""

    def precmd_hook_too_many_parameters(self, one: plugin.PrecommandData, two: str) -> plugin.PrecommandData:
        """A precommand hook with too many parameters"""
        return one

    def precmd_hook_no_parameter_annotation(self, data) -> plugin.PrecommandData:
        """A precommand hook with no type annotation on the parameter"""
        return data

    def precmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PrecommandData:
        """A precommand hook with the incorrect type annotation on the parameter"""
        return data

    def precmd_hook_no_return_annotation(self, data: plugin.PrecommandData):
        """A precommand hook with no type annotation on the return value"""
        return data

    def precmd_hook_wrong_return_annotation(self, data: plugin.PrecommandData) -> cmd2.Statement:
        return self.statement_parser.parse('hi there')

    ###
    #
    # postcommand hooks, some valid, some invalid
    #
    ###
    def postcmd(self, stop: bool, statement: cmd2.Statement) -> bool:
        """Override cmd.Cmd method"""
        self.called_postcmd += 1
        return stop

    def postcmd_hook(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
        """A postcommand hook"""
        self.called_postcmd += 1
        return data

    def postcmd_hook_exception(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
        """A postcommand hook with raises an exception"""
        self.called_postcmd += 1
        raise ZeroDivisionError

    def postcmd_hook_not_enough_parameters(self) -> plugin.PostcommandData:
        """A precommand hook with no parameters"""

    def postcmd_hook_too_many_parameters(self, one: plugin.PostcommandData, two: str) -> plugin.PostcommandData:
        """A precommand hook with too many parameters"""
        return one

    def postcmd_hook_no_parameter_annotation(self, data) -> plugin.PostcommandData:
        """A precommand hook with no type annotation on the parameter"""
        return data

    def postcmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PostcommandData:
        """A precommand hook with the incorrect type annotation on the parameter"""
        return data

    def postcmd_hook_no_return_annotation(self, data: plugin.PostcommandData):
        """A precommand hook with no type annotation on the return value"""
        return data

    def postcmd_hook_wrong_return_annotation(self, data: plugin.PostcommandData) -> cmd2.Statement:
        return self.statement_parser.parse('hi there')

    ###
    #
    # command finalization hooks, some valid, some invalid
    #
    ###
    def cmdfinalization_hook(self, data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData:
        """A command finalization hook."""
        self.called_cmdfinalization += 1
        return data

    def cmdfinalization_hook_stop(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
        """A command finalization hook which requests application exit"""
        self.called_cmdfinalization += 1
        data.stop = True
        return data

    def cmdfinalization_hook_exception(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
        """A command finalization hook which raises an exception"""
        self.called_cmdfinalization += 1
        raise ValueError

    def cmdfinalization_hook_system_exit(
        self, data: cmd2.plugin.CommandFinalizationData
    ) -> cmd2.plugin.CommandFinalizationData:
        """A command finalization hook which raises a SystemExit"""
        self.called_cmdfinalization += 1
        raise SystemExit(5)

    def cmdfinalization_hook_keyboard_interrupt(
        self, data: cmd2.plugin.CommandFinalizationData
    ) -> cmd2.plugin.CommandFinalizationData:
        """A command finalization hook which raises a KeyboardInterrupt"""
        self.called_cmdfinalization += 1
        raise KeyboardInterrupt

    def cmdfinalization_hook_passthrough_exception(
        self, data: cmd2.plugin.CommandFinalizationData
    ) -> cmd2.plugin.CommandFinalizationData:
        """A command finalization hook which raises a PassThroughException"""
        self.called_cmdfinalization += 1
        wrapped_ex = OSError("Pass me up")
        raise exceptions.PassThroughException(wrapped_ex=wrapped_ex)

    def cmdfinalization_hook_not_enough_parameters(self) -> plugin.CommandFinalizationData:
        """A command finalization hook with no parameters."""

    def cmdfinalization_hook_too_many_parameters(
        self, one: plugin.CommandFinalizationData, two: str
    ) -> plugin.CommandFinalizationData:
        """A command finalization hook with too many parameters."""
        return one

    def cmdfinalization_hook_no_parameter_annotation(self, data) -> plugin.CommandFinalizationData:
        """A command finalization hook with no type annotation on the parameter."""
        return data

    def cmdfinalization_hook_wrong_parameter_annotation(self, data: str) -> plugin.CommandFinalizationData:
        """A command finalization hook with the incorrect type annotation on the parameter."""
        return data

    def cmdfinalization_hook_no_return_annotation(self, data: plugin.CommandFinalizationData):
        """A command finalizationhook with no type annotation on the return value."""
        return data

    def cmdfinalization_hook_wrong_return_annotation(self, data: plugin.CommandFinalizationData) -> cmd2.Statement:
        """A command finalization hook with the wrong return type annotation."""
        return self.statement_parser.parse('hi there')


class PluggedApp(Plugin, cmd2.Cmd):
    """A sample app with a plugin mixed in"""

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

    def do_say(self, statement) -> None:
        """Repeat back the arguments"""
        self.poutput(statement)

    def do_skip_postcmd_hooks(self, _) -> NoReturn:
        self.poutput("In do_skip_postcmd_hooks")
        raise exceptions.SkipPostcommandHooks

    parser = Cmd2ArgumentParser(description="Test parser")
    parser.add_argument("my_arg", help="some help text")

    @with_argparser(parser)
    def do_argparse_cmd(self, namespace: argparse.Namespace) -> None:
        """Repeat back the arguments"""
        self.poutput(namespace.cmd2_statement.get())


###
#
# test pre and postloop hooks
#
###
def test_register_preloop_hook_too_many_parameters() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_preloop_hook(app.prepost_hook_too_many_parameters)


def test_register_preloop_hook_with_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_preloop_hook(app.prepost_hook_with_wrong_return_annotation)


def test_preloop_hook(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", "say hello", 'quit']

    with mock.patch.object(sys, 'argv', testargs):
        app = PluggedApp()

    app.register_preloop_hook(app.prepost_hook_one)
    app.cmdloop()
    out, err = capsys.readouterr()
    assert out == 'one\nhello\n'
    assert not err


def test_preloop_hooks(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", "say hello", 'quit']

    with mock.patch.object(sys, 'argv', testargs):
        app = PluggedApp()

    app.register_preloop_hook(app.prepost_hook_one)
    app.register_preloop_hook(app.prepost_hook_two)
    app.cmdloop()
    out, err = capsys.readouterr()
    assert out == 'one\ntwo\nhello\n'
    assert not err


def test_register_postloop_hook_too_many_parameters() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postloop_hook(app.prepost_hook_too_many_parameters)


def test_register_postloop_hook_with_wrong_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postloop_hook(app.prepost_hook_with_wrong_return_annotation)


def test_postloop_hook(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", "say hello", 'quit']

    with mock.patch.object(sys, 'argv', testargs):
        app = PluggedApp()

    app.register_postloop_hook(app.prepost_hook_one)
    app.cmdloop()
    out, err = capsys.readouterr()
    assert out == 'hello\none\n'
    assert not err


def test_postloop_hooks(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", "say hello", 'quit']

    with mock.patch.object(sys, 'argv', testargs):
        app = PluggedApp()

    app.register_postloop_hook(app.prepost_hook_one)
    app.register_postloop_hook(app.prepost_hook_two)
    app.cmdloop()
    out, err = capsys.readouterr()
    assert out == 'hello\none\ntwo\n'
    assert not err


###
#
# test preparse hook
#
###
def test_preparse(capsys) -> None:
    app = PluggedApp()
    app.register_postparsing_hook(app.preparse)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_preparse == 1


###
#
# test postparsing hooks
#
###
def test_postparsing_hook_too_many_parameters() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postparsing_hook(app.postparse_hook_too_many_parameters)


def test_postparsing_hook_undeclared_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postparsing_hook(app.postparse_hook_undeclared_parameter_annotation)


def test_postparsing_hook_wrong_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postparsing_hook(app.postparse_hook_wrong_parameter_annotation)


def test_postparsing_hook_undeclared_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postparsing_hook(app.postparse_hook_undeclared_return_annotation)


def test_postparsing_hook_wrong_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postparsing_hook(app.postparse_hook_wrong_return_annotation)


def test_postparsing_hook(capsys) -> None:
    app = PluggedApp()
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert not app.called_postparsing

    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_postparsing == 1

    # register the function again, so it should be called twice
    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_postparsing == 2


def test_postparsing_hook_stop_first(capsys) -> None:
    app = PluggedApp()
    app.register_postparsing_hook(app.postparse_hook_stop)
    stop = app.onecmd_plus_hooks('say hello')
    assert app.called_postparsing == 1
    assert stop

    # register another function but it shouldn't be called
    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook)
    stop = app.onecmd_plus_hooks('say hello')
    assert app.called_postparsing == 1
    assert stop


def test_postparsing_hook_stop_second(capsys) -> None:
    app = PluggedApp()
    app.register_postparsing_hook(app.postparse_hook)
    stop = app.onecmd_plus_hooks('say hello')
    assert app.called_postparsing == 1
    assert not stop

    # register another function and make sure it gets called
    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook_stop)
    stop = app.onecmd_plus_hooks('say hello')
    assert app.called_postparsing == 2
    assert stop

    # register a third function which shouldn't be called
    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook)
    stop = app.onecmd_plus_hooks('say hello')
    assert app.called_postparsing == 2
    assert stop


def test_postparsing_hook_emptystatement_first(capsys) -> None:
    app = PluggedApp()
    app.register_postparsing_hook(app.postparse_hook_emptystatement)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    assert app.called_postparsing == 1

    # register another function but it shouldn't be called
    app.reset_counters()
    stop = app.register_postparsing_hook(app.postparse_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    assert app.called_postparsing == 1


def test_postparsing_hook_emptystatement_second(capsys) -> None:
    app = PluggedApp()
    app.register_postparsing_hook(app.postparse_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert not err
    assert app.called_postparsing == 1

    # register another function and make sure it gets called
    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook_emptystatement)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    assert app.called_postparsing == 2

    # register a third function which shouldn't be called
    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    assert app.called_postparsing == 2


def test_postparsing_hook_exception(capsys) -> None:
    app = PluggedApp()
    app.register_postparsing_hook(app.postparse_hook_exception)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert err
    assert app.called_postparsing == 1

    # register another function, but it shouldn't be called
    app.reset_counters()
    app.register_postparsing_hook(app.postparse_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert err
    assert app.called_postparsing == 1


###
#
# test precmd hooks
#
#####
def test_register_precmd_hook_parameter_count() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_precmd_hook(app.precmd_hook_not_enough_parameters)
    with pytest.raises(TypeError):
        app.register_precmd_hook(app.precmd_hook_too_many_parameters)


def test_register_precmd_hook_no_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_precmd_hook(app.precmd_hook_no_parameter_annotation)


def test_register_precmd_hook_wrong_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_precmd_hook(app.precmd_hook_wrong_parameter_annotation)


def test_register_precmd_hook_no_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_precmd_hook(app.precmd_hook_no_return_annotation)


def test_register_precmd_hook_wrong_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_precmd_hook(app.precmd_hook_wrong_return_annotation)


def test_precmd_hook(capsys) -> None:
    app = PluggedApp()
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    # without registering any hooks, precmd() should be called
    assert app.called_precmd == 1

    app.reset_counters()
    app.register_precmd_hook(app.precmd_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    # with one hook registered, we should get precmd() and the hook
    assert app.called_precmd == 2

    # register the function again, so it should be called twice
    app.reset_counters()
    app.register_precmd_hook(app.precmd_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    # with two hooks registered, we should get precmd() and both hooks
    assert app.called_precmd == 3


def test_precmd_hook_emptystatement_first(capsys) -> None:
    app = PluggedApp()
    app.register_precmd_hook(app.precmd_hook_emptystatement)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    # since the registered hooks are called before precmd(), if a registered
    # hook throws an exception, precmd() is never called
    assert app.called_precmd == 1

    # register another function but it shouldn't be called
    app.reset_counters()
    stop = app.register_precmd_hook(app.precmd_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    # the exception raised by the first hook should prevent the second
    # hook from being called, and it also prevents precmd() from being
    # called
    assert app.called_precmd == 1


def test_precmd_hook_emptystatement_second(capsys) -> None:
    app = PluggedApp()
    app.register_precmd_hook(app.precmd_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert not err
    # with one hook registered, we should get precmd() and the hook
    assert app.called_precmd == 2

    # register another function and make sure it gets called
    app.reset_counters()
    app.register_precmd_hook(app.precmd_hook_emptystatement)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    # since the registered hooks are called before precmd(), if a registered
    # hook throws an exception, precmd() is never called
    assert app.called_precmd == 2

    # register a third function which shouldn't be called
    app.reset_counters()
    app.register_precmd_hook(app.precmd_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert not out
    assert not err
    # the exception raised by the second hook should prevent the third
    # hook from being called. since the registered hooks are called before precmd(),
    # if a registered hook throws an exception, precmd() is never called
    assert app.called_precmd == 2


###
#
# test postcmd hooks
#
####
def test_register_postcmd_hook_parameter_count() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postcmd_hook(app.postcmd_hook_not_enough_parameters)
    with pytest.raises(TypeError):
        app.register_postcmd_hook(app.postcmd_hook_too_many_parameters)


def test_register_postcmd_hook_no_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postcmd_hook(app.postcmd_hook_no_parameter_annotation)


def test_register_postcmd_hook_wrong_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postcmd_hook(app.postcmd_hook_wrong_parameter_annotation)


def test_register_postcmd_hook_no_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postcmd_hook(app.postcmd_hook_no_return_annotation)


def test_register_postcmd_hook_wrong_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_postcmd_hook(app.postcmd_hook_wrong_return_annotation)


def test_postcmd(capsys) -> None:
    app = PluggedApp()
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    # without registering any hooks, postcmd() should be called
    assert app.called_postcmd == 1

    app.reset_counters()
    app.register_postcmd_hook(app.postcmd_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    # with one hook registered, we should get precmd() and the hook
    assert app.called_postcmd == 2

    # register the function again, so it should be called twice
    app.reset_counters()
    app.register_postcmd_hook(app.postcmd_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    # with two hooks registered, we should get precmd() and both hooks
    assert app.called_postcmd == 3


def test_postcmd_exception_first(capsys) -> None:
    app = PluggedApp()
    app.register_postcmd_hook(app.postcmd_hook_exception)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert err
    # since the registered hooks are called before postcmd(), if a registered
    # hook throws an exception, postcmd() is never called. So we should have
    # a count of one because we called the hook that raised the exception
    assert app.called_postcmd == 1

    # register another function but it shouldn't be called
    app.reset_counters()
    stop = app.register_postcmd_hook(app.postcmd_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert err
    # the exception raised by the first hook should prevent the second
    # hook from being called, and it also prevents postcmd() from being
    # called
    assert app.called_postcmd == 1


def test_postcmd_exception_second(capsys) -> None:
    app = PluggedApp()
    app.register_postcmd_hook(app.postcmd_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert not err
    # with one hook registered, we should get the hook and postcmd()
    assert app.called_postcmd == 2

    # register another function which should be called
    app.reset_counters()
    stop = app.register_postcmd_hook(app.postcmd_hook_exception)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert err
    # the exception raised by the first hook should prevent the second
    # hook from being called, and it also prevents postcmd() from being
    # called. So we have the first hook, and the second hook, which raised
    # the exception
    assert app.called_postcmd == 2


##
#
# command finalization
#
###
def test_register_cmdfinalization_hook_parameter_count() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_cmdfinalization_hook(app.cmdfinalization_hook_not_enough_parameters)
    with pytest.raises(TypeError):
        app.register_cmdfinalization_hook(app.cmdfinalization_hook_too_many_parameters)


def test_register_cmdfinalization_hook_no_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_parameter_annotation)


def test_register_cmdfinalization_hook_wrong_parameter_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_parameter_annotation)


def test_register_cmdfinalization_hook_no_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_return_annotation)


def test_register_cmdfinalization_hook_wrong_return_annotation() -> None:
    app = PluggedApp()
    with pytest.raises(TypeError):
        app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_return_annotation)


def test_cmdfinalization(capsys) -> None:
    app = PluggedApp()
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_cmdfinalization == 0

    app.register_cmdfinalization_hook(app.cmdfinalization_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_cmdfinalization == 1

    # register the function again, so it should be called twice
    app.reset_counters()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook)
    app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_cmdfinalization == 2


def test_cmdfinalization_stop_first(capsys) -> None:
    app = PluggedApp()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop)
    app.register_cmdfinalization_hook(app.cmdfinalization_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_cmdfinalization == 2
    assert stop


def test_cmdfinalization_stop_second(capsys) -> None:
    app = PluggedApp()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook)
    app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert out == 'hello\n'
    assert not err
    assert app.called_cmdfinalization == 2
    assert stop


def test_cmdfinalization_hook_exception(capsys) -> None:
    app = PluggedApp()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook_exception)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert err
    assert app.called_cmdfinalization == 1

    # register another function, but it shouldn't be called
    app.reset_counters()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook)
    stop = app.onecmd_plus_hooks('say hello')
    out, err = capsys.readouterr()
    assert not stop
    assert out == 'hello\n'
    assert err
    assert app.called_cmdfinalization == 1


def test_cmdfinalization_hook_system_exit() -> None:
    app = PluggedApp()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook_system_exit)
    stop = app.onecmd_plus_hooks('say hello')
    assert stop
    assert app.called_cmdfinalization == 1
    assert app.exit_code == 5


def test_cmdfinalization_hook_keyboard_interrupt() -> None:
    app = PluggedApp()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook_keyboard_interrupt)

    # First make sure KeyboardInterrupt isn't raised unless told to
    stop = app.onecmd_plus_hooks('say hello', raise_keyboard_interrupt=False)
    assert not stop
    assert app.called_cmdfinalization == 1

    # Now enable raising the KeyboardInterrupt
    app.reset_counters()
    with pytest.raises(KeyboardInterrupt):
        stop = app.onecmd_plus_hooks('say hello', raise_keyboard_interrupt=True)
    assert not stop
    assert app.called_cmdfinalization == 1

    # Now make sure KeyboardInterrupt isn't raised if stop is already True
    app.reset_counters()
    stop = app.onecmd_plus_hooks('quit', raise_keyboard_interrupt=True)
    assert stop
    assert app.called_cmdfinalization == 1


def test_cmdfinalization_hook_passthrough_exception() -> None:
    app = PluggedApp()
    app.register_cmdfinalization_hook(app.cmdfinalization_hook_passthrough_exception)

    expected_err = "Pass me up"
    with pytest.raises(OSError, match=expected_err):
        app.onecmd_plus_hooks('say hello')
    assert app.called_cmdfinalization == 1


def test_skip_postcmd_hooks(capsys) -> None:
    app = PluggedApp()
    app.register_postcmd_hook(app.postcmd_hook)
    app.register_cmdfinalization_hook(app.cmdfinalization_hook)

    # Cause a SkipPostcommandHooks exception and verify no postcmd stuff runs but cmdfinalization_hook still does
    app.onecmd_plus_hooks('skip_postcmd_hooks')
    out, _err = capsys.readouterr()
    assert "In do_skip_postcmd_hooks" in out
    assert app.called_postcmd == 0
    assert app.called_cmdfinalization == 1


def test_cmd2_argparse_exception(capsys) -> None:
    """Verify Cmd2ArgparseErrors raised after calling a command prevent postcmd events from
    running but do not affect cmdfinalization events
    """
    app = PluggedApp()
    app.register_postcmd_hook(app.postcmd_hook)
    app.register_cmdfinalization_hook(app.cmdfinalization_hook)

    # First generate no exception and make sure postcmd_hook, postcmd, and cmdfinalization_hook run
    app.onecmd_plus_hooks('argparse_cmd arg_val')
    out, err = capsys.readouterr()
    assert out == 'arg_val\n'
    assert not err
    assert app.called_postcmd == 2
    assert app.called_cmdfinalization == 1

    app.reset_counters()

    # Next cause an argparse exception and verify no postcmd stuff runs but cmdfinalization_hook still does
    app.onecmd_plus_hooks('argparse_cmd')
    out, err = capsys.readouterr()
    assert not out
    assert "Error: the following arguments are required: my_arg" in err
    assert app.called_postcmd == 0
    assert app.called_cmdfinalization == 1
