#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
#

"""Unit tests for connection-level options surfaced on Client,
DriverRemoteConnection, and the SigV4 credentials-provider variant."""

import warnings
from concurrent.futures import Future
from unittest.mock import MagicMock, patch

import pytest

from gremlin_python.driver.client import Client
from gremlin_python.driver.connection import Connection
from gremlin_python.driver.request import RequestMessage
from gremlin_python.process.traversal import GremlinLang, GValue


# Patch Connection so Client._fill_pool does not attempt any real connections.
def _make_client(**kwargs):
    with patch('gremlin_python.driver.client.connection.Connection', MagicMock()):
        return Client('http://localhost:8182/gremlin', 'g', **kwargs)


class TestMaxConnections:

    def test_default_is_128(self):
        client = _make_client()
        assert client._max_connections == 128

    def test_explicit_max_connections(self):
        client = _make_client(max_connections=4)
        assert client._max_connections == 4


class TestBatchSize:

    def test_default_is_64(self):
        client = _make_client()
        assert client._batch_size == 64

    def test_explicit_value(self):
        client = _make_client(batch_size=200)
        assert client._batch_size == 200

    def test_fills_batch_size_when_unset(self):
        client = _make_client(batch_size=64)
        conn = MagicMock()
        client._pool.get = MagicMock(return_value=conn)
        client.submit_async('g.V()')
        sent = conn.write.call_args[0][0]
        assert sent.fields['batchSize'] == 64

    def test_does_not_override_per_request_batch_size(self):
        client = _make_client(batch_size=64)
        conn = MagicMock()
        client._pool.get = MagicMock(return_value=conn)
        client.submit_async('g.V()', request_options={'batchSize': 10})
        sent = conn.write.call_args[0][0]
        assert sent.fields['batchSize'] == 10


class TestMaxConnectionsThreadedToTransport:

    def test_max_connections_forwarded_to_connection(self):
        # Client must pass max_connections through to each Connection so the
        # aiohttp connector limit can be set, in addition to sizing the pool.
        captured = {}

        def fake_connection(*args, **kwargs):
            captured.update(kwargs)
            return MagicMock()

        with patch('gremlin_python.driver.client.connection.Connection',
                   side_effect=fake_connection):
            Client('http://localhost:8182/gremlin', 'g', max_connections=5)
        assert captured.get('max_connections') == 5

    def test_default_max_connections_forwarded_to_connection(self):
        captured = {}

        def fake_connection(*args, **kwargs):
            captured.update(kwargs)
            return MagicMock()

        with patch('gremlin_python.driver.client.connection.Connection',
                   side_effect=fake_connection):
            Client('http://localhost:8182/gremlin', 'g')
        assert captured.get('max_connections') == 128


class TestRequestMessageNoMutation:

    def test_resubmit_does_not_accumulate_fields(self):
        # A caller-supplied RequestMessage must not be mutated in place: the
        # second submit with different options must not see the first submit's
        # batchSize/request_options leak in.
        client = _make_client(batch_size=64)
        conn = MagicMock()
        client._pool.get = MagicMock(return_value=conn)

        original = RequestMessage(fields={'g': 'g'}, gremlin='g.V()')

        client.submit_async(original, request_options={'timeoutMillis': 1000})
        # The caller's original message must be untouched.
        assert 'batchSize' not in original.fields
        assert 'timeoutMillis' not in original.fields
        assert original.fields == {'g': 'g'}

        # Resubmit the same message with different options; it must not carry
        # over state from the first submit.
        client.submit_async(original, request_options={'batchSize': 5})
        sent = conn.write.call_args[0][0]
        assert sent.fields['batchSize'] == 5
        assert 'timeoutMillis' not in sent.fields
        # And the original is still pristine.
        assert original.fields == {'g': 'g'}

    def test_batch_size_not_written_to_caller_message(self):
        # Use a non-default value so the assertion proves the configured
        # batch_size flowed through, not the library default (64).
        client = _make_client(batch_size=32)
        conn = MagicMock()
        client._pool.get = MagicMock(return_value=conn)
        original = RequestMessage(fields={'g': 'g'}, gremlin='g.V()')
        client.submit_async(original)
        # default batchSize was applied to the sent clone, not the caller's msg
        sent = conn.write.call_args[0][0]
        assert sent.fields['batchSize'] == 32
        assert 'batchSize' not in original.fields


class TestHeadersKwargRemoved:

    def test_client_rejects_headers_kwarg(self):
        # headers is no longer a named parameter; it lands in transport_kwargs
        # and would be forwarded to the transport. Verify Client has no _headers.
        client = _make_client()
        assert not hasattr(client, '_headers')

    def test_connection_has_no_headers_param(self):
        import inspect
        params = inspect.signature(Connection.__init__).parameters
        assert 'headers' not in params

    def test_connection_internal_headers_default_none(self):
        conn = Connection(
            url='http://localhost:8182/gremlin',
            traversal_source='g',
            executor=MagicMock(),
            pool=MagicMock(),
            enable_user_agent_on_connect=False,
        )
        assert conn._headers is None


class TestDriverRemoteConnectionOptions:

    def test_max_connections_forwarded(self):
        from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
        with patch('gremlin_python.driver.driver_remote_connection.client.Client') as MockClient:
            instance = MockClient.return_value
            instance._url = 'http://localhost:8182/gremlin'
            instance._traversal_source = 'g'
            DriverRemoteConnection('http://localhost:8182/gremlin', 'g',
                                   max_connections=16, batch_size=32)
        _, kwargs = MockClient.call_args
        assert kwargs['max_connections'] == 16
        assert kwargs['batch_size'] == 32

    def test_no_headers_param(self):
        import inspect
        from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
        params = inspect.signature(DriverRemoteConnection.__init__).parameters
        assert 'headers' not in params
        assert 'pool_size' not in params
        assert 'max_connections' in params

    def test_submit_async_signature_single_arg(self):
        import inspect
        from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
        params = list(inspect.signature(DriverRemoteConnection.submit_async).parameters)
        # self + gremlin_lang only
        assert params == ['self', 'gremlin_lang']

    def test_submit_does_not_send_traversal_source_as_parameter(self):
        from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
        with patch('gremlin_python.driver.driver_remote_connection.client.Client') as MockClient:
            instance = MockClient.return_value
            instance._url = 'http://localhost:8182/gremlin'
            instance._traversal_source = 'mySource'
            instance.submit.return_value = []
            connection = DriverRemoteConnection('http://localhost:8182/gremlin', 'mySource')
            gremlin_lang = GremlinLang()
            gremlin_lang.add_step('V')

            connection.submit(gremlin_lang)

        request_options = instance.submit.call_args.kwargs['request_options']
        assert "'g':" not in request_options.get('parameters', '')

    def test_submit_sends_only_explicit_parameters(self):
        from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
        with patch('gremlin_python.driver.driver_remote_connection.client.Client') as MockClient:
            instance = MockClient.return_value
            instance._url = 'http://localhost:8182/gremlin'
            instance._traversal_source = 'mySource'
            instance.submit.return_value = []
            connection = DriverRemoteConnection('http://localhost:8182/gremlin', 'mySource')
            gremlin_lang = GremlinLang()
            gremlin_lang.add_step('V', GValue('x', 42))

            connection.submit(gremlin_lang)

        request_options = instance.submit.call_args.kwargs['request_options']
        assert request_options['parameters'] == "['x':42]"
        assert "'g':" not in request_options['parameters']

    def test_submit_async_does_not_send_traversal_source_as_parameter(self):
        from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
        with patch('gremlin_python.driver.driver_remote_connection.client.Client') as MockClient:
            instance = MockClient.return_value
            instance._url = 'http://localhost:8182/gremlin'
            instance._traversal_source = 'mySource'
            result = Future()
            result.set_result([])
            instance.submit_async.return_value = result
            connection = DriverRemoteConnection('http://localhost:8182/gremlin', 'mySource')
            gremlin_lang = GremlinLang()
            gremlin_lang.add_step('V')

            connection.submit_async(gremlin_lang).result()

        request_options = instance.submit_async.call_args.kwargs['request_options']
        assert "'g':" not in request_options.get('parameters', '')

    def test_transaction_submit_does_not_send_traversal_source_as_parameter(self):
        from gremlin_python.driver.transaction import TransactionRemoteConnection
        client = MagicMock()
        client._url = 'http://localhost:8182/gremlin'
        client._traversal_source = 'mySource'
        client.submit.return_value = []
        transaction = MagicMock()
        transaction._client = client
        transaction.is_open = True
        transaction.transaction_id = 'tx-1'
        connection = TransactionRemoteConnection(transaction)
        gremlin_lang = GremlinLang()
        gremlin_lang.add_step('V')

        connection.submit(gremlin_lang)

        request_options = client.submit.call_args.kwargs['request_options']
        assert "'g':" not in request_options.get('parameters', '')

    def test_transaction_submit_sends_only_explicit_parameters(self):
        from gremlin_python.driver.transaction import TransactionRemoteConnection
        client = MagicMock()
        client._url = 'http://localhost:8182/gremlin'
        client._traversal_source = 'mySource'
        client.submit.return_value = []
        transaction = MagicMock()
        transaction._client = client
        transaction.is_open = True
        transaction.transaction_id = 'tx-1'
        connection = TransactionRemoteConnection(transaction)
        gremlin_lang = GremlinLang()
        gremlin_lang.add_step('V', GValue('x', 42))

        connection.submit(gremlin_lang)

        request_options = client.submit.call_args.kwargs['request_options']
        assert request_options['transactionId'] == 'tx-1'
        assert request_options['parameters'] == "['x':42]"
        assert "'g':" not in request_options['parameters']



class TestSigV4CredentialsProvider:

    def _fake_request(self):
        req = MagicMock()
        req.method = 'POST'
        req.url = 'http://localhost:8182/gremlin'
        req.serialize_body.return_value = b'{}'
        req.headers = {}
        return req

    def test_callable_credentials_provider_used(self):
        from gremlin_python.driver.auth import sigv4

        sentinel_creds = object()
        provider = MagicMock(return_value=sentinel_creds)

        with patch('botocore.auth.SigV4Auth') as MockAuth:
            signer = MockAuth.return_value
            signer.add_auth = MagicMock()
            interceptor = sigv4('us-east-1', 'neptune-db', credentials=provider)
            req = self._fake_request()
            interceptor(req)

        provider.assert_called_once()
        # SigV4Auth must be constructed with the credentials returned by the provider
        args, _ = MockAuth.call_args
        assert args[0] is sentinel_creds

    def test_credentials_object_with_get_credentials(self):
        from gremlin_python.driver.auth import sigv4

        resolved = object()

        class FakeSession:  # not callable; mimics a botocore Session
            def get_credentials(self_inner):
                return resolved

        with patch('botocore.auth.SigV4Auth') as MockAuth:
            MockAuth.return_value.add_auth = MagicMock()
            interceptor = sigv4('us-east-1', 'neptune-db', credentials=FakeSession())
            interceptor(self._fake_request())

        args, _ = MockAuth.call_args
        assert args[0] is resolved

    def test_env_fallback_when_no_provider(self):
        from gremlin_python.driver.auth import sigv4

        resolved = object()
        with patch('boto3.Session') as MockSession, \
                patch('botocore.auth.SigV4Auth') as MockAuth:
            MockSession.return_value.get_credentials.return_value = resolved
            MockAuth.return_value.add_auth = MagicMock()
            interceptor = sigv4('us-east-1', 'neptune-db')
            interceptor(self._fake_request())

        # With no provider supplied, credentials must be resolved from the AWS
        # environment via a boto3 Session, and those resolved credentials must be
        # the ones handed to the SigV4 signer.
        MockSession.assert_called_once()
        MockSession.return_value.get_credentials.assert_called_once()
        args, _ = MockAuth.call_args
        assert args[0] is resolved
