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

from __future__ import annotations

import platform
import warnings
from copy import copy
from datetime import timedelta

import pytest

from couchbase.auth import CertificateAuthenticator, PasswordAuthenticator
from couchbase.cluster import Cluster
from couchbase.exceptions import (CouchbaseException,
                                  InvalidArgumentException,
                                  UnAmbiguousTimeoutException)
from couchbase.logic.cluster_impl import ClusterImpl
from couchbase.options import (CONFIG_PROFILES,
                               ClusterMetricsOptions,
                               ClusterOptions,
                               ClusterOrphanReportingOptions,
                               ClusterTimeoutOptions,
                               ClusterTracingOptions,
                               ConfigProfile,
                               IpProtocol,
                               KnownConfigProfiles,
                               TLSVerifyMode)
from couchbase.serializer import DefaultJsonSerializer
from couchbase.transcoder import JSONTranscoder
from tests.environments import CollectionType


class ConnectionTestSuite:
    TEST_MANIFEST = [
        'test_cluster_auth_fail',
        'test_cluster_cert_auth',
        'test_cluster_cert_auth_fail',
        'test_cluster_cert_auth_ts_connstr',
        'test_cluster_cert_auth_ts_kwargs',
        'test_cluster_ldap_auth',
        'test_cluster_ldap_auth_real',
        'test_cluster_legacy_sasl_mech_force',
        'test_cluster_legacy_sasl_mech_force_real',
        'test_cluster_legacy_ssl_no_verify',
        'test_cluster_options',
        'test_cluster_pw_auth',
        'test_cluster_pw_auth_fail',
        'test_cluster_pw_auth_with_cert',
        'test_cluster_pw_auth_with_cert_connstr',
        'test_cluster_pw_auth_with_cert_kwargs',
        'test_cluster_sasl_mech_default',
        'test_cluster_sasl_mech_legacy_multiple',
        'test_cluster_sasl_mech_multiple',
        'test_cluster_metrics_options',
        'test_cluster_metrics_options_fail',
        'test_cluster_metrics_options_kwargs',
        'test_cluster_metrics_options_override',
        'test_cluster_orphan_reporting_options',
        'test_cluster_orphan_reporting_options_fail',
        'test_cluster_orphan_reporting_options_kwargs',
        'test_cluster_orphan_reporting_options_override',
        'test_cluster_timeout_options',
        'test_cluster_timeout_options_fail',
        'test_cluster_timeout_options_kwargs',
        'test_cluster_tracing_options',
        'test_cluster_tracing_options_fail',
        'test_cluster_tracing_options_kwargs',
        'test_cluster_tracing_options_override',
        'test_config_profile_fail',
        'test_custom_config_profile',
        'test_custom_config_profile_fail',
        'test_invalid_connection_strings',
        'test_connection_string_options',
        'test_valid_connection_strings',
        'test_wan_config_profile',
        'test_wan_config_profile_with_auth',
    ]

    def test_cluster_auth_fail(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string)

    def test_cluster_cert_auth(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()

        expected_auth = {
            'cert_path': 'path/to/a/cert',
            'key_path': 'path/to/a/key',
            'trust_store_path': 'path/to/truststore'
        }

        auth = CertificateAuthenticator(**expected_auth)
        cluster = ClusterImpl(conn_string, ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts, _ = cluster._get_connection_opts()
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

        # check as kwargs
        cluster = ClusterImpl(conn_string, authenticator=auth, skip_connect='TEST_SKIP_CONNECT')
        auth_opts, _ = cluster._get_connection_opts()
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

    def test_cluster_cert_auth_fail(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        ts_path = 'path/to/truststore'
        expected_auth = {
            'cert_path': 'path/to/a/cert',
            'key_path': 'path/to/a/key',
            'trust_store_path': ts_path
        }

        with pytest.raises(InvalidArgumentException):
            CertificateAuthenticator(expected_auth)

        auth_copy = copy(expected_auth)
        auth_copy['cert_path'] = None
        with pytest.raises(InvalidArgumentException):
            CertificateAuthenticator(**auth_copy)

        auth_copy = copy(expected_auth)
        auth_copy['cert_path'] = {}
        with pytest.raises(InvalidArgumentException):
            CertificateAuthenticator(**auth_copy)

        auth_copy = copy(expected_auth)
        auth_copy['key_path'] = None
        with pytest.raises(InvalidArgumentException):
            CertificateAuthenticator(**auth_copy)

        auth_copy = copy(expected_auth)
        auth_copy['key_path'] = {}
        with pytest.raises(InvalidArgumentException):
            CertificateAuthenticator(**auth_copy)

        auth_copy = copy(expected_auth)
        auth_copy['trust_store_path'] = {}
        with pytest.raises(InvalidArgumentException):
            CertificateAuthenticator(**auth_copy)

        auth = CertificateAuthenticator(**expected_auth)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, ClusterOptions(auth), trust_store_path=ts_path)

        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, authenticator=auth, trust_store_path=ts_path)

    def test_cluster_cert_auth_ts_connstr(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()

        expected_auth = {
            'cert_path': 'path/to/a/cert',
            'key_path': 'path/to/a/key'
        }

        auth = CertificateAuthenticator(**expected_auth)
        ts_path = 'path/to/truststore'
        cluster = ClusterImpl(f'{conn_string}?truststorepath={ts_path}',
                              ClusterOptions(auth),
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth
        assert cluster_opts is not None
        assert isinstance(cluster_opts, dict)
        assert 'trust_store_path' in cluster_opts
        assert cluster_opts['trust_store_path'] == ts_path

        # check as kwargs
        cluster = ClusterImpl(f'{conn_string}?truststorepath={ts_path}',
                              authenticator=auth,
                              trust_store_path=ts_path,
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth
        assert cluster_opts is not None
        assert isinstance(cluster_opts, dict)
        assert 'trust_store_path' in cluster_opts
        assert cluster_opts['trust_store_path'] == ts_path

    def test_cluster_cert_auth_ts_kwargs(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()

        expected_auth = {
            'cert_path': 'path/to/a/cert',
            'key_path': 'path/to/a/key'
        }

        auth = CertificateAuthenticator(**expected_auth)
        ts_path = 'path/to/truststore'
        cluster = ClusterImpl(conn_string,
                              ClusterOptions(auth),
                              trust_store_path=ts_path,
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth
        assert cluster_opts is not None
        assert isinstance(cluster_opts, dict)
        assert 'trust_store_path' in cluster_opts
        assert cluster_opts['trust_store_path'] == ts_path

        # check as kwargs
        cluster = ClusterImpl(conn_string,
                              authenticator=auth,
                              trust_store_path=ts_path,
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth
        assert cluster_opts is not None
        assert isinstance(cluster_opts, dict)
        assert 'trust_store_path' in cluster_opts
        assert cluster_opts['trust_store_path'] == ts_path

    def test_cluster_ldap_auth(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator.ldap_compatible(username, pw)
        assert isinstance(auth, PasswordAuthenticator)
        expected_auth = auth.as_dict()
        cluster = ClusterImpl(conn_string, ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

    # creating a new connection, allow retries
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    def test_cluster_ldap_auth_real(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()
        auth = PasswordAuthenticator.ldap_compatible(username, pw)
        cluster = Cluster.connect(conn_string, ClusterOptions(auth))

        client_opts = cluster._impl.get_connection_info()
        assert client_opts['credentials'] is not None
        assert client_opts['credentials']['allowed_sasl_mechanisms'] == ['PLAIN']

    def test_cluster_legacy_sasl_mech_force(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        expected_auth = auth.as_dict()
        expected_auth['allowed_sasl_mechanisms'] = ['PLAIN']
        cluster = ClusterImpl(f'{conn_string}?sasl_mech_force=PLAIN',
                              ClusterOptions(auth),
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert cluster_opts is not None
        assert auth_opts is not None
        assert auth_opts == expected_auth

    # creating a new connection, allow retries
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    def test_cluster_legacy_sasl_mech_force_real(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()
        auth = PasswordAuthenticator(username, pw)
        cluster = Cluster.connect(f'{conn_string}?sasl_mech_force=PLAIN', ClusterOptions(auth))

        client_opts = cluster._impl.get_connection_info()
        assert client_opts['credentials'] is not None
        assert client_opts['credentials']['allowed_sasl_mechanisms'] == ['PLAIN']

    def test_cluster_legacy_ssl_no_verify(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster = ClusterImpl(f'{conn_string}?ssl=no_verify', ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        assert cluster_opts is not None
        assert cluster_opts['tls_verify'] == 'none'

    def test_cluster_options(self, couchbase_config):
        opts = {
            'enable_tls': True,
            'enable_mutation_tokens': True,
            'enable_tcp_keep_alive': True,
            'ip_protocol': IpProtocol.Any,
            'enable_dns_srv': True,
            'show_queries': True,
            'enable_unordered_execution': True,
            'enable_clustermap_notification': True,
            'enable_compression': True,
            'enable_tracing': True,
            'enable_metrics': True,
            'enable_orphan_reporting': True,
            'network': 'external',
            'tls_verify': TLSVerifyMode.NO_VERIFY,
            'disable_mozilla_ca_certificates': False,
            'serializer': DefaultJsonSerializer(),
            'transcoder': JSONTranscoder(),
            'tcp_keep_alive_interval': timedelta(seconds=30),
            'config_poll_interval': timedelta(seconds=30),
            'config_poll_floor': timedelta(seconds=30),
            'max_http_connections': 10,
            'num_io_threads': 1,
            'dump_configuration': True,
            'preferred_server_group': 'group1',
            'enable_app_telemetry': True,
            'app_telemetry_endpoint': 'ws://localhost:8093',
            'app_telemetry_backoff': timedelta(seconds=10),
            'app_telemetry_ping_interval': timedelta(seconds=60),
            'app_telemetry_ping_timeout': timedelta(seconds=5),
        }

        expected_opts = copy(opts)
        # serializer is not passed to connection options
        expected_opts.pop('serializer')
        # transcoder is not passed to connection options
        expected_opts.pop('transcoder')
        # timedeltas are translated to microseconds
        # UPDATE: now translated to milliseconds to match C++ core
        expected_opts['tcp_keep_alive_interval'] = 30000
        expected_opts['config_poll_interval'] = 30000
        expected_opts['config_poll_floor'] = 30000
        expected_opts['app_telemetry_backoff'] = 10000
        expected_opts['app_telemetry_ping_interval'] = 60000
        expected_opts['app_telemetry_ping_timeout'] = 5000
        # IpProtocol is translated to string and has another name
        expected_opts.pop('ip_protocol')
        expected_opts['use_ip_protocol'] = 'any'
        # TLSVerifyMode is translated to string
        expected_opts['tls_verify'] = 'none'
        # PYCBC-1718: updates how metrics, orphan reporting and tracing options are handled
        expected_opts.pop('enable_metrics')
        expected_opts['metrics_options'] = {'enable_metrics': True}
        expected_opts.pop('enable_orphan_reporting')
        expected_opts['orphan_reporting_options'] = {'enable_orphan_reporting': True}
        expected_opts.pop('enable_tracing')
        expected_opts['setup_sdk_tracing'] = True

        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)

        # check via ClusterOptions
        cluster_opts = ClusterOptions(auth, **opts)
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT')
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        user_agent = cluster_opts.pop('user_agent_extra', None)
        assert cluster_opts is not None
        assert isinstance(cluster_opts, dict)
        assert cluster_opts == expected_opts
        assert user_agent == f'python/{platform.python_version()}'

        # check via kwargs
        cluster_opts = ClusterOptions(auth)
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT', **opts)
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        user_agent = cluster_opts.pop('user_agent_extra', None)
        assert cluster_opts is not None
        assert isinstance(cluster_opts, dict)
        assert cluster_opts == expected_opts
        assert user_agent == f'python/{platform.python_version()}'

    def test_cluster_pw_auth(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        expected_auth = auth.as_dict()
        cluster = ClusterImpl(conn_string, ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth
        assert auth_opts['allowed_sasl_mechanisms'] is None

        # check as kwargs
        cluster = ClusterImpl(conn_string, authenticator=auth, skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth
        assert auth_opts['allowed_sasl_mechanisms'] is None

    def test_cluster_pw_auth_fail(self):
        expected_auth = {
            'username': 'Administrator',
            'password': 'password',
        }

        # this is OK
        auth = PasswordAuthenticator(**expected_auth)
        assert isinstance(auth, PasswordAuthenticator)

        with pytest.raises(InvalidArgumentException):
            PasswordAuthenticator(None, None)

        auth_copy = copy(expected_auth)
        auth_copy['username'] = None
        with pytest.raises(InvalidArgumentException):
            PasswordAuthenticator(*auth_copy.values())

        auth_copy = copy(expected_auth)
        auth_copy['password'] = None
        with pytest.raises(InvalidArgumentException):
            PasswordAuthenticator(*auth_copy.values())

        with pytest.raises(InvalidArgumentException):
            PasswordAuthenticator(*expected_auth.values(), cert_path={})

        with pytest.raises(InvalidArgumentException):
            PasswordAuthenticator(*expected_auth.values(), allowed_sasl_mechanisms={})

        with pytest.raises(InvalidArgumentException):
            PasswordAuthenticator(*expected_auth.values(), allowed_sasl_mechanisms=['PLAIN', 1])

    def test_cluster_pw_auth_with_cert(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw, cert_path='path/to/cert')
        expected_auth = auth.as_dict()
        cluster = ClusterImpl(conn_string, ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

        # check as kwargs
        cluster = ClusterImpl(conn_string, authenticator=auth, skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

    def test_cluster_pw_auth_with_cert_connstr(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cert_path = 'path/to/cert'
        expected_auth = auth.as_dict()
        expected_auth['cert_path'] = cert_path
        cluster = ClusterImpl(f'{conn_string}?certpath={cert_path}',
                              ClusterOptions(auth),
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

    def test_cluster_pw_auth_with_cert_kwargs(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cert_path = 'path/to/cert'
        expected_auth = auth.as_dict()
        expected_auth['cert_path'] = cert_path
        cluster = ClusterImpl(conn_string,
                              ClusterOptions(auth),
                              cert_path=cert_path,
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

        # check as kwargs
        cluster = ClusterImpl(conn_string, authenticator=auth, cert_path=cert_path, skip_connect='TEST_SKIP_CONNECT')
        auth_opts = cluster._get_connection_opts(auth_only=True)
        assert auth_opts is not None
        assert isinstance(auth_opts, dict)
        assert auth_opts == expected_auth

    # creating a new connection, allow retries
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    def test_cluster_sasl_mech_default(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()
        auth = PasswordAuthenticator(username, pw)
        cluster = Cluster.connect(f'{conn_string}', ClusterOptions(auth))

        client_opts = cluster._impl.get_connection_info()
        assert client_opts['credentials'] is not None
        assert client_opts['credentials']['allowed_sasl_mechanisms'] == []

    def test_cluster_sasl_mech_legacy_multiple(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        expected_auth = auth.as_dict()
        expected_auth['allowed_sasl_mechanisms'] = ['SCRAM-SHA512', 'SCRAM-SHA256']
        conn_str = f'{conn_string}?sasl_mech_force=SCRAM-SHA512&sasl_mech_force=SCRAM-SHA256'
        cluster = ClusterImpl(conn_str, ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert cluster_opts is not None
        assert auth_opts is not None
        assert auth_opts == expected_auth

        cluster = ClusterImpl(f'{conn_string}?sasl_mech_force=SCRAM-SHA512,SCRAM-SHA256',
                              ClusterOptions(auth),
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert cluster_opts is not None
        assert auth_opts is not None
        assert auth_opts == expected_auth

    def test_cluster_sasl_mech_multiple(self, couchbase_config):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        expected_auth = auth.as_dict()
        expected_auth['allowed_sasl_mechanisms'] = ['SCRAM-SHA512', 'SCRAM-SHA256']
        conn_str = f'{conn_string}?allowed_sasl_mechanisms=SCRAM-SHA512&allowed_sasl_mechanisms=SCRAM-SHA256'
        cluster = ClusterImpl(conn_str, ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert cluster_opts is not None
        assert auth_opts is not None
        assert auth_opts == expected_auth

        cluster = ClusterImpl(f'{conn_string}?allowed_sasl_mechanisms=SCRAM-SHA512,SCRAM-SHA256',
                              ClusterOptions(auth),
                              skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert cluster_opts is not None
        assert auth_opts is not None
        assert auth_opts == expected_auth

        auth = PasswordAuthenticator(username, pw, allowed_sasl_mechanisms='SCRAM-SHA512,SCRAM-SHA256')
        cluster = ClusterImpl(f'{conn_string}', ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert cluster_opts is not None
        assert auth_opts is not None
        assert auth_opts == expected_auth

        auth = PasswordAuthenticator(username, pw, allowed_sasl_mechanisms=['SCRAM-SHA512', 'SCRAM-SHA256'])
        cluster = ClusterImpl(f'{conn_string}', ClusterOptions(auth), skip_connect='TEST_SKIP_CONNECT')
        auth_opts, cluster_opts = cluster._get_connection_opts()
        assert cluster_opts is not None
        assert auth_opts is not None
        assert auth_opts == expected_auth

    @pytest.mark.parametrize('opts, expected_opts',
                             [({'enable_metrics': True, 'emit_interval': timedelta(seconds=30)},
                               {'enable_metrics': True, 'metrics_emit_interval': 30000}),
                              ])
    def test_cluster_metrics_options(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth, metrics_options=ClusterMetricsOptions(**opts))
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT')
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        metrics_opts = cluster_opts.get('metrics_options', None)
        assert metrics_opts is not None
        assert isinstance(metrics_opts, dict)
        assert metrics_opts == expected_opts

    # when working w/ kwargs, some options append 'metrics_' to avoid key clash with orphan reporting options
    @pytest.mark.parametrize('opts',
                             [
                                 {'enable_metrics': 1},
                                 {'metrics_emit_interval': 30},
                             ])
    def test_cluster_metrics_options_fail(self, couchbase_config, opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts, **opts)

        cluster_opts = ClusterOptions(auth, metrics_options=opts)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts)

    # when working w/ kwargs, some options append 'metrics_' to avoid key clash with orphan reporting options
    @pytest.mark.parametrize('opts, expected_opts',
                             [({'enable_metrics': True, 'metrics_emit_interval': timedelta(seconds=30)},
                               {'enable_metrics': True, 'metrics_emit_interval': 30000}),
                              ])
    def test_cluster_metrics_options_kwargs(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT', **opts)
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        metrics_opts = cluster_opts.get('metrics_options', None)
        assert metrics_opts is not None
        assert isinstance(metrics_opts, dict)
        assert metrics_opts == expected_opts

    @pytest.mark.parametrize('opts, cluster_opts, expected_opts',
                             [(None,
                               {'enable_metrics': False, 'logging_meter_emit_interval': timedelta(seconds=60)},
                               {'enable_metrics': False, 'metrics_emit_interval': 60000}),
                              ({'enable_metrics': True, 'emit_interval': timedelta(seconds=60)},
                               {'enable_metrics': False, 'logging_meter_emit_interval': timedelta(seconds=30)},
                               {'enable_metrics': True, 'metrics_emit_interval': 60000}),
                              ])
    def test_cluster_metrics_options_override(self, couchbase_config, opts, cluster_opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        metrics_options = ClusterMetricsOptions(**opts) if opts else None
        cluster_options = ClusterOptions(auth,
                                         metrics_options=metrics_options,
                                         **cluster_opts)
        cluster = ClusterImpl(conn_string, cluster_options, skip_connect='TEST_SKIP_CONNECT')
        conn_cluster_opts = cluster._get_connection_opts(conn_only=True)
        assert 'logging_meter_emit_interval' not in conn_cluster_opts
        metrics_opts = conn_cluster_opts.get('metrics_options', None)
        assert metrics_opts is not None
        assert isinstance(metrics_opts, dict)
        assert metrics_opts == expected_opts

    @pytest.mark.parametrize('opts, expected_opts',
                             [({'enable_orphan_reporting': True,
                                'emit_interval': timedelta(seconds=30),
                                'sample_size': 20},
                               {'enable_orphan_reporting': True,
                                'orphan_emit_interval': 30000,
                                'orphan_sample_size': 20}),
                              ])
    def test_cluster_orphan_reporting_options(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth, orphan_reporting_options=ClusterOrphanReportingOptions(**opts))
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT')
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        orphan_opts = cluster_opts.get('orphan_reporting_options', None)
        assert orphan_opts is not None
        assert isinstance(orphan_opts, dict)
        assert orphan_opts == expected_opts

    # when working w/ kwargs, some options append 'orphan_' to avoid key clash with metrics options
    @pytest.mark.parametrize('opts',
                             [
                                 {'enable_orphan_reporting': 1},
                                 {'orphan_emit_interval': 30},
                                 {'orphan_sample_size': 35.1},
                             ])
    def test_cluster_orphan_reporting_options_fail(self, couchbase_config, opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts, **opts)

        cluster_opts = ClusterOptions(auth, orphan_reporting_options=opts)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts)

    # when working w/ kwargs, some options append 'orphan_' to avoid key clash with metrics options
    @pytest.mark.parametrize('opts, expected_opts',
                             [({'enable_orphan_reporting': True,
                                'orphan_emit_interval': timedelta(seconds=30),
                                'orphan_sample_size': 20},
                               {'enable_orphan_reporting': True,
                                'orphan_emit_interval': 30000,
                                'orphan_sample_size': 20}),
                              ])
    def test_cluster_orphan_reporting_options_kwargs(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT', **opts)
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        orphan_opts = cluster_opts.get('orphan_reporting_options', None)
        assert orphan_opts is not None
        assert isinstance(orphan_opts, dict)
        assert orphan_opts == expected_opts

    @pytest.mark.parametrize('opts, cluster_opts, tracing_opts, expected_opts',
                             [(None,
                               {'enable_orphan_reporting': False},
                               {'tracing_orphaned_queue_flush_interval': timedelta(seconds=60),
                                'tracing_orphaned_queue_size': 50},
                               {'enable_orphan_reporting': False,
                                'orphan_emit_interval': 60000,
                                'orphan_sample_size': 50}),
                              ({'enable_orphan_reporting': True,
                                'emit_interval': timedelta(seconds=60),
                                'sample_size': 50},
                               {'enable_orphan_reporting': False},
                               {'tracing_orphaned_queue_flush_interval': timedelta(seconds=30),
                                'tracing_orphaned_queue_size': 20},
                               {'enable_orphan_reporting': True,
                                'orphan_emit_interval': 60000,
                                'orphan_sample_size': 50}),
                              ])
    def test_cluster_orphan_reporting_options_override(self,
                                                       couchbase_config,
                                                       opts,
                                                       cluster_opts,
                                                       tracing_opts,
                                                       expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        orphan_reporting_opts = ClusterOrphanReportingOptions(**opts) if opts else None
        tracing_opts = ClusterTracingOptions(**tracing_opts)
        cluster_opts = ClusterOptions(auth,
                                      orphan_reporting_options=orphan_reporting_opts,
                                      tracing_options=tracing_opts,
                                      **cluster_opts)
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT')
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        orphan_opts = cluster_opts.get('orphan_reporting_options', None)
        tracing_opts = cluster_opts.get('tracing_options', None)
        assert tracing_opts is None
        assert orphan_opts is not None
        assert isinstance(orphan_opts, dict)
        assert orphan_opts == expected_opts

    @pytest.mark.parametrize('opts, expected_opts',
                             [({'bootstrap_timeout': timedelta(seconds=30)},
                               {'bootstrap_timeout': 30000}),
                              ({'resolve_timeout': timedelta(seconds=30)},
                               {'resolve_timeout': 30000}),
                              ({'connect_timeout': timedelta(seconds=30)},
                               {'connect_timeout': 30000}),
                              ({'kv_timeout': timedelta(seconds=30)},
                               {'key_value_timeout': 30000}),
                              ({'kv_durable_timeout': timedelta(seconds=30)},
                               {'key_value_durable_timeout': 30000}),
                              ({'views_timeout': timedelta(seconds=30)},
                               {'view_timeout': 30000}),
                              ({'query_timeout': timedelta(seconds=30)},
                               {'query_timeout': 30000}),
                              ({'analytics_timeout': timedelta(seconds=30)},
                               {'analytics_timeout': 30000}),
                              ({'search_timeout': timedelta(seconds=30)},
                               {'search_timeout': 30000}),
                              ({'management_timeout': timedelta(seconds=30)},
                               {'management_timeout': 30000}),
                              ({'dns_srv_timeout': timedelta(seconds=30)},
                               {'dns_srv_timeout': 30000}),
                              ({'idle_http_connection_timeout': timedelta(seconds=30)},
                               {'idle_http_connection_timeout': 30000}),
                              ({'config_idle_redial_timeout': timedelta(seconds=30)},
                               {'config_idle_redial_timeout': 30000}),
                              ({'bootstrap_timeout': timedelta(seconds=60),
                                'kv_timeout': timedelta(seconds=5),
                                'query_timeout': timedelta(seconds=30),
                                'search_timeout': timedelta(seconds=120),
                                'management_timeout': timedelta(seconds=60)},
                               {'bootstrap_timeout': 60000,
                               'key_value_timeout': 5000,
                                'query_timeout': 30000,
                                'search_timeout': 120000,
                                'management_timeout': 60000}),
                              ])
    def test_cluster_timeout_options(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth, timeout_options=ClusterTimeoutOptions(**opts))
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT')
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        timeout_opts = cluster_opts.get('timeout_options', None)
        assert timeout_opts is not None
        assert isinstance(timeout_opts, dict)
        assert timeout_opts == expected_opts

    @pytest.mark.parametrize('opts',
                             [
                                 {'bootstrap_timeout': 30},
                                 {'resolve_timeout': 30},
                                 {'connect_timeout': 30},
                                 {'kv_timeout': 30},
                                 {'kv_durable_timeout': 30},
                                 {'views_timeout': 30},
                                 {'query_timeout': 30},
                                 {'analytics_timeout': 30},
                                 {'search_timeout': 30},
                                 {'management_timeout': 30},
                                 {'dns_srv_timeout': 30},
                                 {'idle_http_connection_timeout': 30},
                                 {'config_idle_redial_timeout': 30},
                                 {'bootstrap_timeout': 60000,
                                  'kv_timeout': 5000,
                                  'query_timeout': 30000,
                                  'search_timeout': 120000,
                                  'management_timeout': 60000}
                             ])
    def test_cluster_timeout_options_fail(self, couchbase_config, opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts, **opts)

        cluster_opts = ClusterOptions(auth, timeout_options=opts)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts)

    @pytest.mark.parametrize('opts, expected_opts',
                             [({'bootstrap_timeout': timedelta(seconds=30)},
                               {'bootstrap_timeout': 30000}),
                              ({'resolve_timeout': timedelta(seconds=30)},
                               {'resolve_timeout': 30000}),
                              ({'connect_timeout': timedelta(seconds=30)},
                               {'connect_timeout': 30000}),
                              ({'kv_timeout': timedelta(seconds=30)},
                               {'key_value_timeout': 30000}),
                              ({'kv_durable_timeout': timedelta(seconds=30)},
                               {'key_value_durable_timeout': 30000}),
                              ({'views_timeout': timedelta(seconds=30)},
                               {'view_timeout': 30000}),
                              ({'query_timeout': timedelta(seconds=30)},
                               {'query_timeout': 30000}),
                              ({'analytics_timeout': timedelta(seconds=30)},
                               {'analytics_timeout': 30000}),
                              ({'search_timeout': timedelta(seconds=30)},
                               {'search_timeout': 30000}),
                              ({'management_timeout': timedelta(seconds=30)},
                               {'management_timeout': 30000}),
                              ({'dns_srv_timeout': timedelta(seconds=30)},
                               {'dns_srv_timeout': 30000}),
                              ({'idle_http_connection_timeout': timedelta(seconds=30)},
                               {'idle_http_connection_timeout': 30000}),
                              ({'config_idle_redial_timeout': timedelta(seconds=30)},
                               {'config_idle_redial_timeout': 30000}),
                              ({'bootstrap_timeout': timedelta(seconds=60),
                                'kv_timeout': timedelta(seconds=5),
                                'query_timeout': timedelta(seconds=30),
                                'search_timeout': timedelta(seconds=120),
                                'management_timeout': timedelta(seconds=60)},
                               {'bootstrap_timeout': 60000,
                               'key_value_timeout': 5000,
                                'query_timeout': 30000,
                                'search_timeout': 120000,
                                'management_timeout': 60000}),
                              ])
    def test_cluster_timeout_options_kwargs(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT', **opts)
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        timeout_opts = cluster_opts.get('timeout_options', None)
        assert timeout_opts is not None
        assert isinstance(timeout_opts, dict)
        assert timeout_opts == expected_opts

    @pytest.mark.parametrize('opts, expected_opts',
                             [({'tracing_threshold_kv': timedelta(milliseconds=30)},
                               {'key_value_threshold': 30}),
                              ({'tracing_threshold_view': timedelta(milliseconds=30)},
                              {'view_threshold': 30}),
                              ({'tracing_threshold_query': timedelta(milliseconds=30)},
                              {'query_threshold': 30}),
                              ({'tracing_threshold_search': timedelta(milliseconds=30)},
                              {'search_threshold': 30}),
                              ({'tracing_threshold_analytics': timedelta(milliseconds=30)},
                              {'analytics_threshold': 30}),
                              ({'tracing_threshold_eventing': timedelta(milliseconds=30)},
                              {'eventing_threshold': 30}),
                              ({'tracing_threshold_management': timedelta(milliseconds=30)},
                              {'management_threshold': 30}),
                              ({'tracing_threshold_queue_size': 20},
                              {'threshold_sample_size': 20}),
                              ({'tracing_threshold_queue_flush_interval': timedelta(
                                  seconds=30)}, {'threshold_emit_interval': 30000}),
                              ({'tracing_orphaned_queue_size': 20},
                              {'orphan_sample_size': 20}),
                              ({'tracing_orphaned_queue_flush_interval': timedelta(
                                  seconds=30)}, {'orphan_emit_interval': 30000}),
                              ({'enable_tracing': True,
                                'tracing_threshold_kv': timedelta(milliseconds=60),
                                'tracing_threshold_query': timedelta(milliseconds=5),
                                'tracing_threshold_management': timedelta(milliseconds=30),
                                'tracing_threshold_queue_size': 20},
                               {'enable_tracing': True,
                                'key_value_threshold': 60,
                                'query_threshold': 5,
                                'management_threshold': 30,
                                'threshold_sample_size': 20}),
                              ])
    def test_cluster_tracing_options(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth, tracing_options=ClusterTracingOptions(**opts))
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT')
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        conn_tracing_options = cluster_opts.get('tracing_options', None)
        orphan_reporting_options = cluster_opts.get('orphan_reporting_options', None)
        if 'orphan_sample_size' in expected_opts or 'orphan_emit_interval' in expected_opts:
            assert orphan_reporting_options is not None
            assert conn_tracing_options is None
            assert isinstance(orphan_reporting_options, dict)
            assert orphan_reporting_options == expected_opts
        else:
            assert orphan_reporting_options is None
            assert conn_tracing_options is None
            tracing_options = cluster._cluster_settings.tracing_options
            expected_opts['enable_tracing'] = True
            assert isinstance(tracing_options, dict)
            assert tracing_options == expected_opts

    @pytest.mark.parametrize('opts',
                             [
                                 {'enable_tracing': 1},
                                 {'tracing_threshold_kv': 30},
                                 {'tracing_threshold_view': 30},
                                 {'tracing_threshold_query': 30},
                                 {'tracing_threshold_search': 30},
                                 {'tracing_threshold_analytics': 30},
                                 {'tracing_threshold_eventing': 30},
                                 {'tracing_threshold_management': 30},
                                 {'tracing_threshold_queue_flush_interval': 30},
                                 {'tracing_orphaned_queue_flush_interval': 30},
                                 {'tracing_threshold_kv': 60000,
                                  'tracing_threshold_query': 5000,
                                  'tracing_threshold_search': 30000,
                                  'tracing_threshold_management': 120000,
                                  'tracing_threshold_queue_flush_interval': 60000}
                             ])
    def test_cluster_tracing_options_fail(self, couchbase_config, opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts, **opts)

        cluster_opts = ClusterOptions(auth, tracing_options=opts)
        with pytest.raises(InvalidArgumentException):
            ClusterImpl(conn_string, cluster_opts)

    @pytest.mark.parametrize('opts, expected_opts',
                             [({'tracing_threshold_kv': timedelta(milliseconds=30)},
                               {'key_value_threshold': 30}),
                              ({'tracing_threshold_view': timedelta(milliseconds=30)},
                               {'view_threshold': 30}),
                              ({'tracing_threshold_query': timedelta(milliseconds=30)},
                               {'query_threshold': 30}),
                              ({'tracing_threshold_search': timedelta(milliseconds=30)},
                               {'search_threshold': 30}),
                              ({'tracing_threshold_analytics': timedelta(milliseconds=30)},
                               {'analytics_threshold': 30}),
                              ({'tracing_threshold_eventing': timedelta(milliseconds=30)},
                               {'eventing_threshold': 30}),
                              ({'tracing_threshold_management': timedelta(milliseconds=30)},
                               {'management_threshold': 30}),
                              ({'tracing_threshold_queue_size': 20},
                               {'threshold_sample_size': 20}),
                              ({'tracing_threshold_queue_flush_interval': timedelta(
                                  seconds=30)}, {'threshold_emit_interval': 30000}),
                              ({'tracing_orphaned_queue_size': 20},
                               {'orphan_sample_size': 20}),
                              ({'tracing_orphaned_queue_flush_interval': timedelta(
                                  seconds=30)}, {'orphan_emit_interval': 30000}),
                              ({'enable_tracing': True,
                                'tracing_threshold_kv': timedelta(milliseconds=60),
                                'tracing_threshold_query': timedelta(milliseconds=5),
                                'tracing_threshold_management': timedelta(milliseconds=30),
                                'tracing_threshold_queue_size': 20},
                               {'enable_tracing': True,
                                'key_value_threshold': 60,
                                'query_threshold': 5,
                                'management_threshold': 30,
                                'threshold_sample_size': 20}),
                              ])
    def test_cluster_tracing_options_kwargs(self, couchbase_config, opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        cluster_opts = ClusterOptions(auth)
        cluster = ClusterImpl(conn_string, cluster_opts, skip_connect='TEST_SKIP_CONNECT', **opts)
        cluster_opts = cluster._get_connection_opts(conn_only=True)
        conn_tracing_options = cluster_opts.get('tracing_options', None)
        orphan_reporting_options = cluster_opts.get('orphan_reporting_options', None)
        if 'orphan_sample_size' in expected_opts or 'orphan_emit_interval' in expected_opts:
            assert orphan_reporting_options is not None
            assert conn_tracing_options is None
            assert isinstance(orphan_reporting_options, dict)
            assert orphan_reporting_options == expected_opts
        else:
            assert orphan_reporting_options is None
            assert conn_tracing_options is None
            tracing_options = cluster._cluster_settings.tracing_options
            expected_opts['enable_tracing'] = True
            assert isinstance(tracing_options, dict)
            assert tracing_options == expected_opts

    @pytest.mark.parametrize('tracing_opts, cluster_opts, expected_opts',
                             [(None,
                               {'enable_tracing': False},
                               {'enable_tracing': False}),
                              ({'enable_tracing': True},
                               {'enable_tracing': False},
                               {'enable_tracing': True}),
                              ])
    def test_cluster_tracing_options_override(self, couchbase_config, tracing_opts, cluster_opts, expected_opts):
        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()

        auth = PasswordAuthenticator(username, pw)
        tracing_options = ClusterTracingOptions(**tracing_opts) if tracing_opts else None
        cluster_options = ClusterOptions(auth,
                                         tracing_options=tracing_options,
                                         **cluster_opts)
        cluster = ClusterImpl(conn_string, cluster_options, skip_connect='TEST_SKIP_CONNECT')
        conn_cluster_opts = cluster._get_connection_opts(conn_only=True)
        conn_tracing_opts = conn_cluster_opts.get('tracing_options', None)
        cluster_tracing_opts = cluster._cluster_settings.tracing_options
        assert conn_tracing_opts is None
        assert cluster_tracing_opts is not None
        assert isinstance(cluster_tracing_opts, dict)
        assert cluster_tracing_opts == expected_opts

    def test_config_profile_fail(self, couchbase_config):
        username, pw = couchbase_config.get_username_and_pw()
        auth = PasswordAuthenticator(username, pw)
        opts = ClusterOptions(auth)
        with pytest.raises(InvalidArgumentException):
            opts.apply_profile('test_profile')

    # creating a new connection, allow retries
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    def test_custom_config_profile(self, couchbase_config):
        expected_opts = {'bootstrap_timeout': 10000,
                         'resolve_timeout': 2000,
                         'dns_srv_timeout': 5000,
                         'connect_timeout': 5000,
                         'key_value_timeout': 5000,
                         'key_value_durable_timeout': 5000,
                         'view_timeout': 60000,
                         'query_timeout': 60000,
                         'analytics_timeout': 60000,
                         'search_timeout': 60000,
                         'management_timeout': 60000}

        class TestProfile(ConfigProfile):
            def __init__(self):
                super().__init__()

            def apply(self,
                      options  # type: ClusterOptions
                      ) -> None:
                options['kv_timeout'] = timedelta(seconds=5)
                options['kv_durable_timeout'] = timedelta(seconds=5)
                options['dns_srv_timeout'] = timedelta(seconds=5)
                options['connect_timeout'] = timedelta(seconds=5)
                options['analytics_timeout'] = timedelta(seconds=60)
                options['query_timeout'] = timedelta(seconds=60)
                options['search_timeout'] = timedelta(seconds=60)
                options['management_timeout'] = timedelta(seconds=60)
                options['views_timeout'] = timedelta(seconds=60)

        CONFIG_PROFILES.register_profile('test_profile', TestProfile())

        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()
        auth = PasswordAuthenticator(username, pw)
        opts = ClusterOptions(auth)
        opts.apply_profile('test_profile')
        cluster = Cluster.connect(conn_string, opts)
        client_opts = cluster._impl.get_connection_info()
        assert 'timeout_options' in client_opts
        timeout_opts = client_opts['timeout_options']
        for k in expected_opts.keys():
            assert timeout_opts[k] == expected_opts[k]

        profile = CONFIG_PROFILES.unregister_profile('test_profile')
        assert isinstance(profile, ConfigProfile)

    def test_custom_config_profile_fail(self):

        class TestProfile():
            def __init__(self):
                super().__init__()

            def apply(self,
                      options  # type: ClusterOptions
                      ) -> None:
                options['kv_timeout'] = timedelta(seconds=5)
                options['kv_durable_timeout'] = timedelta(seconds=5)
                options['connect_timeout'] = timedelta(seconds=5)
                options['analytics_timeout'] = timedelta(seconds=60)
                options['query_timeout'] = timedelta(seconds=60)
                options['search_timeout'] = timedelta(seconds=60)
                options['management_timeout'] = timedelta(seconds=60)
                options['views_timeout'] = timedelta(seconds=60)

        with pytest.raises(InvalidArgumentException):
            CONFIG_PROFILES.register_profile('test_profile', TestProfile())

    # creating a new connection, allow retries
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    @pytest.mark.parametrize("conn_str", ['http://host1,http://host2',
                                          'https://host2:8091,host3:8091',
                                          'http://::ffff:00ee:2122',
                                          'couchbase://12345:qwer###/default'])
    def test_invalid_connection_strings(self, conn_str):
        try:
            Cluster(conn_str, authenticator=PasswordAuthenticator(
                'Administrator', 'password'), bootstrap_timeout=timedelta(seconds=1))
        except (InvalidArgumentException, UnAmbiguousTimeoutException):
            pass
        except Exception as ex:
            pytest.fail(f'Unexpected exception occurred: {ex}')

    @pytest.mark.parametrize("conn_str", ['10.0.0.1:8091',
                                          'http://10.0.0.1',
                                          'couchbase://10.0.0.1',
                                          'couchbases://10.0.0.1:11222,10.0.0.2,10.0.0.3:11207',
                                          'couchbase://10.0.0.1;10.0.0.2:11210;10.0.0.3',
                                          'couchbase://[3ffe:2a00:100:7031::1]',
                                          'couchbases://[::ffff:192.168.0.1]:11207,[::ffff:192.168.0.2]:11207',
                                          'couchbase://test.local:11210?key=value',
                                          'http://fqdn',
                                          'http://fqdn?key=value',
                                          'couchbases://fqdn'
                                          ])
    def test_valid_connection_strings(self, conn_str):
        expected_opts = {'timeout_options': {'bootstrap_timeout': 1000}, 'setup_sdk_tracing': True}
        try:
            if conn_str == '10.0.0.1:8091':
                with warnings.catch_warnings(record=True) as w:
                    # Cause all warnings to always be triggered.
                    warnings.simplefilter("always")

                    cl = ClusterImpl(conn_str,
                                     authenticator=PasswordAuthenticator('Administrator', 'password'),
                                     skip_connect='TEST_SKIP_CONNECT',
                                     bootstrap_timeout=timedelta(seconds=1))
                    assert len(w) == 1
                    assert issubclass(w[-1].category, DeprecationWarning)
                    assert "deprecated" in str(w[-1].message)
            else:
                cl = ClusterImpl(conn_str,
                                 authenticator=PasswordAuthenticator('Administrator', 'password'),
                                 skip_connect='TEST_SKIP_CONNECT',
                                 bootstrap_timeout=timedelta(seconds=1))

            user_agent = cl.cluster_settings.cluster_options.pop('user_agent_extra', None)
            assert expected_opts == cl.cluster_settings.cluster_options
            assert user_agent == f'python/{platform.python_version()}'
            expected_conn_str = conn_str.split('?')[0]
            assert expected_conn_str == cl.cluster_settings.connstr
        except CouchbaseException:
            pass
        except Exception as ex:
            pytest.fail(f'Unexpected exception occurred: {ex}')

    @pytest.mark.parametrize('conn_str, expected_opts',
                             [('couchbase://10.0.0.1?num_io_threads=1&dump_configuration=true',
                               {'num_io_threads': 1, 'dump_configuration': True}),
                              ('couchbase://10.0.0.1?max_http_connections=4&disable_mozilla_ca_certificates=False',
                               {'max_http_connections': 4, 'disable_mozilla_ca_certificates': False}),
                              ('couchbase://localhost?enable_app_telemetry=true&app_telemetry_endpoint=ws://localhost:8091',  # noqa: E501
                               {'enable_app_telemetry': True, 'app_telemetry_endpoint': 'ws://localhost:8091'}),
                              ('couchbase://10.0.0.1?an_invalid_option=10',
                               {}),
                              ])
    def test_connection_string_options(self, conn_str, expected_opts):
        try:
            cl = ClusterImpl(conn_str,
                             authenticator=PasswordAuthenticator('Administrator', 'password'),
                             skip_connect='TEST_SKIP_CONNECT')

            user_agent = cl.cluster_settings.cluster_options.pop('user_agent_extra', None)
            expected_opts['setup_sdk_tracing'] = True
            assert expected_opts == cl.cluster_settings.cluster_options
            assert user_agent == f'python/{platform.python_version()}'
            expected_conn_str = conn_str.split('?')[0]
            assert expected_conn_str == cl.cluster_settings.connstr
        except CouchbaseException:
            pass
        except Exception as ex:
            pytest.fail(f'Unexpected exception occurred: {ex}')

    # creating a new connection, allow retries
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    @pytest.mark.parametrize('profile',
                             [KnownConfigProfiles.WanDevelopment, 'wan_development'])
    def test_wan_config_profile(self, couchbase_config, profile):
        expected_opts = {'bootstrap_timeout': 120000,
                         'resolve_timeout': 20000,
                         'connect_timeout': 20000,
                         'dns_srv_timeout': 20000,
                         'key_value_timeout': 20000,
                         'key_value_durable_timeout': 20000,
                         'view_timeout': 120000,
                         'query_timeout': 120000,
                         'analytics_timeout': 120000,
                         'search_timeout': 120000,
                         'management_timeout': 120000}

        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()
        auth = PasswordAuthenticator(username, pw)
        opts = ClusterOptions(auth)
        opts.apply_profile(profile)
        cluster = Cluster.connect(conn_string, opts)
        client_opts = cluster._impl.get_connection_info()
        assert 'timeout_options' in client_opts
        timeout_opts = client_opts['timeout_options']
        for k in expected_opts.keys():
            assert timeout_opts[k] == expected_opts[k]

    # creating a new connection, allow retries
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    @pytest.mark.parametrize('profile',
                             [KnownConfigProfiles.WanDevelopment, 'wan_development'])
    def test_wan_config_profile_with_auth(self, couchbase_config, profile):
        expected_opts = {'bootstrap_timeout': 120000,
                         'resolve_timeout': 20000,
                         'connect_timeout': 20000,
                         'dns_srv_timeout': 20000,
                         'key_value_timeout': 20000,
                         'key_value_durable_timeout': 20000,
                         'view_timeout': 120000,
                         'query_timeout': 120000,
                         'analytics_timeout': 120000,
                         'search_timeout': 120000,
                         'management_timeout': 120000}

        conn_string = couchbase_config.get_connection_string()
        username, pw = couchbase_config.get_username_and_pw()
        auth = PasswordAuthenticator(username, pw)
        cluster = Cluster.connect(conn_string, ClusterOptions.create_options_with_profile(auth, profile))
        client_opts = cluster._impl.get_connection_info()
        assert 'timeout_options' in client_opts
        timeout_opts = client_opts['timeout_options']
        for k in expected_opts.keys():
            assert timeout_opts[k] == expected_opts[k]


class ClassicConnectionTests(ConnectionTestSuite):

    @pytest.fixture(scope='class', autouse=True)
    def manifest_validated(self):
        def valid_test_method(meth):
            attr = getattr(ClassicConnectionTests, meth)
            return callable(attr) and not meth.startswith('__') and meth.startswith('test')
        method_list = [meth for meth in dir(ClassicConnectionTests) if valid_test_method(meth)]
        test_list = set(ConnectionTestSuite.TEST_MANIFEST).symmetric_difference(method_list)
        if test_list:
            pytest.fail(f'Test manifest not validated.  Missing/extra tests: {test_list}.')

    @pytest.fixture(scope='class', name='cb_env', params=[CollectionType.DEFAULT])
    def couchbase_test_environment(self, cb_base_env, request):
        cb_base_env.setup(request.param)
        yield cb_base_env
        cb_base_env.teardown(request.param)
