import unittest

from parameterized import parameterized
from yaml import Loader, load

from docfx_yaml import extension


class TestGenerate(unittest.TestCase):
    def test_finds_unique_name(self):
        entries = {}

        # Disambiguate with unique entries.
        schema_v1_entry = "google.cloud.aiplatform.v1.schema.predict.instance_v1.types"
        schema_v1beta2_entry = (
            "google.cloud.aiplatform.v1beta2.schema.predict.instance_v1.types"
        )
        for entry in [schema_v1_entry, schema_v1beta2_entry]:
            for word in entry.split("."):
                if word not in entries:
                    entries[word] = 1
                else:
                    entries[word] += 1

        unique_v1_name = extension.find_unique_name(schema_v1_entry.split("."), entries)
        unique_v1beta2_name = extension.find_unique_name(
            schema_v1beta2_entry.split("."), entries
        )

        self.assertEqual("v1.types", ".".join(unique_v1_name))
        self.assertEqual("v1beta2.types", ".".join(unique_v1beta2_name))

    test_entries = [
        [
            "tests/yaml_pre.yaml",
            "tests/yaml_post.yaml",
            {
                "google.cloud.spanner_admin_database_v1.types": "spanner_admin_database_v1.types",
                "google.cloud.spanner_admin_instance_v1.types": "spanner_admin_instance_v1.types",
                "google.cloud.spanner_v1.types": "spanner_v1.types",
            },
        ],
        [
            # Tests duplicate names
            "tests/yaml_pre_duplicate.yaml",
            "tests/yaml_post_duplicate.yaml",
            {
                "google.api_core.client_info": "client_info",
                "google.api_core.gapic_v1.client_info": "gapic_v1.client_info",
            },
        ],
        [
            # Tests repeated duplicates in a row. No changes expected.
            "tests/yaml_repeat_duplicate.yaml",
            "tests/yaml_repeat_duplicate.yaml",
            {},
        ],
    ]

    @parameterized.expand(test_entries)
    def test_disambiguates_toc_name(
        self,
        test_filename,
        expected_filename,
        expected_disambiguated_names,
    ):
        with open(test_filename, "r") as test_file:
            test_yaml = load(test_file, Loader=Loader)
        with open(expected_filename, "r") as expected_yaml_file:
            expected_yaml = load(expected_yaml_file, Loader=Loader)

        disambiguated_names = extension.disambiguate_toc_name(test_yaml)

        self.assertEqual(expected_yaml, test_yaml)
        self.assertEqual(expected_disambiguated_names, disambiguated_names)

    test_entries = [
        [
            """
If a ``stream`` is attached to this download, then the downloaded
resource will be written to the stream.

Args:
    transport (~google.cloud.requests.Session): A ``requests`` object which can
        make authenticated requests.

    timeout (Optional[Union[float, Tuple[float, float]]]):
        The number of seconds to wait for the server response.
        Depending on the retry strategy, a request may be repeated
        several times using the same timeout each time.

        Can also be passed as a tuple (connect_timeout, read_timeout).
        See :meth:`google.cloud.requests.Session.request` documentation for details.

Returns:
    ~google.cloud.requests.Response: The HTTP response returned by ``transport``.

Raises:
    ~google.cloud.resumable_media.common.DataCorruption: If the download"s
        checksum doesn't agree with server-computed checksum.
    ValueError: If the current :class:`Download` has already
        finished.
            """,
            """
If a ``stream`` is attached to this download, then the downloaded
resource will be written to the stream.

Args:
    transport (<xref uid="google.cloud.requests.Session">Session</xref>): A ``requests`` object which can
        make authenticated requests.

    timeout (Optional[Union[float, Tuple[float, float]]]):
        The number of seconds to wait for the server response.
        Depending on the retry strategy, a request may be repeated
        several times using the same timeout each time.

        Can also be passed as a tuple (connect_timeout, read_timeout).
        See <xref uid="google.cloud.requests.Session.request">request</xref> documentation for details.

Returns:
    <xref uid="google.cloud.requests.Response">Response</xref>: The HTTP response returned by ``transport``.

Raises:
    <xref uid="google.cloud.resumable_media.common.DataCorruption">DataCorruption</xref>: If the download"s
        checksum doesn't agree with server-computed checksum.
    ValueError: If the current `Download` has already
        finished.
            """,
            [
                "google.cloud.requests.Session",
                "google.cloud.requests.Session.request",
                "google.cloud.requests.Response",
                "google.cloud.resumable_media.common.DataCorruption",
            ],
        ],
        [
            """
If a ~dateutil.time.stream() is attached to this download, then the downloaded
resource will be written to the stream.

Args:
    transport (~google.cloud.requests.Session()): A ``requests`` object which can
        make authenticated requests.

    timeout (Optional[Union[float, Tuple[float, float]]]):
        The number of seconds to wait for the server response.
        Depending on the retry strategy, a request may be repeated
        several times using the same timeout each time.

        Can also be passed as a :func:`~google.cloud.requests.tuple()` (connect_timeout, read_timeout).
        See :meth:`google.cloud.requests.Session.request()` documentation for details.
            """,
            """
If a `dateutil.time.stream()` is attached to this download, then the downloaded
resource will be written to the stream.

Args:
    transport (<xref uid="google.cloud.requests.Session">Session()</xref>): A ``requests`` object which can
        make authenticated requests.

    timeout (Optional[Union[float, Tuple[float, float]]]):
        The number of seconds to wait for the server response.
        Depending on the retry strategy, a request may be repeated
        several times using the same timeout each time.

        Can also be passed as a <xref uid="google.cloud.requests.tuple">tuple()</xref> (connect_timeout, read_timeout).
        See <xref uid="google.cloud.requests.Session.request">request()</xref> documentation for details.
            """,
            [
                "google.cloud.requests.Session",
                "google.cloud.requests.tuple",
                "google.cloud.requests.Session.request",
            ],
        ],
    ]

    @parameterized.expand(test_entries)
    def test_resolves_references_in_summary(
        self,
        test_docstring,
        expected_content,
        expected_xrefs,
    ):
        xrefs_to_check = []
        # Resolve over different regular expressions for different types of reference patterns.
        content_to_resolve, xrefs = extension._resolve_reference_in_module_summary(
            extension.REF_PATTERN,
            test_docstring.split("\n"),
        )
        xrefs_to_check.extend(xrefs)

        resolved_content, xrefs = extension._resolve_reference_in_module_summary(
            extension.REF_PATTERN_LAST,
            content_to_resolve,
        )
        xrefs_to_check.extend(xrefs)

        self.assertEqual(resolved_content, expected_content.split("\n"))
        self.assertCountEqual(xrefs_to_check, expected_xrefs)

    test_entries = [
        [
            """Required.

The [name][google.cloud.kms.v1.KeyRing.name] of the [KeyRing][google.cloud.kms.v1.KeyRing] associated with the [ImportJobs][google.cloud.kms.v1.ImportJob].
            """,
            """Required.

The <xref uid="google.cloud.kms.v1.KeyRing.name">name</xref> of the <xref uid="google.cloud.kms.v1.KeyRing">KeyRing</xref> associated with the <xref uid="google.cloud.kms.v1.ImportJob">ImportJobs</xref>.
            """,
            [
                "google.cloud.kms.v1.KeyRing.name",
                "google.cloud.kms.v1.KeyRing",
                "google.cloud.kms.v1.ImportJob",
            ],
        ],
    ]

    @parameterized.expand(test_entries)
    def test_resolves_square_bracket_references(
        self,
        summary,
        expected_summary,
        expected_xrefs,
    ):
        resolved_summary, xrefs = extension._resolve_reference_in_module_summary(
            extension.REF_PATTERN_BRACKETS,
            summary.split("\n"),
        )

        self.assertEqual(resolved_summary, expected_summary.split("\n"))
        self.assertCountEqual(xrefs, expected_xrefs)

    def test_raises_error_for_invalid_references(self):
        with self.assertRaises(ValueError):
            extension._resolve_reference_in_module_summary(
                ".*",
                "not a valid ref line".split("\n"),
            )

    test_entries = [
        [
            """
Simple test for docstring.

Args:
    arg1(int): simple description.
    arg2(str): simple description for `arg2`.

Returns:
    str: simple description for return value.

Raises:
    AttributeError: if `condition x`.
            """,
            "\nSimple test for docstring.\n\n",
            {
                "variables": {
                    "arg1": {
                        "var_type": "int",
                        "description": "simple description.",
                    },
                    "arg2": {
                        "var_type": "str",
                        "description": "simple description for `arg2`.",
                    },
                },
                "returns": [
                    {
                        "var_type": "str",
                        "description": "simple description for return value.",
                    }
                ],
                "exceptions": [
                    {
                        "var_type": "AttributeError",
                        "description": "if `condition x`.",
                    }
                ],
            },
        ],
        [
            # Tests summary in mixed format
            """
Simple test for docstring.

:type arg1: int
:param arg1: simple description.
:param arg2: simple description for `arg2`.
:type arg2: str

:rtype: str
:returns: simple description for return value.

:raises AttributeError: if `condition x`.
            """,
            "\nSimple test for docstring.\n\n",
            {
                "variables": {
                    "arg1": {
                        "var_type": "int",
                        "description": "simple description.",
                    },
                    "arg2": {
                        "var_type": "str",
                        "description": "simple description for `arg2`.",
                    },
                },
                "returns": [
                    {
                        "var_type": "str",
                        "description": "simple description for return value.",
                    }
                ],
                "exceptions": [
                    {
                        "var_type": "AttributeError",
                        "description": "if `condition x`.",
                    }
                ],
            },
        ],
        [
            # Tests summary for docstring tokens and not custom fields
            """
Union[int, None]: Expiration time in milliseconds for a partition.

If :attr:`partition_expiration` is set and <xref:type_> is
not set, :attr:`type_` will default to
:attr:`~google.cloud.bigquery.table.TimePartitioningType.DAY`.
It could return :param: with :returns as well.
            """,
            """
Union[int, None]: Expiration time in milliseconds for a partition.

If :attr:`partition_expiration` is set and <xref:type_> is
not set, :attr:`type_` will default to
:attr:`~google.cloud.bigquery.table.TimePartitioningType.DAY`.
It could return :param: with :returns as well.
            """,
            {
                "variables": {},
                "returns": [],
                "exceptions": [],
            },
        ],
        [
            # Tests summary with xrefs
            """
Simple test for docstring.

:type arg1: <xref uid="google.spanner_v1.type.Type">Type</xref>
:param arg1: simple description.
:param arg2: simple description for `arg2`.
:type arg2: ~google.spanner_v1.type.dict

:rtype: <xref uid="Pair">Pair</xref>
:returns: simple description for return value.

:raises <xref uid="SpannerException">SpannerException</xref>: if `condition x`.
            """,
            "\nSimple test for docstring.\n\n",
            {
                "variables": {
                    "arg1": {
                        "var_type": '<xref uid="google.spanner_v1.type.Type">Type</xref>',
                        "description": "simple description.",
                    },
                    "arg2": {
                        "var_type": "~google.spanner_v1.type.dict",
                        "description": "simple description for `arg2`.",
                    },
                },
                "returns": [
                    {
                        "var_type": '<xref uid="Pair">Pair</xref>',
                        "description": "simple description for return value.",
                    }
                ],
                "exceptions": [
                    {
                        "var_type": '<xref uid="SpannerException">SpannerException</xref>',
                        "description": "if `condition x`.",
                    }
                ],
            },
        ],
        [
            # Tests docstring without a summary
            """
Args:
    arg1(int): simple description.
    arg2(str): simple description for `arg2`.

Returns:
    str: simple description for return value.

Raises:
    AttributeError: if `condition x`.
            """,
            "\n",
            {
                "variables": {
                    "arg1": {
                        "var_type": "int",
                        "description": "simple description.",
                    },
                    "arg2": {
                        "var_type": "str",
                        "description": "simple description for `arg2`.",
                    },
                },
                "returns": [
                    {
                        "var_type": "str",
                        "description": "simple description for return value.",
                    }
                ],
                "exceptions": [
                    {
                        "var_type": "AttributeError",
                        "description": "if `condition x`.",
                    }
                ],
            },
        ],
    ]

    @parameterized.expand(test_entries)
    def test_extracts_docstring_info(
        self, summary, expected_top_summary, expected_summary_info
    ):
        summary_info = {"variables": {}, "returns": [], "exceptions": []}

        top_summary = extension._extract_docstring_info(
            summary_info,
            summary,
            "",
        )

        self.assertEqual(top_summary, expected_top_summary)
        self.assertDictEqual(summary_info, expected_summary_info)

    test_entries = [
        [
            """
Description of docstring which should fail.

:returns:param:
            """,
            ValueError,
            "Error string",
        ],
        [
            """
Description of malformed docstring.

Raises:
    Error that should fail: if condition `x`.
            """,
            KeyError,
            "Malformed docstring",
        ],
    ]

    @parameterized.expand(test_entries)
    def test_raises_error_extracting_malformed_docstring(
        self,
        summary,
        error_type,
        error_string,
    ):
        with self.assertRaises(error_type):
            extension._extract_docstring_info({}, summary, error_string)

    test_entries = [
        [
            "google.cloud.spanner_v1beta2.services.admin_database_v1.types",
            "google.cloud.spanner_v1beta2",
        ],
    ]

    @parameterized.expand(test_entries)
    def test_finds_package_group(
        self,
        uid,
        expected_package_group,
    ):
        package_group = extension.find_package_group(uid)

        self.assertEqual(package_group, expected_package_group)

    test_entries = [
        [
            "google.cloud.spanner_v1beta2",
            "Spanner V1beta2",
        ],
    ]

    @parameterized.expand(test_entries)
    def test_finds_pretty_package_name(
        self,
        package_group,
        expected_package_name,
    ):
        package_name = extension.pretty_package_name(package_group)

        self.assertEqual(package_name, expected_package_name)

    test_entries = [
        [
            # toc_yaml entry
            [
                {
                    "name": "database_admin",
                    "uidname": "google.cloud.spanner_admin_database_v1.services.database_admin",
                    "items": [
                        {
                            "name": "Overview",
                            "uidname": "google.cloud.spanner_admin_database_v1.services.database_admin",
                            "uid": "google.cloud.spanner_admin_database_v1.services.database_admin",
                        },
                        {
                            "name": "ListBackupOperationsAsyncPager",
                            "uidname": "google.cloud.spanner_admin_database_v1.services.database_admin.pagers.ListBackupOperationsAsyncPager",
                            "uid": "google.cloud.spanner_admin_database_v1.services.database_admin.pagers.ListBackupOperationsAsyncPager",
                        },
                    ],
                },
                {
                    "name": "spanner_admin_database_v1.types",
                    "uidname": "google.cloud.spanner_admin_database_v1.types",
                    "items": [
                        {
                            "name": "Overview",
                            "uidname": "google.cloud.spanner_admin_database_v1.types",
                            "uid": "google.cloud.spanner_admin_database_v1.types",
                        },
                        {
                            "name": "BackupInfo",
                            "uidname": "google.cloud.spanner_admin_database_v1.types.BackupInfo",
                            "uid": "google.cloud.spanner_admin_database_v1.types.BackupInfo",
                        },
                    ],
                },
                {
                    "name": "pool",
                    "uidname": "google.cloud.spanner_v1.pool",
                    "items": [
                        {
                            "name": "Overview",
                            "uidname": "google.cloud.spanner_v1.pool",
                            "uid": "google.cloud.spanner_v1.pool",
                        },
                        {
                            "name": "AbstractSessionPool",
                            "uidname": "google.cloud.spanner_v1.pool.AbstractSessionPool",
                            "uid": "google.cloud.spanner_v1.pool.AbstractSessionPool",
                        },
                    ],
                },
            ],
            # expected_toc_yaml entry
            [
                {
                    "name": "Spanner Admin Database V1",
                    "uidname": "google.cloud.spanner_admin_database_v1",
                    "items": [
                        {
                            "name": "database_admin",
                            "uidname": "google.cloud.spanner_admin_database_v1.services.database_admin",
                            "items": [
                                {
                                    "name": "Overview",
                                    "uidname": "google.cloud.spanner_admin_database_v1.services.database_admin",
                                    "uid": "google.cloud.spanner_admin_database_v1.services.database_admin",
                                },
                                {
                                    "name": "ListBackupOperationsAsyncPager",
                                    "uidname": "google.cloud.spanner_admin_database_v1.services.database_admin.pagers.ListBackupOperationsAsyncPager",
                                    "uid": "google.cloud.spanner_admin_database_v1.services.database_admin.pagers.ListBackupOperationsAsyncPager",
                                },
                            ],
                        },
                        {
                            "name": "spanner_admin_database_v1.types",
                            "uidname": "google.cloud.spanner_admin_database_v1.types",
                            "items": [
                                {
                                    "name": "Overview",
                                    "uidname": "google.cloud.spanner_admin_database_v1.types",
                                    "uid": "google.cloud.spanner_admin_database_v1.types",
                                },
                                {
                                    "name": "BackupInfo",
                                    "uidname": "google.cloud.spanner_admin_database_v1.types.BackupInfo",
                                    "uid": "google.cloud.spanner_admin_database_v1.types.BackupInfo",
                                },
                            ],
                        },
                    ],
                },
                {
                    "name": "Spanner V1",
                    "uidname": "google.cloud.spanner_v1",
                    "items": [
                        {
                            "name": "pool",
                            "uidname": "google.cloud.spanner_v1.pool",
                            "items": [
                                {
                                    "name": "Overview",
                                    "uidname": "google.cloud.spanner_v1.pool",
                                    "uid": "google.cloud.spanner_v1.pool",
                                },
                                {
                                    "name": "AbstractSessionPool",
                                    "uidname": "google.cloud.spanner_v1.pool.AbstractSessionPool",
                                    "uid": "google.cloud.spanner_v1.pool.AbstractSessionPool",
                                },
                            ],
                        }
                    ],
                },
            ],
        ],
    ]

    @parameterized.expand(test_entries)
    def test_groups_by_package(
        self,
        toc_yaml,
        expected_toc_yaml,
    ):
        toc_yaml = extension.group_by_package(toc_yaml)

        self.assertCountEqual(toc_yaml, expected_toc_yaml)

    test_entries = [
        [
            (
                """


.. code-block:: python

\n    from google.api_core.client_options import ClientOptions
\n    from google.cloud.vision_v1 import ImageAnnotatorClient
\n    def get_client_cert():
\n        # code to load client certificate and private key.
\n        return client_cert_bytes, client_private_key_bytes
\n    options = ClientOptions(api_endpoint=\"foo.googleapis.com\",
\n        client_cert_source=get_client_cert)
\n    client = ImageAnnotatorClient(client_options=options)


You can also pass a mapping object.


\n.. code-block:: ruby

\n    from google.cloud.vision_v1 import ImageAnnotatorClient
\n    client = ImageAnnotatorClient(
\n        client_options={
\n            \"api_endpoint\": \"foo.googleapis.com\",
\n            \"client_cert_source\" : get_client_cert
\n        })
"""
            ),
            (
                """```python
from google.api_core.client_options import ClientOptions

from google.cloud.vision_v1 import ImageAnnotatorClient

def get_client_cert():

    # code to load client certificate and private key.

    return client_cert_bytes, client_private_key_bytes

options = ClientOptions(api_endpoint=\"foo.googleapis.com\",

    client_cert_source=get_client_cert)

client = ImageAnnotatorClient(client_options=options)
```

You can also pass a mapping object.

```ruby
from google.cloud.vision_v1 import ImageAnnotatorClient

client = ImageAnnotatorClient(

    client_options={

        \"api_endpoint\": \"foo.googleapis.com\",

        \"client_cert_source\" : get_client_cert

    })

```
"""
            ),
        ],
        [
            # Check that nothing changes for literalincludes.
            (
                """
.. literalinclude::
    note that these are not supported yet, so they will be ignored for now.

And any other documentation that the source code would have could go here.
"""
            ),
            (
                """
.. literalinclude::
    note that these are not supported yet, so they will be ignored for now.

And any other documentation that the source code would have could go here.

"""
            ),
        ],
        [
            # Tests notices are processed properly.
            (
                """
.. note::
\n    this is a note.


.. caution::
\n    another type of notice.


.. key-term::
\n    hyphenated term notice.
"""
            ),
            (
                """<aside class="note">
<b>Note:</b>
this is a note.
</aside>
<aside class="caution">
<b>Caution:</b>
another type of notice.
</aside>
<aside class="key-term">
<b>Key Term:</b>
hyphenated term notice.
</aside>"""
            ),
        ],
    ]

    @parameterized.expand(test_entries)
    def test_parses_docstring_summary(
        self,
        summary,
        expected_summary,
    ):
        parsed_summary, attributes, enums = extension._parse_docstring_summary(summary)
        self.assertEqual(parsed_summary, expected_summary)
        self.assertEqual(attributes, [])
        self.assertEqual(enums, [])

    test_entries = [
        [
            (
                """


.. code:: python

\nprint("This should throw an exception.")
\nfor i in range(10):
\n    print(i)
"""
            ),
            ValueError,
        ],
        [
            (
                """
.. warning::
this is not a properly formatted warning.
"""
            ),
            ValueError,
        ],
        [
            (
                """
Values:
BAD_FORMATTING (-1): this is not properly formatted enum.
"""
            ),
            ValueError,
        ],
    ]

    @parameterized.expand(test_entries)
    def test_raises_error_parsing_malformed_docstring(self, summary, error_type):
        with self.assertRaises(error_type):
            extension._parse_docstring_summary(summary)

    test_entries = [
        [
            (
                """


.. attribute:: simple name

\nsimple description

\n:type: str
"""
            ),
            [
                {
                    "id": "simple name",
                    "description": "simple description",
                    "var_type": "str",
                }
            ],
            [],
        ],
        [
            # Tests for multiple attributes.
            (
                """


.. attribute:: simple name

\nsimple description

\n:type: str


.. attribute:: table_insert_request

\nTable insert request.

\n:type: google.cloud.bigquery_logging_v1.types.TableInsertRequest
"""
            ),
            [
                {
                    "id": "simple name",
                    "description": "simple description",
                    "var_type": "str",
                },
                {
                    "id": "table_insert_request",
                    "description": "Table insert request.",
                    "var_type": "google.cloud.bigquery_logging_v1.types.TableInsertRequest",
                },
            ],
            [],
        ],
        [
            # Tests only attributes in valid format are parsed.
            (
                """


.. attribute:: table_insert_request

\nTable insert request.

\ntype: google.cloud.bigquery_logging_v1.types.TableInsertRequest


.. attribute:: proper name

\nproper description.

\n:type: str
"""
            ),
            [
                {
                    "id": "proper name",
                    "description": "proper description.",
                    "var_type": "str",
                }
            ],
            [],
        ],
        [
            # Tests enums are parsed.
            (
                """
Values:
    EMPLOYMENT_TYPE_UNSPECIFIED (0):
        The default value if the employment type isn't specified.
    FULL_TIME (1):
        The job requires working a number of hours that constitute full time
        employment, typically 40 or more hours per week.
    PART_TIME (2):
        The job entails working fewer hours than a full time job,
        typically less than 40 hours a week.
"""
            ),
            [],
            [
                {
                    "id": "EMPLOYMENT_TYPE_UNSPECIFIED",
                    "description": (
                        "The default value if the employment type isn't specified."
                    ),
                },
                {
                    "id": "FULL_TIME",
                    "description": (
                        "The job requires working a number of hours that"
                        " constitute full time employment, typically 40 or"
                        " more hours per week."
                    ),
                },
                {
                    "id": "PART_TIME",
                    "description": (
                        "The job entails working fewer hours than a full"
                        " time job, typically less than 40 hours a week."
                    ),
                },
            ],
        ],
    ]

    @parameterized.expand(test_entries)
    def test_parses_docstring_summary_for_attributes_and_enums(
        self,
        summary,
        expected_attributes,
        expected_enums,
    ):
        _, attributes, enums = extension._parse_docstring_summary(summary)

        self.assertCountEqual(attributes, expected_attributes)
        self.assertCountEqual(enums, expected_enums)

        for attribute, expected_attribute in zip(attributes, expected_attributes):
            self.assertDictEqual(attribute, expected_attribute)
        for enum, expected_enum in zip(enums, expected_enums):
            self.assertDictEqual(enum, expected_enum)

    def test_merges_markdown_and_package_toc(self):
        known_uids = {
            "acl",
            "batch",
            "blob",
            "client",
            "constants",
            "fileio",
            "hmac_key",
            "notification",
            "retry",
        }
        markdown_pages = {
            "storage": [
                {"name": "FileIO", "href": "fileio.md"},
                {"name": "Retry", "href": "retry.md"},
                {"name": "Notification", "href": "notification.md"},
                {"name": "HMAC Key Metadata", "href": "hmac_key.md"},
                {"name": "Batches", "href": "batch.md"},
                {"name": "Constants", "href": "constants.md"},
                {"name": "Storage Client", "href": "client.md"},
                {"name": "Blobs / Objects", "href": "blobs.md"},
            ],
            "acl": [
                {"name": "ACL", "href": "acl.md"},
                {"name": "ACL guide", "href": "acl_guide.md"},
            ],
            "/": [
                {"name": "Overview", "href": "index.md"},
                {"name": "Changelog", "href": "changelog.md"},
            ],
        }
        pkg_toc_yaml = [
            {
                "name": "Storage",
                "items": [
                    {
                        "name": "acl",
                        "uid": "google.cloud.storage.acl",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.acl",
                            },
                        ],
                    },
                    {
                        "name": "batch",
                        "uid": "google.cloud.storage.batch",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.batch",
                            },
                        ],
                    },
                    {
                        "name": "blob",
                        "uid": "google.cloud.storage.blob",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.blob",
                            },
                        ],
                    },
                    {
                        "name": "bucket",
                        "uid": "google.cloud.storage.bucket",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.bucket",
                            },
                        ],
                    },
                    {
                        "name": "client",
                        "uid": "google.cloud.storage.client",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.client",
                            },
                        ],
                    },
                    {
                        "name": "constants",
                        "uid": "google.cloud.storage.constants",
                    },
                    {
                        "name": "fileio",
                        "uid": "google.cloud.storage.fileio",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.fileio",
                            },
                        ],
                    },
                    {
                        "name": "hmac_key",
                        "uid": "google.cloud.storage.hmac_key",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.hmac_key",
                            },
                        ],
                    },
                    {
                        "name": "notification",
                        "uid": "google.cloud.storage.notification",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.notification",
                            },
                        ],
                    },
                    {
                        "name": "retry",
                        "uid": "google.cloud.storage.retry",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.retry",
                            },
                        ],
                    },
                ],
            },
        ]

        added_pages, merged_pkg_toc_yaml = extension.merge_markdown_and_package_toc(
            pkg_toc_yaml, markdown_pages, known_uids
        )

        expected_merged_pkg_toc_yaml = [
            {"name": "Overview", "href": "index.md"},
            {"name": "Changelog", "href": "changelog.md"},
            {
                "name": "Storage",
                "items": [
                    {"name": "Blobs / Objects", "href": "blobs.md"},
                    {
                        "name": "acl",
                        "uid": "google.cloud.storage.acl",
                        "items": [
                            {"name": "ACL guide", "href": "acl_guide.md"},
                            {"name": "Overview", "uid": "google.cloud.storage.acl"},
                        ],
                    },
                    {
                        "name": "batch",
                        "uid": "google.cloud.storage.batch",
                        "items": [
                            {"name": "Overview", "uid": "google.cloud.storage.batch"}
                        ],
                    },
                    {
                        "name": "blob",
                        "uid": "google.cloud.storage.blob",
                        "items": [
                            {"name": "Overview", "uid": "google.cloud.storage.blob"}
                        ],
                    },
                    {
                        "name": "bucket",
                        "uid": "google.cloud.storage.bucket",
                        "items": [
                            {"name": "Overview", "uid": "google.cloud.storage.bucket"}
                        ],
                    },
                    {
                        "name": "client",
                        "uid": "google.cloud.storage.client",
                        "items": [
                            {"name": "Overview", "uid": "google.cloud.storage.client"}
                        ],
                    },
                    {"name": "constants", "uid": "google.cloud.storage.constants"},
                    {
                        "name": "fileio",
                        "uid": "google.cloud.storage.fileio",
                        "items": [
                            {"name": "Overview", "uid": "google.cloud.storage.fileio"}
                        ],
                    },
                    {
                        "name": "hmac_key",
                        "uid": "google.cloud.storage.hmac_key",
                        "items": [
                            {"name": "Overview", "uid": "google.cloud.storage.hmac_key"}
                        ],
                    },
                    {
                        "name": "notification",
                        "uid": "google.cloud.storage.notification",
                        "items": [
                            {
                                "name": "Overview",
                                "uid": "google.cloud.storage.notification",
                            }
                        ],
                    },
                    {
                        "name": "retry",
                        "uid": "google.cloud.storage.retry",
                        "items": [
                            {"name": "Overview", "uid": "google.cloud.storage.retry"}
                        ],
                    },
                ],
            },
        ]
        self.assertSetEqual(
            added_pages,
            {
                "index.md",
                "changelog.md",
                "blobs.md",
                "acl_guide.md",
            },
        )
        (
            self.assertListEqual(
                merged_pkg_toc_yaml,
                [
                    {"name": "Overview", "href": "index.md"},
                    {"name": "Changelog", "href": "changelog.md"},
                    {
                        "name": "Storage",
                        "items": [
                            {"name": "Blobs / Objects", "href": "blobs.md"},
                            {
                                "name": "acl",
                                "uid": "google.cloud.storage.acl",
                                "items": [
                                    {"name": "ACL guide", "href": "acl_guide.md"},
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.acl",
                                    },
                                ],
                            },
                            {
                                "name": "batch",
                                "uid": "google.cloud.storage.batch",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.batch",
                                    }
                                ],
                            },
                            {
                                "name": "blob",
                                "uid": "google.cloud.storage.blob",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.blob",
                                    }
                                ],
                            },
                            {
                                "name": "bucket",
                                "uid": "google.cloud.storage.bucket",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.bucket",
                                    }
                                ],
                            },
                            {
                                "name": "client",
                                "uid": "google.cloud.storage.client",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.client",
                                    }
                                ],
                            },
                            {
                                "name": "constants",
                                "uid": "google.cloud.storage.constants",
                            },
                            {
                                "name": "fileio",
                                "uid": "google.cloud.storage.fileio",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.fileio",
                                    }
                                ],
                            },
                            {
                                "name": "hmac_key",
                                "uid": "google.cloud.storage.hmac_key",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.hmac_key",
                                    }
                                ],
                            },
                            {
                                "name": "notification",
                                "uid": "google.cloud.storage.notification",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.notification",
                                    }
                                ],
                            },
                            {
                                "name": "retry",
                                "uid": "google.cloud.storage.retry",
                                "items": [
                                    {
                                        "name": "Overview",
                                        "uid": "google.cloud.storage.retry",
                                    }
                                ],
                            },
                        ],
                    },
                ],
            ),
        )


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