# Copyright 2017 Google LLC
#
# 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.

import datetime

import mock
import pytest

try:
    import grpc  # noqa: F401
except ImportError:
    pytest.skip("No GRPC", allow_module_level=True)


from google.api_core import exceptions
from google.api_core import retry
from google.api_core import timeout
import google.api_core.gapic_v1.client_info
import google.api_core.gapic_v1.method
import google.api_core.page_iterator


def _utcnow_monotonic():
    curr_value = datetime.datetime.min
    delta = datetime.timedelta(seconds=0.5)
    while True:
        yield curr_value
        curr_value += delta


def test__determine_timeout():
    # Check _determine_timeout always returns a Timeout object.
    timeout_type_timeout = timeout.ConstantTimeout(600.0)
    returned_timeout = google.api_core.gapic_v1.method._determine_timeout(
        600.0, 600.0, None
    )
    assert isinstance(returned_timeout, timeout.ConstantTimeout)
    returned_timeout = google.api_core.gapic_v1.method._determine_timeout(
        600.0, timeout_type_timeout, None
    )
    assert isinstance(returned_timeout, timeout.ConstantTimeout)
    returned_timeout = google.api_core.gapic_v1.method._determine_timeout(
        timeout_type_timeout, 600.0, None
    )
    assert isinstance(returned_timeout, timeout.ConstantTimeout)
    returned_timeout = google.api_core.gapic_v1.method._determine_timeout(
        timeout_type_timeout, timeout_type_timeout, None
    )
    assert isinstance(returned_timeout, timeout.ConstantTimeout)


def test_wrap_method_basic():
    method = mock.Mock(spec=["__call__"], return_value=42)

    wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)

    result = wrapped_method(1, 2, meep="moop")

    assert result == 42
    method.assert_called_once_with(1, 2, meep="moop", metadata=mock.ANY)

    # Check that the default client info was specified in the metadata.
    metadata = method.call_args[1]["metadata"]
    assert len(metadata) == 1
    client_info = google.api_core.gapic_v1.client_info.DEFAULT_CLIENT_INFO
    user_agent_metadata = client_info.to_grpc_metadata()
    assert user_agent_metadata in metadata


def test_wrap_method_with_no_client_info():
    method = mock.Mock(spec=["__call__"])

    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
        method, client_info=None
    )

    wrapped_method(1, 2, meep="moop")

    method.assert_called_once_with(1, 2, meep="moop")


def test_wrap_method_with_custom_client_info():
    client_info = google.api_core.gapic_v1.client_info.ClientInfo(
        python_version=1,
        grpc_version=2,
        api_core_version=3,
        gapic_version=4,
        client_library_version=5,
    )
    method = mock.Mock(spec=["__call__"])

    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
        method, client_info=client_info
    )

    wrapped_method(1, 2, meep="moop")

    method.assert_called_once_with(1, 2, meep="moop", metadata=mock.ANY)

    # Check that the custom client info was specified in the metadata.
    metadata = method.call_args[1]["metadata"]
    assert client_info.to_grpc_metadata() in metadata


def test_invoke_wrapped_method_with_metadata():
    method = mock.Mock(spec=["__call__"])

    wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)

    wrapped_method(mock.sentinel.request, metadata=[("a", "b")])

    method.assert_called_once_with(mock.sentinel.request, metadata=mock.ANY)
    metadata = method.call_args[1]["metadata"]
    # Metadata should have two items: the client info metadata and our custom
    # metadata.
    assert len(metadata) == 2
    assert ("a", "b") in metadata


def test_invoke_wrapped_method_with_metadata_as_none():
    method = mock.Mock(spec=["__call__"])

    wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)

    wrapped_method(mock.sentinel.request, metadata=None)

    method.assert_called_once_with(mock.sentinel.request, metadata=mock.ANY)
    metadata = method.call_args[1]["metadata"]
    # Metadata should have just one items: the client info metadata.
    assert len(metadata) == 1


@mock.patch("time.sleep")
def test_wrap_method_with_default_retry_and_timeout(unusued_sleep):
    method = mock.Mock(
        spec=["__call__"], side_effect=[exceptions.InternalServerError(None), 42]
    )
    default_retry = retry.Retry()
    default_timeout = timeout.ConstantTimeout(60)
    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
        method, default_retry, default_timeout
    )

    result = wrapped_method()

    assert result == 42
    assert method.call_count == 2
    method.assert_called_with(timeout=60, metadata=mock.ANY)


@mock.patch("time.sleep")
def test_wrap_method_with_default_retry_and_timeout_using_sentinel(unusued_sleep):
    method = mock.Mock(
        spec=["__call__"], side_effect=[exceptions.InternalServerError(None), 42]
    )
    default_retry = retry.Retry()
    default_timeout = timeout.ConstantTimeout(60)
    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
        method, default_retry, default_timeout
    )

    result = wrapped_method(
        retry=google.api_core.gapic_v1.method.DEFAULT,
        timeout=google.api_core.gapic_v1.method.DEFAULT,
    )

    assert result == 42
    assert method.call_count == 2
    method.assert_called_with(timeout=60, metadata=mock.ANY)


@mock.patch("time.sleep")
def test_wrap_method_with_overriding_retry_and_timeout(unusued_sleep):
    method = mock.Mock(spec=["__call__"], side_effect=[exceptions.NotFound(None), 42])
    default_retry = retry.Retry()
    default_timeout = timeout.ConstantTimeout(60)
    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
        method, default_retry, default_timeout
    )

    result = wrapped_method(
        retry=retry.Retry(retry.if_exception_type(exceptions.NotFound)),
        timeout=timeout.ConstantTimeout(22),
    )

    assert result == 42
    assert method.call_count == 2
    method.assert_called_with(timeout=22, metadata=mock.ANY)


@mock.patch("time.sleep")
@mock.patch(
    "google.api_core.datetime_helpers.utcnow",
    side_effect=_utcnow_monotonic(),
    autospec=True,
)
def test_wrap_method_with_overriding_retry_deadline(utcnow, unused_sleep):
    method = mock.Mock(
        spec=["__call__"],
        side_effect=([exceptions.InternalServerError(None)] * 4) + [42],
    )
    default_retry = retry.Retry()
    default_timeout = timeout.ExponentialTimeout(deadline=60)
    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
        method, default_retry, default_timeout
    )

    # Overriding only the retry's deadline should also override the timeout's
    # deadline.
    result = wrapped_method(retry=default_retry.with_deadline(30))

    assert result == 42
    timeout_args = [call[1]["timeout"] for call in method.call_args_list]
    assert timeout_args == [5.0, 10.0, 20.0, 26.0, 25.0]
    assert utcnow.call_count == (
        1
        + 5  # First to set the deadline.
        + 5  # One for each min(timeout, maximum, (DEADLINE - NOW).seconds)
    )


def test_wrap_method_with_overriding_timeout_as_a_number():
    method = mock.Mock(spec=["__call__"], return_value=42)
    default_retry = retry.Retry()
    default_timeout = timeout.ConstantTimeout(60)
    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
        method, default_retry, default_timeout
    )

    result = wrapped_method(timeout=22)

    assert result == 42
    method.assert_called_once_with(timeout=22, metadata=mock.ANY)
