#!/usr/bin/env python3
#
# Copyright (C) 2018-present MongoDB, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the Server Side Public License, version 1,
# as published by MongoDB, Inc.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# Server Side Public License for more details.
#
# You should have received a copy of the Server Side Public License
# along with this program. If not, see
# <http://www.mongodb.com/licensing/server-side-public-license>.
#
# As a special exception, the copyright holders give permission to link the
# code of portions of this program with the OpenSSL library under certain
# conditions as described in each individual source file and distribute
# linked combinations including the program with the OpenSSL library. You
# must comply with the Server Side Public License in all respects for
# all of the code used other than as permitted herein. If you modify file(s)
# with this exception, you may extend this exception to your version of the
# file(s), but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version. If you delete this
# exception statement from all source files in the program, then also delete
# it in the license file.
#
# pylint: disable=too-many-lines
"""Test cases for IDL binder."""

import textwrap
import unittest

# import package so that it works regardless of whether we run as a module or file
if __package__ is None:
    import sys
    from os import path
    sys.path.append(path.dirname(path.abspath(__file__)))
    from context import idl
    import testcase
else:
    from .context import idl
    from . import testcase

# All YAML tests assume 4 space indent
INDENT_SPACE_COUNT = 4


def fill_spaces(count):
    # type: (int) -> str
    """Fill a string full of spaces."""
    fill = ''
    for _ in range(count * INDENT_SPACE_COUNT):
        fill += ' '

    return fill


def indent_text(count, unindented_text):
    # type: (int, str) -> str
    """Indent each line of a multi-line string."""
    lines = unindented_text.splitlines()
    fill = fill_spaces(count)
    return '\n'.join(fill + line for line in lines)


class TestBinder(testcase.IDLTestcase):
    """Test cases for the IDL binder."""

    # pylint: disable=too-many-public-methods

    def test_empty(self):
        # type: () -> None
        """Test an empty document works."""
        self.assert_bind("")

    def test_global_positive(self):
        # type: () -> None
        """Postive global tests."""
        spec = self.assert_bind(
            textwrap.dedent("""
        global:
            cpp_namespace: 'something'
            cpp_includes:
                - 'bar'
                - 'foo'"""))
        self.assertEqual(spec.globals.cpp_namespace, "something")
        self.assertListEqual(spec.globals.cpp_includes, ['bar', 'foo'])

    def test_type_positive(self):
        # type: () -> None
        """Positive type tests."""
        self.assert_bind(
            textwrap.dedent("""
        types:
            foo:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo
            """))

        # Test supported types
        for bson_type in [
                "bool", "date", "null", "decimal", "double", "int", "long", "objectid", "regex",
                "string", "timestamp", "undefined"
        ]:
            self.assert_bind(
                textwrap.dedent("""
        types:
            foofoo:
                description: foo
                cpp_type: foo
                bson_serialization_type: %s
                default: foo
                deserializer: BSONElement::fake
            """ % (bson_type)))

        # Test supported numeric types
        for cpp_type in [
                "std::int32_t",
                "std::uint32_t",
                "std::int32_t",
                "std::uint64_t",
                "std::vector<std::uint8_t>",
                "std::array<std::uint8_t, 16>",
        ]:
            self.assert_bind(
                textwrap.dedent("""
                types:
                    foofoo:
                        description: foo
                        cpp_type: %s
                        bson_serialization_type: int
                        deserializer: BSONElement::fake
                """ % (cpp_type)))

        # Test object
        self.assert_bind(
            textwrap.dedent("""
        types:
            foofoo:
                description: foo
                cpp_type: foo
                bson_serialization_type: object
                serializer: foo
                deserializer: foo
                default: foo
            """))

        # Test 'any'
        self.assert_bind(
            textwrap.dedent("""
        types:
            foofoo:
                description: foo
                cpp_type: foo
                bson_serialization_type: any
                serializer: foo
                deserializer: foo
                default: foo
            """))

        # Test 'chain'
        self.assert_bind(
            textwrap.dedent("""
        types:
            foofoo:
                description: foo
                cpp_type: foo
                bson_serialization_type: chain
                serializer: foo
                deserializer: foo
                default: foo
            """))

        # Test supported bindata_subtype
        for bindata_subtype in ["generic", "function", "uuid", "md5"]:
            self.assert_bind(
                textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: bindata
                    bindata_subtype: %s
                    deserializer: BSONElement::fake
                """ % (bindata_subtype)))

    def test_type_negative(self):
        # type: () -> None
        """Negative type tests for properties that types and fields share."""

        # Test bad bson type name
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: foo
            """), idl.errors.ERROR_ID_BAD_BSON_TYPE)

        # Test bad cpp_type name
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: StringData
                    bson_serialization_type: string
                    deserializer: bar
            """), idl.errors.ERROR_ID_NO_STRINGDATA)

        # Test unsupported serialization
        for cpp_type in [
                "char",
                "signed char",
                "unsigned char",
                "signed short int",
                "short int",
                "short",
                "signed short",
                "unsigned short",
                "unsigned short int",
                "signed int",
                "signed",
                "unsigned int",
                "unsigned",
                "signed long int",
                "signed long",
                "int",
                "long int",
                "long",
                "unsigned long int",
                "unsigned long",
                "signed long long int",
                "signed long long",
                "long long int",
                "long long",
                "unsigned long int",
                "unsigned long",
                "wchar_t",
                "char16_t",
                "char32_t",
                "int8_t",
                "int16_t",
                "int32_t",
                "int64_t",
                "uint8_t",
                "uint16_t",
                "uint32_t",
                "uint64_t",
        ]:
            self.assert_bind_fail(
                textwrap.dedent("""
                types:
                    foofoo:
                        description: foo
                        cpp_type: %s
                        bson_serialization_type: int
                        deserializer: BSONElement::int
                    """ % (cpp_type)), idl.errors.ERROR_ID_BAD_NUMERIC_CPP_TYPE)

        # Test the std prefix 8 and 16-byte integers fail
        for std_cpp_type in ["std::int8_t", "std::int16_t", "std::uint8_t", "std::uint16_t"]:
            self.assert_bind_fail(
                textwrap.dedent("""
                types:
                    foofoo:
                        description: foo
                        cpp_type: %s
                        bson_serialization_type: int
                        deserializer: BSONElement::int
                    """ % (std_cpp_type)), idl.errors.ERROR_ID_BAD_NUMERIC_CPP_TYPE)

        # Test bindata_subtype missing
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: bindata
                    deserializer: BSONElement::fake
            """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_VALUE)

        # Test fake bindata_subtype is wrong
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: bindata
                    bindata_subtype: foo
                    deserializer: BSONElement::fake
            """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_VALUE)

        # Test deprecated bindata_subtype 'binary', and 'uuid_old' are wrong
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: bindata
                    bindata_subtype: binary
            """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_VALUE)

        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: bindata
                    bindata_subtype: uuid_old
            """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_VALUE)

        # Test bindata_subtype on wrong type
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: string
                    bindata_subtype: generic
                    deserializer: BSONElement::fake
            """), idl.errors.ERROR_ID_BAD_BSON_BINDATA_SUBTYPE_TYPE)

        # Test bindata with default
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: bindata
                    bindata_subtype: uuid
                    default: 42
            """), idl.errors.ERROR_ID_BAD_BINDATA_DEFAULT)

        # Test bindata in list of types
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type:
                                - bindata
                                - string
            """), idl.errors.ERROR_ID_BAD_BSON_TYPE)

        # Test bindata in list of types
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: StringData
                    bson_serialization_type:
                                - bindata
                                - string
            """), idl.errors.ERROR_ID_BAD_BSON_TYPE)

        # Test 'any' in list of types
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type:
                                - any
                                - int
            """), idl.errors.ERROR_ID_BAD_ANY_TYPE_USE)

        # Test object in list of types
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type:
                                - object
                                - int
            """), idl.errors.ERROR_ID_BAD_BSON_TYPE_LIST)

        # Test fake in list of types
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type:
                                - int
                                - fake
            """), idl.errors.ERROR_ID_BAD_BSON_TYPE)

        # Test 'chain' in list of types
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type:
                                - chain
                                - int
            """), idl.errors.ERROR_ID_BAD_ANY_TYPE_USE)

        # Test unsupported serialization
        for bson_type in [
                "bool", "date", "null", "decimal", "double", "int", "long", "objectid", "regex",
                "timestamp", "undefined"
        ]:
            self.assert_bind_fail(
                textwrap.dedent("""
                types:
                    foofoo:
                        description: foo
                        cpp_type: std::string
                        bson_serialization_type: %s
                        serializer: foo
                        deserializer: BSONElement::fake
                    """ % (bson_type)),
                idl.errors.ERROR_ID_CUSTOM_SCALAR_SERIALIZATION_NOT_SUPPORTED)

            self.assert_bind_fail(
                textwrap.dedent("""
                types:
                    foofoo:
                        description: foo
                        cpp_type: std::string
                        bson_serialization_type: %s
                        deserializer: foo
                    """ % (bson_type)),
                idl.errors.ERROR_ID_CUSTOM_SCALAR_SERIALIZATION_NOT_SUPPORTED)

        # Test 'any' serialization needs deserializer
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: any
            """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)

        # Test 'chain' serialization needs deserializer
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: chain
                    serializer: bar
            """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)

        # Test 'string' serialization needs deserializer
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: string
                    serializer: bar
            """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)

        # Test 'date' serialization needs deserializer
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: date
            """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)

        # Test 'chain' serialization needs serializer
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: chain
                    deserializer: bar
            """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)

        # Test list of bson types needs deserializer
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                foofoo:
                    description: foo
                    cpp_type: std::int32_t
                    bson_serialization_type:
                                - int
                                - string
            """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)

        # Test array as name
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                array<foo>:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: string
                    deserializer: bar
            """), idl.errors.ERROR_ID_ARRAY_NOT_VALID_TYPE)

    def test_struct_positive(self):
        # type: () -> None
        """Positive struct tests."""

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo
            int:
                description: foo
                cpp_type: std::int32_t
                bson_serialization_type: int
                deserializer: mongo::BSONElement::_numberInt
        """)

        self.assert_bind(test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo: string
            """))

        self.assert_bind(test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo: array<int>
            """))

    def test_struct_negative(self):
        # type: () -> None
        """Negative struct tests."""

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo
            int:
                description: foo
                cpp_type: std::int32_t
                bson_serialization_type: int
                deserializer: mongo::BSONElement::_numberInt
        """)

        # Test array as name
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                array<foo>:
                    description: foo
                    strict: true
                    fields:
                        foo: string
            """), idl.errors.ERROR_ID_ARRAY_NOT_VALID_TYPE)

        self.assert_bind(test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo: array<int>
            """))

    def test_variant_negative(self):
        # type: () -> None
        """Negative variant test cases."""

        # Setup some common types
        test_preamble = textwrap.dedent("""
        enums:
            foo_enum:
                description: foo
                type: int
                values:
                    v1: 0
                    v2: 1
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo
            int:
                description: foo
                cpp_type: std::int32_t
                bson_serialization_type: int
                deserializer: mongo::BSONElement::_numberInt
            safeInt:
                bson_serialization_type:
                - long
                - int
                - decimal
                - double
                description: foo
                cpp_type: "std::int32_t"
                deserializer: "mongo::BSONElement::safeNumberInt"
            bindata_function:
                bson_serialization_type: bindata
                bindata_subtype: function
                description: "A BSON bindata of function sub type"
                cpp_type: "std::vector<std::uint8_t>"
                deserializer: "mongo::BSONElement::_binDataVector"
        """)

        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - string
            """), idl.errors.ERROR_ID_USELESS_VARIANT)

        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - string
                            - int
                            - not_defined
            """), idl.errors.ERROR_ID_UNKNOWN_TYPE)

        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - string
                            - int
                        default: 1
            """), idl.errors.ERROR_ID_VARIANT_NO_DEFAULT)

        # Bindata is banned in variants for now.
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - string
                            - bindata_function
            """), idl.errors.ERROR_ID_BAD_BSON_TYPE)

        # Enums are banned in variants for now.
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - string
                            - foo_enum
            """), idl.errors.ERROR_ID_NO_VARIANT_ENUM)

        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - string
                            - string
            """), idl.errors.ERROR_ID_VARIANT_DUPLICATE_TYPES)

        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - array<string>
                            - array<string>
            """), idl.errors.ERROR_ID_VARIANT_DUPLICATE_TYPES)

        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            struct0:
                description: foo
            struct1:
                description: foo
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - array<struct0>
                            - array<struct1>
            """), idl.errors.ERROR_ID_VARIANT_DUPLICATE_TYPES)

        # At most one array can have BSON serialization type NumberInt.
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - array<int>
                            - array<safeInt>
            """), idl.errors.ERROR_ID_VARIANT_DUPLICATE_TYPES)

        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            one_string:
                description: foo
                fields: {value: string}
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - one_string
                            - one_string
                            - int
            """), idl.errors.ERROR_ID_VARIANT_STRUCTS)

        # At most one type can have BSON serialization type Object.
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            one_string:
                description: foo
                fields: {value: string}
            one_int:
                description: foo
                fields: {value: int}
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - one_string
                            - one_int
                            - int
            """), idl.errors.ERROR_ID_VARIANT_STRUCTS)

        # At most one type can have BSON serialization type NumberInt.
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                fields:
                    my_variant_field:
                        type:
                            variant:
                            - safeInt
                            - int
      """), idl.errors.ERROR_ID_VARIANT_DUPLICATE_TYPES)

    def test_field_positive(self):
        # type: () -> None
        """Positive test cases for field."""

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
        """)

        # Short type
        self.assert_bind(test_preamble + textwrap.dedent("""
        structs:
            bar:
                description: foo
                strict: false
                fields:
                    foo: string
            """))

        # Long type
        self.assert_bind(test_preamble + textwrap.dedent("""
        structs:
            bar:
                description: foo
                strict: false
                fields:
                    foo:
                        type: string
            """))

        # Long type with default
        self.assert_bind(test_preamble + textwrap.dedent("""
        structs:
            bar:
                description: foo
                strict: false
                fields:
                    foo:
                        type: string
                        default: bar
            """))

        # Test array as field type
        self.assert_bind(test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo: array<string>
            """))

        # Test array as field type
        self.assert_bind(
            textwrap.dedent("""
            types:
                 arrayfake:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: string
                    serializer: foo
                    deserializer: foo

            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        arrayOfString: arrayfake
            """))

        # Test always_serialize with optional
        self.assert_bind(test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo:
                            type: string
                            optional: true
                            always_serialize: true
            """))

    def test_field_negative(self):
        # type: () -> None
        """Negative field tests."""

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo

            bindata:
                description: foo
                cpp_type: foo
                bson_serialization_type: bindata
                bindata_subtype: uuid
        """)

        # Test field of a struct type with a default
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    fields:
                        field1: string

                bar:
                    description: foo
                    fields:
                        field2:
                            type: foo
                            default: foo

            """), idl.errors.ERROR_ID_FIELD_MUST_BE_EMPTY_FOR_STRUCT)

        # Test array as field name
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        array<foo>: string
            """), idl.errors.ERROR_ID_ARRAY_NOT_VALID_TYPE)

        # Test recursive array as field type
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo: array<array<string>>
            """), idl.errors.ERROR_ID_BAD_ARRAY_TYPE_NAME)

        # Test inherited default with array
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo: array<string>
            """), idl.errors.ERROR_ID_ARRAY_NO_DEFAULT)

        # Test non-inherited default with array
        self.assert_bind_fail(
            textwrap.dedent("""
            types:
                string:
                    description: foo
                    cpp_type: foo
                    bson_serialization_type: string
                    serializer: foo
                    deserializer: foo

            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo:
                            type: array<string>
                            default: 123
            """), idl.errors.ERROR_ID_ARRAY_NO_DEFAULT)

        # Test bindata with default
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo:
                            type: bindata
                            default: 42
            """), idl.errors.ERROR_ID_BAD_BINDATA_DEFAULT)

        # Test default and optional for the same field
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo:
                            type: string
                            default: 42
                            optional: true
            """), idl.errors.ERROR_ID_ILLEGAL_FIELD_DEFAULT_AND_OPTIONAL)

        # Test always_serialize without optional for the same field
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: true
                    fields:
                        foo:
                            type: string
                            default: 42
                            always_serialize: true
            """), idl.errors.ERROR_ID_ILLEGAL_FIELD_ALWAYS_SERIALIZE_NOT_OPTIONAL)

        # Test duplicate comparison order
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo:
                description: foo
                strict: false
                generate_comparison_operators: true
                fields:
                    foo:
                        type: string
                        comparison_order: 1
                    bar:
                        type: string
                        comparison_order: 1
            """), idl.errors.ERROR_ID_IS_DUPLICATE_COMPARISON_ORDER)

        # Test field marked with non_const_getter in immutable struct
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    immutable: true
                    fields:
                        foo:
                            type: string
                            non_const_getter: true
            """), idl.errors.ERROR_ID_NON_CONST_GETTER_IN_IMMUTABLE_STRUCT)

    def test_ignored_field_negative(self):
        # type: () -> None
        """Test that if a field is marked as ignored, no other properties are set."""
        for test_value in [
                "optional: true",
                "default: foo",
        ]:
            self.assert_bind_fail(
                textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    strict: false
                    fields:
                        foo:
                            type: string
                            ignore: true
                            %s
                """ % (test_value)), idl.errors.ERROR_ID_FIELD_MUST_BE_EMPTY_FOR_IGNORED)

    def test_chained_type_positive(self):
        # type: () -> None
        """Positive parser chaining test cases."""
        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo


            foo1:
                description: foo
                cpp_type: foo
                bson_serialization_type: chain
                serializer: foo
                deserializer: foo
                default: foo

        """)

        # Chaining only
        self.assert_bind(test_preamble + textwrap.dedent("""
        structs:
            bar1:
                description: foo
                strict: false
                chained_types:
                    foo1: alias
        """))

    def test_chained_type_negative(self):
        # type: () -> None
        """Negative parser chaining test cases."""
        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo

            foo1:
                description: foo
                cpp_type: foo
                bson_serialization_type: chain
                serializer: foo
                deserializer: foo

        """)

        # Chaining with strict struct
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            bar1:
                description: foo
                strict: true
                chained_types:
                    foo1: alias
        """), idl.errors.ERROR_ID_CHAINED_NO_TYPE_STRICT)

        # Non-'any' type as chained type
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            bar1:
                description: foo
                strict: false
                chained_types:
                    string: alias
        """), idl.errors.ERROR_ID_CHAINED_TYPE_WRONG_BSON_TYPE)

        # Chaining and fields only with same name
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            bar1:
                description: foo
                strict: false
                chained_types:
                    foo1: alias
                fields:
                    foo1: string
        """), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)

        # Non-existent chained type
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            bar1:
                description: foo
                strict: false
                chained_types:
                    foobar1: alias
                fields:
                    foo1: string
        """), idl.errors.ERROR_ID_UNKNOWN_TYPE)

        # A regular field as a chained type
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            bar1:
                description: foo
                strict: false
                fields:
                    foo1: string
                    foo2: foobar1
        """), idl.errors.ERROR_ID_UNKNOWN_TYPE)

        # Array of chained types
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            bar1:
                description: foo
                strict: true
                fields:
                    field1: array<foo1>
        """), idl.errors.ERROR_ID_NO_ARRAY_OF_CHAIN)

    def test_chained_struct_positive(self):
        # type: () -> None
        """Positive parser chaining test cases."""
        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo


            foo1:
                description: foo
                cpp_type: foo
                bson_serialization_type: chain
                serializer: foo
                deserializer: foo
                default: foo

        structs:
            chained:
                description: foo
                strict: false
                chained_types:
                    foo1: alias

            chained2:
                description: foo
                strict: false
                fields:
                    field1: string
        """)

        # A struct with only chaining
        self.assert_bind(test_preamble + indent_text(
            1,
            textwrap.dedent("""
            bar1:
                description: foo
                strict: true
                chained_structs:
                    chained2: alias
        """)))

        # Chaining struct's fields and explicit fields
        self.assert_bind(test_preamble + indent_text(
            1,
            textwrap.dedent("""
            bar1:
                description: foo
                strict: true
                chained_structs:
                    chained2: alias
                fields:
                    str1: string
        """)))

        # Chained types and structs
        self.assert_bind(test_preamble + indent_text(
            1,
            textwrap.dedent("""
            bar1:
                description: foo
                strict: false
                chained_types:
                    foo1: alias
                chained_structs:
                    chained2: alias
                fields:
                    str1: string
        """)))

        # Non-strict chained struct
        self.assert_bind(test_preamble + indent_text(
            1,
            textwrap.dedent("""
            bar1:
                description: foo
                strict: false
                chained_structs:
                    chained2: alias
                fields:
                    foo1: string
        """)))

        # Inline Chained struct with strict true
        self.assert_bind(test_preamble + indent_text(
            1,
            textwrap.dedent("""
            bar1:
                description: foo
                strict: true
                fields:
                    field1: string

            foobar:
                description: foo
                strict: false
                inline_chained_structs: true
                chained_structs:
                    bar1: alias
                fields:
                    f1: string

        """)))

        # Inline Chained struct with strict true and inline_chained_structs defaulted
        self.assert_bind(test_preamble + indent_text(
            1,
            textwrap.dedent("""
            bar1:
                description: foo
                strict: true
                fields:
                    field1: string

            foobar:
                description: foo
                strict: false
                chained_structs:
                    bar1: alias
                fields:
                    f1: string
        """)))

    def test_chained_struct_negative(self):
        # type: () -> None
        """Negative parser chaining test cases."""
        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo


            foo1:
                description: foo
                cpp_type: foo
                bson_serialization_type: chain
                serializer: foo
                deserializer: foo
                default: foo

        structs:
            chained:
                description: foo
                strict: false
                fields:
                    field1: string

            chained2:
                description: foo
                strict: false
                fields:
                    field1: string
        """)

        # Non-existing chained struct
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: true
                chained_structs:
                    foobar1: alias
        """)), idl.errors.ERROR_ID_UNKNOWN_TYPE)

        # Type as chained struct
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: true
                chained_structs:
                    foo1: alias
        """)), idl.errors.ERROR_ID_CHAINED_STRUCT_NOT_FOUND)

        # Struct as chained type
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: false
                chained_types:
                    chained: alias
        """)), idl.errors.ERROR_ID_CHAINED_TYPE_NOT_FOUND)

        # Duplicated field names across chained struct's fields and fields
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: false
                chained_structs:
                    chained: alias
                fields:
                    field1: string
        """)), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)

        # Duplicated field names across chained structs
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: false
                chained_structs:
                    chained: alias
                    chained2: alias
        """)), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)

        # Chained struct with strict true
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: true
                fields:
                    field1: string

            foobar:
                description: foo
                strict: false
                inline_chained_structs: false
                chained_structs:
                    bar1: alias
                fields:
                    f1: string

        """)), idl.errors.ERROR_ID_CHAINED_NO_NESTED_STRUCT_STRICT)

        # Chained struct with nested chained struct
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: false
                chained_structs:
                    chained: alias

            foobar:
                description: foo
                strict: false
                chained_structs:
                    bar1: alias
                fields:
                    f1: string

        """)), idl.errors.ERROR_ID_CHAINED_NO_NESTED_CHAINED)

        # Chained struct with nested chained type
        self.assert_bind_fail(
            test_preamble + indent_text(
                1,
                textwrap.dedent("""
            bar1:
                description: foo
                strict: false
                chained_types:
                    foo1: alias

            foobar:
                description: foo
                strict: false
                chained_structs:
                    bar1: alias
                fields:
                    f1: bar1

        """)), idl.errors.ERROR_ID_CHAINED_NO_NESTED_CHAINED)

    def test_enum_positive(self):
        # type: () -> None
        """Positive enum test cases."""

        # Test int
        self.assert_bind(
            textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: int
                values:
                    v1: 3
                    v2: 1
                    v3: 2
            """))

        # Test string
        self.assert_bind(
            textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: string
                values:
                    v1: 0
                    v2: 1
                    v3: 2
            """))

    def test_enum_negative(self):
        # type: () -> None
        """Negative enum test cases."""

        # Test wrong type
        self.assert_bind_fail(
            textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: foo
                values:
                    v1: 0
            """), idl.errors.ERROR_ID_ENUM_BAD_TYPE)

        # Test int - non continuous
        self.assert_bind_fail(
            textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: int
                values:
                    v1: 0
                    v3: 2
            """), idl.errors.ERROR_ID_ENUM_NON_CONTINUOUS_RANGE)

        # Test int - dups
        self.assert_bind_fail(
            textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: int
                values:
                    v1: 1
                    v3: 1
            """), idl.errors.ERROR_ID_ENUM_NON_UNIQUE_VALUES)

        # Test int - non-integer value
        self.assert_bind_fail(
            textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: int
                values:
                    v1: foo
                    v3: 1
            """), idl.errors.ERROR_ID_ENUM_BAD_INT_VAUE)

        # Test string - dups
        self.assert_bind_fail(
            textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: string
                values:
                    v1: foo
                    v3: foo
            """), idl.errors.ERROR_ID_ENUM_NON_UNIQUE_VALUES)

    def test_struct_enum_negative(self):
        # type: () -> None
        """Negative enum test cases."""

        test_preamble = textwrap.dedent("""
        enums:
            foo:
                description: foo
                type: int
                values:
                    v1: 0
                    v2: 1
        """)

        # Test array of enums
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        structs:
            foo1:
                description: foo
                fields:
                    foo1: array<foo>
            """), idl.errors.ERROR_ID_NO_ARRAY_ENUM)

    def test_command_positive(self):
        # type: () -> None
        """Positive command tests."""

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo

        structs:
            reply:
                description: foo
                fields:
                    foo: string
        """)

        self.assert_bind(test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    strict: true
                    fields:
                        foo1: string
                    reply_type: reply
            """))

    def test_command_negative(self):
        # type: () -> None
        """Negative command tests."""

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
                default: foo
        """)

        # Commands cannot be fields in other commands
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo1: string

                bar:
                    description: foo
                    command_name: bar
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo: foo
            """), idl.errors.ERROR_ID_FIELD_NO_COMMAND)

        # Commands cannot be fields in structs
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo1: string

            structs:
                bar:
                    description: foo
                    fields:
                        foo: foo
            """), idl.errors.ERROR_ID_FIELD_NO_COMMAND)

        # Commands cannot have a field as the same name
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo: string
            """), idl.errors.ERROR_ID_COMMAND_DUPLICATES_FIELD)

        # Reply type must be resolvable
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    reply_type: not_defined
            """), idl.errors.ERROR_ID_UNKNOWN_TYPE)

        # Reply type must be a struct
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    reply_type: string
            """), idl.errors.ERROR_ID_INVALID_REPLY_TYPE)

    def test_command_doc_sequence_positive(self):
        # type: () -> None
        """Positive supports_doc_sequence tests."""
        # pylint: disable=invalid-name

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            object:
                description: foo
                cpp_type: foo
                bson_serialization_type: object
                serializer: foo
                deserializer: foo

            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo

        structs:
            foo_struct:
                description: foo
                strict: true
                fields:
                    foo: object
        """)

        self.assert_bind(test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo1:
                            type: array<object>
                            supports_doc_sequence: true
            """))

        self.assert_bind(test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo1:
                            type: array<foo_struct>
                            supports_doc_sequence: true
            """))

    def test_command_doc_sequence_negative(self):
        # type: () -> None
        """Negative supports_doc_sequence tests."""
        # pylint: disable=invalid-name

        # Setup some common types
        test_preamble = textwrap.dedent("""
        types:
            object:
                description: foo
                cpp_type: foo
                bson_serialization_type: object
                serializer: foo
                deserializer: foo

            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo

            any_type:
                description: foo
                cpp_type: foo
                bson_serialization_type: any
                serializer: foo
                deserializer: foo
        """)

        test_preamble2 = test_preamble + textwrap.dedent("""
        structs:
            foo_struct:
                description: foo
                strict: true
                fields:
                    foo: object
        """)

        # A struct
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            structs:
                foo:
                    description: foo
                    fields:
                        foo:
                            type: array<object>
                            supports_doc_sequence: true
            """), idl.errors.ERROR_ID_STRUCT_NO_DOC_SEQUENCE)

        # A non-array type
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo:
                            type: object
                            supports_doc_sequence: true
            """), idl.errors.ERROR_ID_NO_DOC_SEQUENCE_FOR_NON_ARRAY)

        # An array of a scalar
        self.assert_bind_fail(
            test_preamble2 + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo1:
                            type: array<string>
                            supports_doc_sequence: true
            """), idl.errors.ERROR_ID_NO_DOC_SEQUENCE_FOR_NON_OBJECT)

        # An array of 'any'
        self.assert_bind_fail(
            test_preamble2 + textwrap.dedent("""
            commands:
                foo:
                    description: foo
                    command_name: foo
                    namespace: ignored
                    api_version: ""
                    fields:
                        foo1:
                            type: array<string>
                            supports_doc_sequence: true
            """), idl.errors.ERROR_ID_NO_DOC_SEQUENCE_FOR_NON_OBJECT)

    def test_command_type_positive(self):
        # type: () -> None
        """Positive command custom type test cases."""
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
        """)

        # string
        self.assert_bind(test_preamble + textwrap.dedent("""
        commands:
            foo:
                description: foo
                command_name: foo
                strict: true
                namespace: type
                api_version: ""
                type: string
                fields:
                    field1: string
            """))

        # array of string
        self.assert_bind(test_preamble + textwrap.dedent("""
        commands:
            foo:
                description: foo
                command_name: foo
                strict: true
                namespace: type
                api_version: ""
                type: array<string>
                fields:
                    field1: string
            """))

    def test_command_type_negative(self):
        # type: () -> None
        """Negative command type test cases."""
        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo
        """)

        # supports_doc_sequence must be a bool
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            foo:
                description: foo
                command_name: foo
                namespace: type
                api_version: ""
                type: int
                fields:
                    field1: string
            """), idl.errors.ERROR_ID_UNKNOWN_TYPE)

    def test_server_parameter_positive(self):
        # type: () -> None
        """Positive server parameter test cases."""

        # server parameter with storage.
        # Also try valid set_at values.
        for set_at in ["startup", "runtime", "[ startup, runtime ]"]:
            self.assert_bind(
                textwrap.dedent("""
            server_parameters:
                foo:
                    set_at: %s
                    description: bar
                    cpp_varname: baz
                """ % (set_at)))

        # server parameter with storage and optional fields.
        self.assert_bind(
            textwrap.dedent("""
        server_parameters:
            foo:
                set_at: startup
                description: bar
                cpp_varname: baz
                default: 42
                on_update: buzz
                validator:
                    gt: 0
                    gte: 1
                    lte: 999
                    lt: 1000
                    callback: qux
            """))

        # Bound setting with arbitrary expression default and validators.
        self.assert_bind(
            textwrap.dedent("""
        server_parameters:
            foo:
                set_at: startup
                description: bar
                cpp_varname: baz
                default:
                    expr: 'kDefaultValue'
                validator:
                    gte:
                        expr: 'kMinimumValue'
                        is_constexpr: true
                    lte:
                        expr: 'kMaximumValue'
                        is_constexpr: false
                    gt: 0
                    lt: 255
            """))

        # Specialized SCPs.
        self.assert_bind(
            textwrap.dedent("""
        server_parameters:
            foo:
                set_at: startup
                description: bar
                cpp_class: baz
        """))

        self.assert_bind(
            textwrap.dedent("""
        server_parameters:
            foo:
                set_at: startup
                description: bar
                cpp_class:
                    name: baz
        """))

        self.assert_bind(
            textwrap.dedent("""
        server_parameters:
            foo:
                set_at: startup
                description: bar
                cpp_class:
                    name: baz
                    data: bling
                    override_set: true
                    override_ctor: false
        """))

        self.assert_bind(
            textwrap.dedent("""
        server_parameters:
            foo:
                set_at: startup
                description: bar
                cpp_class: baz
                condition: { expr: "true" }
                redact: true
                test_only: true
                deprecated_name: bling
        """))

        # Default without data.
        self.assert_bind(
            textwrap.dedent("""
        server_parameters:
            foo:
                set_at: startup
                description: bar
                cpp_class: baz
                default: blong
            """))

    def test_server_parameter_negative(self):
        # type: () -> None
        """Negative server parameter test cases."""

        # Invalid set_at values.
        self.assert_bind_fail(
            textwrap.dedent("""
            server_parameters:
                foo:
                    set_at: shutdown
                    description: bar
                    cpp_varname: baz
            """), idl.errors.ERROR_ID_BAD_SETAT_SPECIFIER)

        # Mix of specialized with bound storage.
        self.assert_bind_fail(
            textwrap.dedent("""
            server_parameters:
                foo:
                    set_at: startup
                    description: bar
                    cpp_class: baz
                    cpp_varname: bling
            """), idl.errors.ERROR_ID_SERVER_PARAMETER_INVALID_ATTR)

    def test_config_option_positive(self):
        # type: () -> None
        """Posative config option test cases."""

        # Every field.
        self.assert_bind(
            textwrap.dedent("""
            configs:
                foo:
                    short_name: bar
                    deprecated_name: baz
                    deprecated_short_name: qux
                    description: comment
                    section: here
                    arg_vartype: String
                    cpp_varname: gStringVal
                    conflicts: bling
                    requires: blong
                    hidden: false
                    default: one
                    implicit: two
                    duplicate_behavior: append
                    source: yaml
                    positional: 1-2
                    validator:
                        gt: 0
                        lt: 100
                        gte: 1
                        lte: 99
                        callback: doSomething
            """))

        # Required fields only.
        self.assert_bind(
            textwrap.dedent("""
            configs:
                foo:
                    description: comment
                    arg_vartype: Switch
                    source: yaml
            """))

        # List and enum variants.
        self.assert_bind(
            textwrap.dedent("""
            configs:
                foo:
                    deprecated_name: [ baz, baz ]
                    deprecated_short_name: [ bling, blong ]
                    description: comment
                    arg_vartype: StringVector
                    source: [ cli, ini, yaml ]
                    conflicts: [ a, b, c ]
                    requires: [ d, e, f ]
                    hidden: true
                    duplicate_behavior: overwrite
            """))

        # Positional variants.
        for positional in ['1', '1-', '-2', '1-2']:
            self.assert_bind(
                textwrap.dedent("""
                configs:
                    foo:
                        short_name: foo
                        description: comment
                        arg_vartype: Bool
                        source: cli
                        positional: %s
                """ % (positional)))
            # With implicit short name.
            self.assert_bind(
                textwrap.dedent("""
                configs:
                    foo:
                        description: comment
                        arg_vartype: Bool
                        source: cli
                        positional: %s
                """ % (positional)))

        # Expressions in default, implicit, and validators.
        self.assert_bind(
            textwrap.dedent("""
            configs:
                foo:
                    description: bar
                    arg_vartype: String
                    source: cli
                    default: { expr: kDefault, is_constexpr: true }
                    implicit: { expr: kImplicit, is_constexpr: false }
                    validator:
                        gte: { expr: kMinimum }
                        lte: { expr: kMaximum }
            """))

    def test_config_option_negative(self):
        # type: () -> None
        """Negative config option test cases."""

        # Invalid source.
        self.assert_bind_fail(
            textwrap.dedent("""
            configs:
                foo:
                    description: comment
                    arg_vartype: Long
                    source: json
            """), idl.errors.ERROR_ID_BAD_SOURCE_SPECIFIER)

        self.assert_bind_fail(
            textwrap.dedent("""
            configs:
                foo:
                    description: comment
                    arg_vartype: StringMap
                    source: [ cli, yaml ]
                    duplicate_behavior: guess
            """), idl.errors.ERROR_ID_BAD_DUPLICATE_BEHAVIOR_SPECIFIER)

        for positional in ["x", "1-2-3", "-2-", "1--3"]:
            self.assert_bind_fail(
                textwrap.dedent("""
                configs:
                    foo:
                        description: comment
                        arg_vartype: String
                        source: cli
                        positional: %s
                """ % (positional)), idl.errors.ERROR_ID_BAD_NUMERIC_RANGE)

        self.assert_bind_fail(
            textwrap.dedent("""
            configs:
                foo:
                    description: comment
                    short_name: "bar.baz"
                    arg_vartype: Bool
                    source: cli
            """), idl.errors.ERROR_ID_INVALID_SHORT_NAME)

        self.assert_bind_fail(
            textwrap.dedent("""
            configs:
                foo:
                    description: comment
                    short_name: bar
                    deprecated_short_name: "baz.qux"
                    arg_vartype: Long
                    source: cli
            """), idl.errors.ERROR_ID_INVALID_SHORT_NAME)

        # dottedName is not valid as a shortName.
        self.assert_bind_fail(
            textwrap.dedent("""
            configs:
                "foo.bar":
                    description: comment
                    arg_vartype: String
                    source: cli
                    positional: 1
            """), idl.errors.ERROR_ID_MISSING_SHORTNAME_FOR_POSITIONAL)

        # Invalid shortname using boost::po format directly.
        self.assert_bind_fail(
            textwrap.dedent("""
            configs:
                foo:
                    short_name: "foo,f"
                    arg_vartype: Switch
                    description: comment
                    source: cli
            """), idl.errors.ERROR_ID_INVALID_SHORT_NAME)

        # Invalid single names, must be single alpha char.
        for name in ["foo", "1", ".", ""]:
            self.assert_bind_fail(
                textwrap.dedent("""
                configs:
                    foo:
                        single_name: "%s"
                        arg_vartype: Switch
                        description: comment
                        source: cli
            """ % (name)), idl.errors.ERROR_ID_INVALID_SINGLE_NAME)

        # Single names require a valid short name.
        self.assert_bind_fail(
            textwrap.dedent("""
            configs:
                "foo.bar":
                    single_name: f
                    arg_vartype: Switch
                    description: comment
                    source: cli
            """), idl.errors.ERROR_ID_MISSING_SHORT_NAME_WITH_SINGLE_NAME)

    def test_feature_flag(self):
        # type: () -> None
        """Test feature flag checks around version."""

        # feature flag can default to false without a version
        self.assert_bind(
            textwrap.dedent("""
            feature_flags:
                featureFlagToaster:
                    description: "Make toast"
                    cpp_varname: gToaster
                    default: false
            """))

        # feature flag can default to true with a version
        self.assert_bind(
            textwrap.dedent("""
            feature_flags:
                featureFlagToaster:
                    description: "Make toast"
                    cpp_varname: gToaster
                    default: true
                    version: 123
            """))

        # true is only allowed with a version
        self.assert_bind_fail(
            textwrap.dedent("""
            feature_flags:
                featureFlagToaster:
                    description: "Make toast"
                    cpp_varname: gToaster
                    default: true
            """), idl.errors.ERROR_ID_FEATURE_FLAG_DEFAULT_TRUE_MISSING_VERSION)

        # false is not allowed with a version
        self.assert_bind_fail(
            textwrap.dedent("""
            feature_flags:
                featureFlagToaster:
                    description: "Make toast"
                    cpp_varname: gToaster
                    default: false
                    version: 123
            """), idl.errors.ERROR_ID_FEATURE_FLAG_DEFAULT_FALSE_HAS_VERSION)

    def test_access_check(self):
        # type: () -> None
        """Test access check."""

        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo

        enums:
            AccessCheck:
                description: "test"
                type: string
                values:
                    kIsAuthenticated :  "is_authenticated"
                    kIsCoAuthorized :  "is_coauthorized"

            ActionType:
                description: "test"
                type: string
                values:
                    addShard :  "addShard"
                    serverStatus :  "serverStatus"

            MatchType:
                description: "test"
                type: string
                values:
                    matchClusterResource :  "cluster"

        structs:
            reply:
                description: foo
                fields:
                    foo: string
        """)

        # Test none
        self.assert_bind(test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    none: true
                fields:
                    foo: string
                reply_type: reply
            """))

        # Test simple with access check
        self.assert_bind(test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    simple:
                        check: is_authenticated
                fields:
                    foo: string
                reply_type: reply
            """))

        # Test simple with privilege
        self.assert_bind(test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    simple:
                        privilege:
                            resource_pattern: cluster
                            action_type: addShard
                fields:
                    foo: string
                reply_type: reply
            """))

        self.assert_parse(
            textwrap.dedent("""
        commands:
            foo:
                description: foo
                command_name: foo
                api_version: 1
                namespace: ignored
                access_check:
                    complex:
                        - privilege:
                            resource_pattern: cluster
                            action_type: addShard
                        - privilege:
                            resource_pattern: cluster
                            action_type: serverStatus
                        - check: is_authenticated
                fields:
                    foo: bar
                reply_type: foo_reply_struct
            """))

    def test_access_check_negative(self):
        # type: () -> None
        """Negative access check tests."""

        test_preamble = textwrap.dedent("""
        types:
            string:
                description: foo
                cpp_type: foo
                bson_serialization_type: string
                serializer: foo
                deserializer: foo

        enums:
            AccessCheck:
                description: "test"
                type: string
                values:
                    kIsAuthenticated :  "is_authenticated"
                    kIsCoAuthorized :  "is_coauthorized"

            ActionType:
                description: "test"
                type: string
                values:
                    addShard :  "addShard"
                    serverStatus :  "serverStatus"

            MatchType:
                description: "test"
                type: string
                values:
                    matchClusterResource :  "cluster"
        structs:
            reply:
                description: foo
                fields:
                    foo: string
        """)

        # Test simple with bad access check
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    simple:
                        check: unknown
                fields:
                    foo: string
                reply_type: reply
            """), idl.errors.ERROR_ID_UNKOWN_ENUM_VALUE)

        # Test simple with bad access check with privilege
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    simple:
                        privilege:
                            resource_pattern: foo
                            action_type: addShard
                fields:
                    foo: string
                reply_type: reply
            """), idl.errors.ERROR_ID_UNKOWN_ENUM_VALUE)

        # Test simple with bad access check with privilege
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    simple:
                        privilege:
                            resource_pattern: cluster
                            action_type: foo
                fields:
                    foo: string
                reply_type: reply
            """), idl.errors.ERROR_ID_UNKOWN_ENUM_VALUE)

        # Test simple with access check and privileges
        self.assert_bind(test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    simple:
                        privilege:
                            resource_pattern: cluster
                            action_type: [addShard, serverStatus]
                fields:
                    foo: string
                reply_type: reply
            """))

        # Test simple with privilege with duplicate action_type
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: ""
                namespace: ignored
                access_check:
                    simple:
                        privilege:
                            resource_pattern: cluster
                            action_type: [addShard, addShard]
                fields:
                    foo: string
                reply_type: reply
            """), idl.errors.ERROR_ID_DUPLICATE_ACTION_TYPE)

        # complex with duplicate check
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: 1
                namespace: ignored
                access_check:
                    complex:
                        - check: is_authenticated
                        - check: is_authenticated
                fields:
                    foo: string
                reply_type: reply
            """), idl.errors.ERROR_ID_DUPLICATE_ACCESS_CHECK)

        # complex with duplicate priv
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: 1
                namespace: ignored
                access_check:
                    complex:
                        - privilege:
                            resource_pattern: cluster
                            action_type: addShard
                        - privilege:
                            resource_pattern: cluster
                            action_type: [addShard, serverStatus]
                fields:
                    foo: string
                reply_type: reply
            """), idl.errors.ERROR_ID_DUPLICATE_ACCESS_CHECK)

        # api_version != "" but not access_check
        self.assert_bind_fail(
            test_preamble + textwrap.dedent("""
        commands:
            test1:
                description: foo
                command_name: foo
                api_version: 1
                namespace: ignored
                fields:
                    foo: string
                reply_type: reply
            """), idl.errors.ERROR_ID_MISSING_ACCESS_CHECK)


if __name__ == '__main__':

    unittest.main()
