# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for the helptext module."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import sys
import textwrap

from afire import formatting
from afire import helptext
from afire import test_components as tc
from afire import testutils
from afire import trace
import six


class HelpTest(testutils.BaseTestCase):
    def setUp(self):
        super(HelpTest, self).setUp()
        os.environ["ANSI_COLORS_DISABLED"] = "1"

    def testHelpTextNoDefaults(self):
        component = tc.NoDefaults
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="NoDefaults"))
        self.assertIn("NAME\n    NoDefaults", help_screen)
        self.assertIn("SYNOPSIS\n    NoDefaults", help_screen)
        self.assertNotIn("DESCRIPTION", help_screen)
        self.assertNotIn("NOTES", help_screen)

    def testHelpTextNoDefaultsObject(self):
        component = tc.NoDefaults()
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="NoDefaults"))
        self.assertIn("NAME\n    NoDefaults", help_screen)
        self.assertIn("SYNOPSIS\n    NoDefaults COMMAND", help_screen)
        self.assertNotIn("DESCRIPTION", help_screen)
        self.assertIn("COMMANDS\n    COMMAND is one of the following:", help_screen)
        self.assertIn("double", help_screen)
        self.assertIn("triple", help_screen)
        self.assertNotIn("NOTES", help_screen)

    def testHelpTextFunction(self):
        component = tc.NoDefaults().double
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="double"))
        self.assertIn("NAME\n    double", help_screen)
        self.assertIn("SYNOPSIS\n    double COUNT", help_screen)
        self.assertNotIn("DESCRIPTION", help_screen)
        self.assertIn("POSITIONAL ARGUMENTS\n    COUNT", help_screen)
        self.assertIn("NOTES\n    You can also use flags syntax for POSITIONAL ARGUMENTS", help_screen)

    def testHelpTextFunctionWithDefaults(self):
        component = tc.WithDefaults().triple
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="triple"))
        self.assertIn("NAME\n    triple", help_screen)
        self.assertIn("SYNOPSIS\n    triple <flags>", help_screen)
        self.assertNotIn("DESCRIPTION", help_screen)
        self.assertIn("FLAGS\n    -c, --count=COUNT\n        Default: 0", help_screen)
        self.assertNotIn("NOTES", help_screen)

    def testHelpTextFunctionWithLongDefaults(self):
        component = tc.WithDefaults().text
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="text"))
        self.assertIn("NAME\n    text", help_screen)
        self.assertIn("SYNOPSIS\n    text <flags>", help_screen)
        self.assertNotIn("DESCRIPTION", help_screen)
        self.assertIn(
            "FLAGS\n    -s, --string=STRING\n"
            "        Default: '0001020304050607080910"
            "1112131415161718192021222324252627282...",
            help_screen,
        )
        self.assertNotIn("NOTES", help_screen)

    def testHelpTextFunctionWithKwargs(self):
        component = tc.fn_with_kwarg
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="text"))
        self.assertIn("NAME\n    text", help_screen)
        self.assertIn("SYNOPSIS\n    text ARG1 ARG2 <flags>", help_screen)
        self.assertIn("DESCRIPTION\n    Function with kwarg", help_screen)
        self.assertIn(
            "FLAGS\n    --arg3\n        Description of arg3.\n    "
            "Additional undocumented flags may also be accepted.",
            help_screen,
        )

    def testHelpTextFunctionWithKwargsAndDefaults(self):
        component = tc.fn_with_kwarg_and_defaults
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="text"))
        self.assertIn("NAME\n    text", help_screen)
        self.assertIn("SYNOPSIS\n    text ARG1 ARG2 <flags>", help_screen)
        self.assertIn("DESCRIPTION\n    Function with kwarg", help_screen)
        self.assertIn(
            "FLAGS\n    -o, --opt=OPT\n        Default: True\n"
            "    The following flags are also accepted."
            "\n    --arg3\n        Description of arg3.\n    "
            "Additional undocumented flags may also be accepted.",
            help_screen,
        )

    @testutils.skipIf(sys.version_info[0:2] < (3, 5), "Python < 3.5 does not support type hints.")
    def testHelpTextFunctionWithDefaultsAndTypes(self):
        component = tc.py3.WithDefaultsAndTypes().double  # pytype: disable=module-attr
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="double"))
        self.assertIn("NAME\n    double", help_screen)
        self.assertIn("SYNOPSIS\n    double <flags>", help_screen)
        self.assertIn("DESCRIPTION", help_screen)
        self.assertIn("FLAGS\n    -c, --count=COUNT\n        Type: float\n        Default: 0", help_screen)
        self.assertNotIn("NOTES", help_screen)

    @testutils.skipIf(sys.version_info[0:2] < (3, 5), "Python < 3.5 does not support type hints.")
    def testHelpTextFunctionWithTypesAndDefaultNone(self):
        component = tc.py3.WithDefaultsAndTypes().get_int  # pytype: disable=module-attr
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="get_int"))
        self.assertIn("NAME\n    get_int", help_screen)
        self.assertIn("SYNOPSIS\n    get_int <flags>", help_screen)
        self.assertNotIn("DESCRIPTION", help_screen)
        self.assertIn(
            "FLAGS\n    -v, --value=VALUE\n" "        Type: Optional[int]\n        Default: None", help_screen
        )
        self.assertNotIn("NOTES", help_screen)

    @testutils.skipIf(sys.version_info[0:2] < (3, 5), "Python < 3.5 does not support type hints.")
    def testHelpTextFunctionWithTypes(self):
        component = tc.py3.WithTypes().double  # pytype: disable=module-attr
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="double"))
        self.assertIn("NAME\n    double", help_screen)
        self.assertIn("SYNOPSIS\n    double COUNT", help_screen)
        self.assertIn("DESCRIPTION", help_screen)
        self.assertIn("POSITIONAL ARGUMENTS\n    COUNT\n        Type: float", help_screen)
        self.assertIn("NOTES\n    You can also use flags syntax for POSITIONAL ARGUMENTS", help_screen)

    @testutils.skipIf(sys.version_info[0:2] < (3, 5), "Python < 3.5 does not support type hints.")
    def testHelpTextFunctionWithLongTypes(self):
        component = tc.py3.WithTypes().long_type  # pytype: disable=module-attr
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, name="long_type"))
        self.assertIn("NAME\n    long_type", help_screen)
        self.assertIn("SYNOPSIS\n    long_type LONG_OBJ", help_screen)
        self.assertNotIn("DESCRIPTION", help_screen)
        # TODO(dbieber): Assert type is displayed correctly. Type displayed
        # differently in Travis vs in Google.
        # self.assertIn(
        #     'POSITIONAL ARGUMENTS\n    LONG_OBJ\n'
        #     '        Type: typing.Tuple[typing.Tuple['
        #     'typing.Tuple[typing.Tuple[typing.Tupl...',
        #     help_screen)
        self.assertIn("NOTES\n    You can also use flags syntax for POSITIONAL ARGUMENTS", help_screen)

    def testHelpTextFunctionWithBuiltin(self):
        component = "test".upper
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, "upper"))
        self.assertIn("NAME\n    upper", help_screen)
        self.assertIn("SYNOPSIS\n    upper", help_screen)
        # We don't check description content here since the content is python
        # version dependent.
        self.assertIn("DESCRIPTION\n", help_screen)
        self.assertNotIn("NOTES", help_screen)

    def testHelpTextFunctionIntType(self):
        component = int
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, "int"))
        self.assertIn("NAME\n    int", help_screen)
        self.assertIn("SYNOPSIS\n    int", help_screen)
        # We don't check description content here since the content is python
        # version dependent.
        self.assertIn("DESCRIPTION\n", help_screen)

    def testHelpTextEmptyList(self):
        component = []
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, "list"))
        self.assertIn("NAME\n    list", help_screen)
        self.assertIn("SYNOPSIS\n    list COMMAND", help_screen)
        # TODO(zuhaochen): Change assertion after custom description is
        # implemented for list type.
        self.assertNotIn("DESCRIPTION", help_screen)
        # We don't check the listed commands either since the list API could
        # potentially change between Python versions.
        self.assertIn("COMMANDS\n    COMMAND is one of the following:\n", help_screen)

    def testHelpTextShortList(self):
        component = [10]
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, "list"))
        self.assertIn("NAME\n    list", help_screen)
        self.assertIn("SYNOPSIS\n    list COMMAND", help_screen)
        # TODO(zuhaochen): Change assertion after custom description is
        # implemented for list type.
        self.assertNotIn("DESCRIPTION", help_screen)

        # We don't check the listed commands comprehensively since the list API
        # could potentially change between Python versions. Check a few
        # functions(command) that we're confident likely remain available.
        self.assertIn("COMMANDS\n    COMMAND is one of the following:\n", help_screen)
        self.assertIn("     append\n", help_screen)

    def testHelpTextInt(self):
        component = 7
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, "7"))
        self.assertIn("NAME\n    7", help_screen)
        self.assertIn("SYNOPSIS\n    7 COMMAND | VALUE", help_screen)
        # TODO(zuhaochen): Change assertion after implementing custom
        # description for int.
        self.assertNotIn("DESCRIPTION", help_screen)
        self.assertIn("COMMANDS\n    COMMAND is one of the following:\n", help_screen)
        self.assertIn("VALUES\n    VALUE is one of the following:\n", help_screen)

    def testHelpTextNoInit(self):
        component = tc.OldStyleEmpty
        help_screen = helptext.HelpText(component=component, trace=trace.FireTrace(component, "OldStyleEmpty"))
        self.assertIn("NAME\n    OldStyleEmpty", help_screen)
        self.assertIn("SYNOPSIS\n    OldStyleEmpty", help_screen)

    @testutils.skipIf(six.PY2, "Python 2 does not support keyword-only arguments.")
    def testHelpTextKeywordOnlyArgumentsWithDefault(self):
        component = tc.py3.KeywordOnly.with_default  # pytype: disable=module-attr
        output = helptext.HelpText(component=component, trace=trace.FireTrace(component, "with_default"))
        self.assertIn("NAME\n    with_default", output)
        self.assertIn("FLAGS\n    -x, --x=X", output)

    @testutils.skipIf(six.PY2, "Python 2 does not support keyword-only arguments.")
    def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
        component = tc.py3.KeywordOnly.double  # pytype: disable=module-attr
        output = helptext.HelpText(component=component, trace=trace.FireTrace(component, "double"))
        self.assertIn("NAME\n    double", output)
        self.assertIn("FLAGS\n    -c, --count=COUNT (required)", output)

    @testutils.skipIf(six.PY2, "Python 2 does not support required name-only arguments.")
    def testHelpTextFunctionMixedDefaults(self):
        component = tc.py3.HelpTextComponent().identity
        t = trace.FireTrace(component, name="FunctionMixedDefaults")
        output = helptext.HelpText(component, trace=t)
        self.assertIn("NAME\n    FunctionMixedDefaults", output)
        self.assertIn("FunctionMixedDefaults <flags>", output)
        self.assertIn("--alpha=ALPHA (required)", output)
        self.assertIn("--beta=BETA\n        Default: '0'", output)

    def testHelpScreen(self):
        component = tc.ClassWithDocstring()
        t = trace.FireTrace(component, name="ClassWithDocstring")
        help_output = helptext.HelpText(component, t)
        expected_output = """
NAME
    ClassWithDocstring - Test class for testing help text output.

SYNOPSIS
    ClassWithDocstring COMMAND | VALUE

DESCRIPTION
    This is some detail description of this test class.

COMMANDS
    COMMAND is one of the following:

     print_msg
       Prints a message.

VALUES
    VALUE is one of the following:

     message
       The default message to print."""
        self.assertEqual(textwrap.dedent(expected_output).strip(), help_output.strip())

    def testHelpScreenForFunctionDocstringWithLineBreak(self):
        component = tc.ClassWithMultilineDocstring.example_generator
        t = trace.FireTrace(component, name="example_generator")
        help_output = helptext.HelpText(component, t)
        expected_output = """
    NAME
        example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.

    SYNOPSIS
        example_generator N

    DESCRIPTION
        Generators have a ``Yields`` section instead of a ``Returns`` section.

    POSITIONAL ARGUMENTS
        N
            The upper limit of the range to generate, from 0 to `n` - 1.

    NOTES
        You can also use flags syntax for POSITIONAL ARGUMENTS"""
        self.assertEqual(textwrap.dedent(expected_output).strip(), help_output.strip())

    def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
        component = tc.WithDefaults().double
        t = trace.FireTrace(component, name="double")
        help_output = helptext.HelpText(component, t)
        expected_output = """
    NAME
        double - Returns the input multiplied by 2.

    SYNOPSIS
        double <flags>

    DESCRIPTION
        Returns the input multiplied by 2.

    FLAGS
        -c, --count=COUNT
            Default: 0
            Input number that you want to double."""
        self.assertEqual(textwrap.dedent(expected_output).strip(), help_output.strip())

    def testHelpTextUnderlineFlag(self):
        component = tc.WithDefaults().triple
        t = trace.FireTrace(component, name="triple")
        help_screen = helptext.HelpText(component, t)
        self.assertIn(formatting.Bold("NAME") + "\n    triple", help_screen)
        self.assertIn(formatting.Bold("SYNOPSIS") + "\n    triple <flags>", help_screen)
        self.assertIn(formatting.Bold("FLAGS") + "\n    -c, --" + formatting.Underline("count"), help_screen)

    def testHelpTextBoldCommandName(self):
        component = tc.ClassWithDocstring()
        t = trace.FireTrace(component, name="ClassWithDocstring")
        help_screen = helptext.HelpText(component, t)
        self.assertIn(formatting.Bold("NAME") + "\n    ClassWithDocstring", help_screen)
        self.assertIn(formatting.Bold("COMMANDS") + "\n", help_screen)
        self.assertIn(formatting.BoldUnderline("COMMAND") + " is one of the following:\n", help_screen)
        self.assertIn(formatting.Bold("print_msg") + "\n", help_screen)

    def testHelpTextObjectWithGroupAndValues(self):
        component = tc.TypedProperties()
        t = trace.FireTrace(component, name="TypedProperties")
        help_screen = helptext.HelpText(component=component, trace=t, verbose=True)
        print(help_screen)
        self.assertIn("GROUPS", help_screen)
        self.assertIn("GROUP is one of the following:", help_screen)
        self.assertIn("charlie\n       Class with functions that have default arguments.", help_screen)
        self.assertIn("VALUES", help_screen)
        self.assertIn("VALUE is one of the following:", help_screen)
        self.assertIn("alpha", help_screen)

    def testHelpTextNameSectionCommandWithSeparator(self):
        component = 9
        t = trace.FireTrace(component, name="int", separator="-")
        t.AddSeparator()
        help_screen = helptext.HelpText(component=component, trace=t, verbose=False)
        self.assertIn("int -", help_screen)
        self.assertNotIn("int - -", help_screen)

    def testHelpTextNameSectionCommandWithSeparatorVerbose(self):
        component = tc.WithDefaults().double
        t = trace.FireTrace(component, name="double", separator="-")
        t.AddSeparator()
        help_screen = helptext.HelpText(component=component, trace=t, verbose=True)
        self.assertIn("double -", help_screen)
        self.assertIn("double - -", help_screen)

    def testHelpTextMultipleKeywoardArgumentsWithShortArgs(self):
        component = tc.fn_with_multiple_defaults
        t = trace.FireTrace(component, name="shortargs")
        help_screen = helptext.HelpText(component, t)
        self.assertIn(formatting.Bold("NAME") + "\n    shortargs", help_screen)
        self.assertIn(formatting.Bold("SYNOPSIS") + "\n    shortargs <flags>", help_screen)
        self.assertIn(formatting.Bold("FLAGS") + "\n    -f, --first", help_screen)
        self.assertIn("\n    --last", help_screen)
        self.assertIn("\n    --late", help_screen)


class UsageTest(testutils.BaseTestCase):
    def testUsageOutput(self):
        component = tc.NoDefaults()
        t = trace.FireTrace(component, name="NoDefaults")
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = """
    Usage: NoDefaults <command>
      available commands:    double | triple

    For detailed information on this command, run:
      NoDefaults --help"""

        self.assertEqual(usage_output, textwrap.dedent(expected_output).lstrip("\n"))

    def testUsageOutputVerbose(self):
        component = tc.NoDefaults()
        t = trace.FireTrace(component, name="NoDefaults")
        usage_output = helptext.UsageText(component, trace=t, verbose=True)
        expected_output = """
    Usage: NoDefaults <command>
      available commands:    double | triple

    For detailed information on this command, run:
      NoDefaults --help"""
        self.assertEqual(usage_output, textwrap.dedent(expected_output).lstrip("\n"))

    def testUsageOutputMethod(self):
        component = tc.NoDefaults().double
        t = trace.FireTrace(component, name="NoDefaults")
        t.AddAccessedProperty(component, "double", ["double"], None, None)
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = """
    Usage: NoDefaults double COUNT

    For detailed information on this command, run:
      NoDefaults double --help"""
        self.assertEqual(usage_output, textwrap.dedent(expected_output).lstrip("\n"))

    def testUsageOutputFunctionWithHelp(self):
        component = tc.function_with_help
        t = trace.FireTrace(component, name="function_with_help")
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = """
    Usage: function_with_help <flags>
      optional flags:        --help

    For detailed information on this command, run:
      function_with_help -- --help"""
        self.assertEqual(usage_output, textwrap.dedent(expected_output).lstrip("\n"))

    def testUsageOutputFunctionWithDocstring(self):
        component = tc.multiplier_with_docstring
        t = trace.FireTrace(component, name="multiplier_with_docstring")
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = """
    Usage: multiplier_with_docstring NUM <flags>
      optional flags:        --rate

    For detailed information on this command, run:
      multiplier_with_docstring --help"""
        self.assertEqual(textwrap.dedent(expected_output).lstrip("\n"), usage_output)

    @testutils.skipIf(six.PY2, "Python 2 does not support required name-only arguments.")
    def testUsageOutputFunctionMixedDefaults(self):
        component = tc.py3.HelpTextComponent().identity
        t = trace.FireTrace(component, name="FunctionMixedDefaults")
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = """
    Usage: FunctionMixedDefaults <flags>
      optional flags:        --beta
      required flags:        --alpha

    For detailed information on this command, run:
      FunctionMixedDefaults --help"""
        expected_output = textwrap.dedent(expected_output).lstrip("\n")
        self.assertEqual(expected_output, usage_output)

    def testUsageOutputCallable(self):
        # This is both a group and a command.
        component = tc.CallableWithKeywordArgument()
        t = trace.FireTrace(component, name="CallableWithKeywordArgument", separator="@")
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = """
    Usage: CallableWithKeywordArgument <command> | <flags>
      available commands:    print_msg
      flags are accepted

    For detailed information on this command, run:
      CallableWithKeywordArgument -- --help"""
        self.assertEqual(textwrap.dedent(expected_output).lstrip("\n"), usage_output)

    def testUsageOutputConstructorWithParameter(self):
        component = tc.InstanceVars
        t = trace.FireTrace(component, name="InstanceVars")
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = """
    Usage: InstanceVars --arg1=ARG1 --arg2=ARG2

    For detailed information on this command, run:
      InstanceVars --help"""
        self.assertEqual(textwrap.dedent(expected_output).lstrip("\n"), usage_output)

    def testUsageOutputConstructorWithParameterVerbose(self):
        component = tc.InstanceVars
        t = trace.FireTrace(component, name="InstanceVars")
        usage_output = helptext.UsageText(component, trace=t, verbose=True)
        expected_output = """
    Usage: InstanceVars <command> | --arg1=ARG1 --arg2=ARG2
      available commands:    run

    For detailed information on this command, run:
      InstanceVars --help"""
        self.assertEqual(textwrap.dedent(expected_output).lstrip("\n"), usage_output)

    def testUsageOutputEmptyDict(self):
        component = {}
        t = trace.FireTrace(component, name="EmptyDict")
        usage_output = helptext.UsageText(component, trace=t, verbose=True)
        expected_output = """
    Usage: EmptyDict

    For detailed information on this command, run:
      EmptyDict --help"""
        self.assertEqual(textwrap.dedent(expected_output).lstrip("\n"), usage_output)

    def testUsageOutputNone(self):
        component = None
        t = trace.FireTrace(component, name="None")
        usage_output = helptext.UsageText(component, trace=t, verbose=True)
        expected_output = """
    Usage: None

    For detailed information on this command, run:
      None --help"""
        self.assertEqual(textwrap.dedent(expected_output).lstrip("\n"), usage_output)

    def testInitRequiresFlagSyntaxSubclassNamedTuple(self):
        component = tc.SubPoint
        t = trace.FireTrace(component, name="SubPoint")
        usage_output = helptext.UsageText(component, trace=t, verbose=False)
        expected_output = "Usage: SubPoint --x=X --y=Y"
        self.assertIn(expected_output, usage_output)


if __name__ == "__main__":
    testutils.main()
