# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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.
import json

from botocore.model import (
    DenormalizedStructureBuilder,
    ListShape,
    MapShape,
    Shape,
    ShapeResolver,
    StringShape,
    StructureShape,
)

from awscli.arguments import CustomArgument
from awscli.bcdoc.restdoc import ReSTDocument
from awscli.clidocs import (
    CLIDocumentEventHandler,
    GlobalOptionsDocumenter,
    OperationDocumentEventHandler,
    ServiceDocumentEventHandler,
    TopicDocumentEventHandler,
    TopicListerDocumentEventHandler,
)
from awscli.help import (
    HelpCommand,
    ServiceHelpCommand,
    TopicHelpCommand,
    TopicListerCommand,
)
from awscli.testutils import FileCreator, mock, unittest


class TestRecursiveShapes(unittest.TestCase):
    def setUp(self):
        self.arg_table = {}
        self.help_command = mock.Mock()
        self.help_command.event_class = 'custom'
        self.help_command.arg_table = self.arg_table
        self.operation_model = mock.Mock()
        self.operation_model.service_model.operation_names = []
        self.help_command.obj = self.operation_model
        self.operation_handler = OperationDocumentEventHandler(
            self.help_command
        )

    def assert_rendered_docs_contain(self, expected):
        writes = [
            args[0][0] for args in self.help_command.doc.write.call_args_list
        ]
        writes = '\n'.join(writes)
        self.assertIn(expected, writes)

    def assert_proper_indentation(self):
        indent = self.help_command.doc.style.indent.call_count
        dedent = self.help_command.doc.style.dedent.call_count
        message = 'Imbalanced indentation: indent (%s) != dedent (%s)'
        self.assertEqual(indent, dedent, message % (indent, dedent))

    def test_handle_recursive_input(self):
        shape_map = {
            'RecursiveStruct': {
                'type': 'structure',
                'members': {
                    'A': {'shape': 'NonRecursive'},
                    'B': {'shape': 'RecursiveStruct'},
                },
            },
            'NonRecursive': {'type': 'string'},
        }
        shape = StructureShape(
            'RecursiveStruct',
            shape_map['RecursiveStruct'],
            ShapeResolver(shape_map),
        )

        self.arg_table['arg-name'] = mock.Mock(argument_model=shape)
        self.operation_handler.doc_option_example(
            'arg-name', self.help_command, 'process-cli-arg.foo.bar'
        )
        self.assert_rendered_docs_contain('{ ... recursive ... }')

    def test_handle_recursive_output(self):
        shape_map = {
            'RecursiveStruct': {
                'type': 'structure',
                'members': {
                    'A': {'shape': 'NonRecursive'},
                    'B': {'shape': 'RecursiveStruct'},
                },
            },
            'NonRecursive': {'type': 'string'},
        }
        shape = StructureShape(
            'RecursiveStruct',
            shape_map['RecursiveStruct'],
            ShapeResolver(shape_map),
        )

        operation_model = mock.Mock()
        operation_model.output_shape = shape
        self.help_command.obj = operation_model
        self.operation_handler.doc_output(self.help_command, 'event-name')
        self.assert_rendered_docs_contain('( ... recursive ... )')

    def test_handle_empty_nested_struct(self):
        shape_map = {
            'InputStruct': {
                'type': 'structure',
                'members': {
                    'A': {'shape': 'Empty'},
                },
            },
            'Empty': {'type': 'structure', 'members': {}},
        }
        shape = StructureShape(
            'InputStruct', shape_map['InputStruct'], ShapeResolver(shape_map)
        )

        self.arg_table['arg-name'] = mock.Mock(argument_model=shape)
        self.operation_handler.doc_option_example(
            'arg-name', self.help_command, 'process-cli-arg.foo.bar'
        )
        self.assert_proper_indentation()

    def test_handle_no_output_shape(self):
        operation_model = mock.Mock()
        operation_model.output_shape = None
        self.help_command.obj = operation_model
        self.operation_handler.doc_output(self.help_command, 'event-name')
        self.assert_rendered_docs_contain('None')

    def test_handle_memberless_output_shape(self):
        shape_map = {'NoMembers': {'type': 'structure', 'members': {}}}
        shape = StructureShape(
            'NoMembers', shape_map['NoMembers'], ShapeResolver(shape_map)
        )

        operation_model = mock.Mock()
        operation_model.output_shape = shape
        self.help_command.obj = operation_model
        self.operation_handler.doc_output(self.help_command, 'event-name')
        self.assert_rendered_docs_contain('None')


class TestCLIDocumentEventHandler(unittest.TestCase):
    def setUp(self):
        self.session = mock.Mock()
        self.obj = None
        self.command_table = {}
        self.arg_table = {}
        self.name = 'my-command'
        self.event_class = 'aws'

    def create_help_command(self):
        help_command = mock.Mock()
        help_command.doc = ReSTDocument()
        help_command.event_class = 'custom'
        help_command.arg_table = {}
        operation_model = mock.Mock()
        operation_model.documentation = 'description'
        operation_model.service_model.operation_names = []
        help_command.obj = operation_model
        return help_command

    def create_tagged_union_shape(self):
        shape_model = {'type': 'structure', 'union': True, 'members': {}}
        tagged_union = StructureShape('tagged_union', shape_model)
        return tagged_union

    def get_help_docs_for_argument(self, shape):
        arg_table = {'arg-name': mock.Mock(argument_model=shape)}
        help_command = mock.Mock()
        help_command.doc = ReSTDocument()
        help_command.event_class = 'custom'
        help_command.arg_table = arg_table
        operation_model = mock.Mock()
        operation_model.service_model.operation_names = []
        help_command.obj = operation_model
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_option('arg-name', help_command)
        return help_command.doc.getvalue().decode('utf-8')

    def test_breadcrumbs_man(self):
        # Create an arbitrary help command class. This was chosen
        # because it is fairly easy to instantiate.
        help_cmd = ServiceHelpCommand(
            self.session,
            self.obj,
            self.command_table,
            self.arg_table,
            self.name,
            self.event_class,
        )

        doc_handler = CLIDocumentEventHandler(help_cmd)
        doc_handler.doc_breadcrumbs(help_cmd)
        # These should not show up in the man page
        self.assertEqual(help_cmd.doc.getvalue().decode('utf-8'), '')

    def test_breadcrumbs_html(self):
        help_cmd = ServiceHelpCommand(
            self.session,
            self.obj,
            self.command_table,
            self.arg_table,
            self.name,
            self.event_class,
        )
        help_cmd.doc.target = 'html'
        doc_handler = CLIDocumentEventHandler(help_cmd)
        doc_handler.doc_breadcrumbs(help_cmd)
        self.assertEqual(
            help_cmd.doc.getvalue().decode('utf-8'), '[ :ref:`aws <cli:aws>` ]\n\n'
        )

    def test_breadcrumbs_service_command_html(self):
        help_cmd = ServiceHelpCommand(
            self.session,
            self.obj,
            self.command_table,
            self.arg_table,
            self.name,
            'ec2',
        )
        help_cmd.doc.target = 'html'
        doc_handler = CLIDocumentEventHandler(help_cmd)
        doc_handler.doc_breadcrumbs(help_cmd)
        self.assertEqual(
            help_cmd.doc.getvalue().decode('utf-8'), '[ :ref:`aws <cli:aws>` ]\n\n'
        )

    def test_breadcrumbs_operation_command_html(self):
        help_cmd = ServiceHelpCommand(
            self.session,
            self.obj,
            self.command_table,
            self.arg_table,
            self.name,
            'ec2.run-instances',
        )
        help_cmd.doc.target = 'html'
        doc_handler = CLIDocumentEventHandler(help_cmd)
        doc_handler.doc_breadcrumbs(help_cmd)
        self.assertEqual(
            help_cmd.doc.getvalue().decode('utf-8'),
            '[ :ref:`aws <cli:aws>` . :ref:`ec2 <cli:aws ec2>` ]\n\n'
        )

    def test_breadcrumbs_wait_command_html(self):
        help_cmd = ServiceHelpCommand(
            self.session,
            self.obj,
            self.command_table,
            self.arg_table,
            self.name,
            's3api.wait.object-exists',
        )
        help_cmd.doc.target = 'html'
        doc_handler = CLIDocumentEventHandler(help_cmd)
        doc_handler.doc_breadcrumbs(help_cmd)
        self.assertEqual(
            help_cmd.doc.getvalue().decode('utf-8'),
            (
                '[ :ref:`aws <cli:aws>` . :ref:`s3api <cli:aws s3api>`'
                ' . :ref:`wait <cli:aws s3api wait>` ]\n\n'
            )
        )

    def test_documents_json_header_shape(self):
        shape = {
            'type': 'string',
            'jsonvalue': True,
            'location': 'header',
            'locationName': 'X-Amz-Header-Name',
        }
        shape = StringShape('JSONValueArg', shape)
        rendered = self.get_help_docs_for_argument(shape)
        self.assertIn('(JSON)', rendered)

    def test_documents_enum_values(self):
        shape = {'type': 'string', 'enum': ['FOO', 'BAZ']}
        shape = StringShape('EnumArg', shape)
        rendered = self.get_help_docs_for_argument(shape)
        self.assertIn('Possible values', rendered)
        self.assertIn('FOO', rendered)
        self.assertIn('BAZ', rendered)

    def test_documents_recursive_input(self):
        shape_map = {
            'RecursiveStruct': {
                'type': 'structure',
                'members': {
                    'A': {'shape': 'NonRecursive'},
                    'B': {'shape': 'RecursiveStruct'},
                },
            },
            'NonRecursive': {'type': 'string'},
        }
        shape = StructureShape(
            'RecursiveStruct',
            shape_map['RecursiveStruct'],
            ShapeResolver(shape_map),
        )
        rendered = self.get_help_docs_for_argument(shape)
        self.assertIn('( ... recursive ... )', rendered)

    def test_documents_nested_structure(self):
        shape_map = {
            'UpperStructure': {
                'type': 'structure',
                'members': {
                    'A': {'shape': 'NestedStruct'},
                    'B': {'shape': 'NestedStruct'},
                },
            },
            'NestedStruct': {
                'type': 'structure',
                'members': {
                    'Nested_A': {'shape': 'Line'},
                    'Nested_B': {'shape': 'Line'},
                },
            },
            'Line': {'type': 'string'},
        }
        shape = StructureShape(
            'UpperStructure',
            shape_map['UpperStructure'],
            ShapeResolver(shape_map),
        )
        rendered = self.get_help_docs_for_argument(shape)
        self.assertEqual(rendered.count('A -> (structure)'), 1)
        self.assertEqual(rendered.count('B -> (structure)'), 1)
        self.assertEqual(rendered.count('Nested_A -> (string)'), 2)
        self.assertEqual(rendered.count('Nested_B -> (string)'), 2)

    def test_documents_nested_list(self):
        shape_map = {
            'UpperList': {
                'type': 'list',
                'member': {'shape': 'NestedStruct'},
            },
            'NestedStruct': {
                'type': 'structure',
                'members': {
                    'Nested_A': {'shape': 'Line'},
                    'Nested_B': {'shape': 'Line'},
                },
            },
            'Line': {'type': 'string'},
        }
        shape = ListShape(
            'UpperList', shape_map['UpperList'], ShapeResolver(shape_map)
        )
        rendered = self.get_help_docs_for_argument(shape)
        self.assertEqual(rendered.count('(structure)'), 1)
        self.assertEqual(rendered.count('Nested_A -> (string)'), 1)
        self.assertEqual(rendered.count('Nested_B -> (string)'), 1)

    def test_documents_nested_map(self):
        shape_map = {
            'UpperMap': {
                'type': 'map',
                'key': {'shape': 'NestedStruct'},
                'value': {'shape': 'NestedStruct'},
            },
            'NestedStruct': {
                'type': 'structure',
                'members': {
                    'Nested_A': {'shape': 'Line'},
                    'Nested_B': {'shape': 'Line'},
                },
            },
            'Line': {'type': 'string'},
        }
        shape = MapShape(
            'UpperMap', shape_map['UpperMap'], ShapeResolver(shape_map)
        )
        rendered = self.get_help_docs_for_argument(shape)
        self.assertEqual(rendered.count('key -> (structure)'), 1)
        self.assertEqual(rendered.count('value -> (structure)'), 1)
        self.assertEqual(rendered.count('Nested_A -> (string)'), 2)
        self.assertEqual(rendered.count('Nested_B -> (string)'), 2)

    def test_description_only_for_crosslink_manpage(self):
        help_command = self.create_help_command()
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_description(help_command=help_command)
        rendered = help_command.doc.getvalue().decode('utf-8')
        # The links are generated in the "man" mode.
        self.assertIn('See also: AWS API Documentation', rendered)

    def test_includes_webapi_crosslink_in_html(self):
        help_command = self.create_help_command()
        # Configure this for 'html' generation:
        help_command.obj.service_model.metadata = {'uid': 'service-1-2-3'}
        help_command.obj.name = 'myoperation'
        help_command.doc.target = 'html'

        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_description(help_command=help_command)
        rendered = help_command.doc.getvalue().decode('utf-8')
        # Should expect an externa link because we're generating html.
        self.assertIn(
            'See also: `AWS API Documentation '
            '<https://docs.aws.amazon.com/goto/'
            'WebAPI/service-1-2-3/myoperation>`_',
            rendered,
        )

    def test_includes_streaming_blob_options(self):
        help_command = self.create_help_command()
        blob_shape = Shape('blob_shape', {'type': 'blob'})
        blob_shape.serialization = {'streaming': True}
        blob_arg = CustomArgument('blob_arg', argument_model=blob_shape)
        help_command.arg_table = {'blob_arg': blob_arg}
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_option(
            arg_name='blob_arg', help_command=help_command
        )
        rendered = help_command.doc.getvalue().decode('utf-8')
        self.assertIn('streaming blob', rendered)

    def test_streaming_blob_comes_after_docstring(self):
        help_command = self.create_help_command()
        blob_shape = Shape('blob_shape', {'type': 'blob'})
        blob_shape.serialization = {'streaming': True}
        blob_arg = CustomArgument(
            name='blob_arg', argument_model=blob_shape, help_text='FooBar'
        )
        help_command.arg_table = {'blob_arg': blob_arg}
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_option(
            arg_name='blob_arg', help_command=help_command
        )
        rendered = help_command.doc.getvalue().decode('utf-8')
        self.assertRegex(rendered, r'FooBar[\s\S]*streaming blob')

    def test_includes_tagged_union_options(self):
        help_command = self.create_help_command()
        tagged_union = self.create_tagged_union_shape()
        arg = CustomArgument(name='tagged_union', argument_model=tagged_union)
        help_command.arg_table = {'tagged_union': arg}
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_option(
            arg_name='tagged_union', help_command=help_command
        )
        rendered = help_command.doc.getvalue().decode('utf-8')
        self.assertIn('(tagged union structure)', rendered)

    def test_tagged_union_comes_after_docstring_options(self):
        help_command = self.create_help_command()
        tagged_union = self.create_tagged_union_shape()
        arg = CustomArgument(
            name='tagged_union',
            argument_model=tagged_union,
            help_text='FooBar',
        )
        help_command.arg_table = {'tagged_union': arg}
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_option(
            arg_name='tagged_union', help_command=help_command
        )
        rendered = help_command.doc.getvalue().decode('utf-8')
        self.assertRegex(rendered, r'FooBar[\s\S]*Tagged Union')

    def test_tagged_union_comes_after_docstring_output(self):
        help_command = self.create_help_command()
        tagged_union = self.create_tagged_union_shape()
        tagged_union.documentation = "FooBar"
        shape = (
            DenormalizedStructureBuilder()
            .with_members(
                {
                    'foo': {
                        'type': 'structure',
                        'union': True,
                        'documentation': 'FooBar',
                        'members': {},
                    }
                }
            )
            .build_model()
        )
        help_command.obj.output_shape = shape
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_output(
            help_command=help_command, event_name='foobar'
        )
        rendered = help_command.doc.getvalue().decode('utf-8')
        self.assertRegex(rendered, r'FooBar[\s\S]*Tagged Union')

    def test_documents_required_parameters(self):
        """Tests that required parameters are correctly documented."""
        shape_map = {
            'ParentStructure': {
                'type': 'structure',
                'required': ['RequiredParameter'],
                'members': {
                    'RequiredParameter': {'shape': 'StringMember'},
                    'OptionalParameter': {'shape': 'StringMember'},
                },
            },
            'StringMember': {'type': 'string'},
        }

        resolver = ShapeResolver(shape_map)
        parent_shape = StructureShape(
            'ParentStructure', shape_map['ParentStructure'], resolver
        )

        rendered = self.get_help_docs_for_argument(parent_shape)

        required_index = rendered.find('RequiredParameter -> (string)')
        optional_index = rendered.find('OptionalParameter -> (string)')

        self.assertIn('[required]', rendered[required_index:optional_index])
        optional_text = rendered[optional_index:]
        self.assertNotIn('[required]', optional_text)

    def test_documents_constraints(self):
        shape = {'type': 'string', 'min': 0, 'max': 10, 'pattern': '.*'}
        shape = StringShape('ConstrainedArg', shape)
        arg = CustomArgument('ConstrainedArg', argument_model=shape)
        help_command = self.create_help_command()
        help_command.arg_table = {'ConstrainedArg': arg}
        operation_handler = OperationDocumentEventHandler(help_command)
        operation_handler.doc_option(
            arg_name='ConstrainedArg', help_command=help_command
        )
        rendered = help_command.doc.getvalue().decode('utf-8')
        self.assertIn('Constraints', rendered)
        self.assertIn('min: ``0``', rendered)
        self.assertIn('max: ``10``', rendered)
        self.assertIn('pattern: ``.*``', rendered)

    def test_meta_description_operation_command_html(self):
        help_cmd = ServiceHelpCommand(
            self.session, self.obj, self.command_table, self.arg_table,
            self.name, 'ec2.run-instances'
        )
        help_cmd.doc.target = 'html'
        doc_handler = OperationDocumentEventHandler(help_cmd)
        doc_handler.doc_meta_description(help_cmd)

        meta_description = help_cmd.doc.getvalue().decode('utf-8')
        self.assertIn(".. meta::\n   :description: ", meta_description)
        self.assertIn('to run the ec2 run-instances command', meta_description)

    def test_meta_description_service_html(self):
        help_cmd = ServiceHelpCommand(
            self.session, self.obj, self.command_table, self.arg_table,
            self.name, 'ec2'
        )
        help_cmd.doc.target = 'html'
        doc_handler = ServiceDocumentEventHandler(help_cmd)
        doc_handler.doc_meta_description(help_cmd)

        meta_description = help_cmd.doc.getvalue().decode('utf-8')
        self.assertIn(".. meta::\n   :description: Learn about the AWS CLI ", meta_description)
        self.assertIn(' ec2 commands', meta_description)


class TestTopicDocumentEventHandlerBase(unittest.TestCase):
    def setUp(self):
        self.session = mock.Mock()
        self.file_creator = FileCreator()

        self.tags_dict = {}

        # Make a temporary json index to base information on
        self.json_index = self.file_creator.create_file('index.json', '')
        with open(self.json_index, 'w') as f:
            json.dump(self.tags_dict, f, indent=4, sort_keys=True)

        self.index_patch = mock.patch(
            'awscli.topictags.TopicTagDB.index_file', self.json_index
        )
        self.dir_patch = mock.patch(
            'awscli.topictags.TopicTagDB.topic_dir', self.file_creator.rootdir
        )
        self.index_patch.start()
        self.dir_patch.start()

    def tearDown(self):
        self.dir_patch.stop()
        self.index_patch.stop()
        self.file_creator.remove_all()


class TestTopicListerDocumentEventHandler(TestTopicDocumentEventHandlerBase):
    def setUp(self):
        super().setUp()
        self.descriptions = [
            'This describes the first topic',
            'This describes the second topic',
            'This describes the third topic',
        ]
        self.tags_dict = {
            'topic-name-1': {
                'title': ['The first topic title'],
                'description': [self.descriptions[0]],
                'category': ['General'],
            },
            'topic-name-2': {
                'title': ['The second topic title'],
                'description': [self.descriptions[1]],
                'category': ['S3'],
            },
            'topic-name-3': {
                'title': ['The third topic title'],
                'description': [self.descriptions[2]],
                'category': ['General'],
            },
        }

        with open(self.json_index, 'w') as f:
            json.dump(self.tags_dict, f, indent=4, sort_keys=True)

        self.cmd = TopicListerCommand(self.session)
        self.doc_handler = TopicListerDocumentEventHandler(self.cmd)

    def test_breadcrumbs(self):
        self.doc_handler.doc_breadcrumbs(self.cmd)
        self.assertEqual(self.cmd.doc.getvalue().decode('utf-8'), '')
        self.cmd.doc.target = 'html'
        self.doc_handler.doc_breadcrumbs(self.cmd)
        self.assertEqual(
            '[ :ref:`aws <cli:aws>` ]', self.cmd.doc.getvalue().decode('utf-8')
        )

    def test_title(self):
        self.doc_handler.doc_title(self.cmd)
        title_contents = self.cmd.doc.getvalue().decode('utf-8')
        self.assertIn(f'.. _cli:aws help {self.cmd.name}:', title_contents)
        self.assertIn('AWS CLI Topic Guide', title_contents)

    def test_description(self):
        self.doc_handler.doc_description(self.cmd)
        self.assertIn(
            'This is the AWS CLI Topic Guide',
            self.cmd.doc.getvalue().decode('utf-8'),
        )

    def test_subitems_start(self):
        ref_output = [
            '-------\nGeneral\n-------',
            (
                f'* topic-name-1: {self.descriptions[0]}\n'
                f'* topic-name-3: {self.descriptions[2]}\n'
            ),
            '--\nS3\n--',
            f'* topic-name-2: {self.descriptions[1]}\n',
        ]

        self.doc_handler.doc_subitems_start(self.cmd)
        contents = self.cmd.doc.getvalue().decode('utf-8')

        for line in ref_output:
            self.assertIn(line, contents)
        # Make sure the toctree is not in the man page
        self.assertNotIn('.. toctree::', contents)

    def test_subitems_start_html(self):
        self.cmd.doc.target = 'html'
        ref_output = [
            '-------\nGeneral\n-------',
            (
                f'* :ref:`topic-name-1 <cli:aws help topic-name-1>`: {self.descriptions[0]}\n'
                f'* :ref:`topic-name-3 <cli:aws help topic-name-3>`: {self.descriptions[2]}\n'
            ),
            '--\nS3\n--',
            (
                f'* :ref:`topic-name-2 <cli:aws help topic-name-2>`: {self.descriptions[1]}\n'
            ),
        ]

        self.doc_handler.doc_subitems_start(self.cmd)
        contents = self.cmd.doc.getvalue().decode('utf-8')

        for line in ref_output:
            self.assertIn(line, contents)
        # Make sure the hidden toctree is in the html
        self.assertIn('.. toctree::', contents)
        self.assertIn(':hidden:', contents)


class TestTopicDocumentEventHandler(TestTopicDocumentEventHandlerBase):
    def setUp(self):
        super().setUp()
        self.name = 'topic-name-1'
        self.title = 'The first topic title'
        self.description = 'This is about the first topic'
        self.category = 'General'
        self.related_command = 'foo'
        self.related_topic = 'topic-name-2'
        self.topic_body = 'Hello World!'

        self.tags_dict = {
            self.name: {
                'title': [self.title],
                'description': [self.description],
                'category': [self.category],
                'related topic': [self.related_topic],
                'related command': [self.related_command],
            }
        }
        with open(self.json_index, 'w') as f:
            json.dump(self.tags_dict, f, indent=4, sort_keys=True)

        self.cmd = TopicHelpCommand(self.session, self.name)
        self.doc_handler = TopicDocumentEventHandler(self.cmd)

    def test_breadcrumbs(self):
        self.doc_handler.doc_breadcrumbs(self.cmd)
        self.assertEqual(self.cmd.doc.getvalue().decode('utf-8'), '')
        self.cmd.doc.target = 'html'
        self.doc_handler.doc_breadcrumbs(self.cmd)
        self.assertEqual(
            '[ :ref:`aws <cli:aws>` . :ref:`topics <cli:aws help topics>` ]',
            self.cmd.doc.getvalue().decode('utf-8'),
        )

    def test_title(self):
        self.doc_handler.doc_title(self.cmd)
        title_contents = self.cmd.doc.getvalue().decode('utf-8')
        self.assertIn(f'.. _cli:aws help {self.name}:', title_contents)
        self.assertIn(self.title, title_contents)

    def test_description(self):
        lines = [
            ':title: ' + self.title,
            ':description: ' + self.description,
            ':category:' + self.category,
            ':related command: ' + self.related_command,
            ':related topic: ' + self.related_topic,
            self.topic_body,
        ]
        body = '\n'.join(lines)
        self.file_creator.create_file(self.name + '.rst', body)
        self.doc_handler.doc_description(self.cmd)
        contents = self.cmd.doc.getvalue().decode('utf-8')
        self.assertIn(self.topic_body, contents)
        self.assertNotIn(':title ' + self.title, contents)

    def test_description_no_tags(self):
        lines = [self.topic_body]
        body = '\n'.join(lines)
        self.file_creator.create_file(self.name + '.rst', body)
        self.doc_handler.doc_description(self.cmd)
        contents = self.cmd.doc.getvalue().decode('utf-8')
        self.assertIn(self.topic_body, contents)

    def test_description_tags_in_body(self):
        lines = [
            ':title: ' + self.title,
            ':description: ' + self.description,
            ':related command: ' + self.related_command,
        ]
        body_lines = [
            ':related_topic: ' + self.related_topic,
            self.topic_body,
            ':foo: bar',
        ]
        body = '\n'.join(lines + body_lines)
        ref_body = '\n'.join(body_lines)
        self.file_creator.create_file(self.name + '.rst', body)
        self.doc_handler.doc_description(self.cmd)
        contents = self.cmd.doc.getvalue().decode('utf-8')
        self.assertIn(ref_body, contents)

    def test_excludes_global_options(self):
        self.doc_handler.doc_global_option(self.cmd)
        global_options = self.cmd.doc.getvalue().decode('utf-8')
        self.assertNotIn('Global Options', global_options)


class TestGlobalOptionsDocumenter(unittest.TestCase):
    def create_help_command(self):
        types = ['blob', 'integer', 'boolean', 'string']
        arg_table = {}
        for t in types:
            name = f'{t}_type'
            help_text = f'This arg type is {t}'
            choices = ['A', 'B', 'C'] if t == 'string' else []
            arg_table[name] = CustomArgument(
                name=name,
                cli_type_name=t,
                help_text=help_text,
                choices=choices,
            )
        help_command = mock.Mock(spec=HelpCommand)
        help_command.arg_table = arg_table
        help_command.doc = ReSTDocument()
        return help_command

    def create_documenter(self):
        return GlobalOptionsDocumenter(self.create_help_command())

    def test_doc_global_options(self):
        documenter = self.create_documenter()
        options = documenter.doc_global_options()
        self.assertIn('``--string_type`` (string)', options)
        self.assertIn('``--integer_type`` (integer)', options)
        self.assertIn('``--boolean_type`` (boolean)', options)
        self.assertIn('``--blob_type`` (blob)', options)
        self.assertIn('*   A', options)
        self.assertIn('*   B', options)
        self.assertIn('*   C', options)

    def test_doc_global_synopsis(self):
        documenter = self.create_documenter()
        synopsis = documenter.doc_global_synopsis()
        self.assertIn('[--string_type <value>]', synopsis)
        self.assertIn('[--integer_type <value>]', synopsis)
        self.assertIn('[--boolean_type]', synopsis)
        self.assertIn('[--blob_type <value>]', synopsis)
