# Copyright 2014 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 http.client
import json

import mock
import pytest
import requests

try:
    import grpc
    from grpc_status import rpc_status
except ImportError:
    grpc = rpc_status = None

from google.api_core import exceptions
from google.protobuf import any_pb2, json_format
from google.rpc import error_details_pb2, status_pb2


def test_create_google_cloud_error():
    exception = exceptions.GoogleAPICallError("Testing")
    exception.code = 600
    assert str(exception) == "600 Testing"
    assert exception.message == "Testing"
    assert exception.errors == []
    assert exception.response is None


def test_create_google_cloud_error_with_args():
    error = {
        "code": 600,
        "message": "Testing",
    }
    response = mock.sentinel.response
    exception = exceptions.GoogleAPICallError("Testing", [error], response=response)
    exception.code = 600
    assert str(exception) == "600 Testing"
    assert exception.message == "Testing"
    assert exception.errors == [error]
    assert exception.response == response


def test_from_http_status():
    message = "message"
    exception = exceptions.from_http_status(http.client.NOT_FOUND, message)
    assert exception.code == http.client.NOT_FOUND
    assert exception.message == message
    assert exception.errors == []


def test_from_http_status_with_errors_and_response():
    message = "message"
    errors = ["1", "2"]
    response = mock.sentinel.response
    exception = exceptions.from_http_status(
        http.client.NOT_FOUND, message, errors=errors, response=response
    )

    assert isinstance(exception, exceptions.NotFound)
    assert exception.code == http.client.NOT_FOUND
    assert exception.message == message
    assert exception.errors == errors
    assert exception.response == response


def test_from_http_status_unknown_code():
    message = "message"
    status_code = 156
    exception = exceptions.from_http_status(status_code, message)
    assert exception.code == status_code
    assert exception.message == message


def make_response(content):
    response = requests.Response()
    response._content = content
    response.status_code = http.client.NOT_FOUND
    response.request = requests.Request(
        method="POST", url="https://example.com"
    ).prepare()
    return response


def test_from_http_response_no_content():
    response = make_response(None)

    exception = exceptions.from_http_response(response)

    assert isinstance(exception, exceptions.NotFound)
    assert exception.code == http.client.NOT_FOUND
    assert exception.message == "POST https://example.com/: unknown error"
    assert exception.response == response


def test_from_http_response_text_content():
    response = make_response(b"message")
    response.encoding = "UTF8"  # suppress charset_normalizer warning

    exception = exceptions.from_http_response(response)

    assert isinstance(exception, exceptions.NotFound)
    assert exception.code == http.client.NOT_FOUND
    assert exception.message == "POST https://example.com/: message"


def test_from_http_response_json_content():
    response = make_response(
        json.dumps({"error": {"message": "json message", "errors": ["1", "2"]}}).encode(
            "utf-8"
        )
    )

    exception = exceptions.from_http_response(response)

    assert isinstance(exception, exceptions.NotFound)
    assert exception.code == http.client.NOT_FOUND
    assert exception.message == "POST https://example.com/: json message"
    assert exception.errors == ["1", "2"]


def test_from_http_response_bad_json_content():
    response = make_response(json.dumps({"meep": "moop"}).encode("utf-8"))

    exception = exceptions.from_http_response(response)

    assert isinstance(exception, exceptions.NotFound)
    assert exception.code == http.client.NOT_FOUND
    assert exception.message == "POST https://example.com/: unknown error"


def test_from_http_response_json_unicode_content():
    response = make_response(
        json.dumps(
            {"error": {"message": "\u2019 message", "errors": ["1", "2"]}}
        ).encode("utf-8")
    )

    exception = exceptions.from_http_response(response)

    assert isinstance(exception, exceptions.NotFound)
    assert exception.code == http.client.NOT_FOUND
    assert exception.message == "POST https://example.com/: \u2019 message"
    assert exception.errors == ["1", "2"]


@pytest.mark.skipif(grpc is None, reason="No grpc")
def test_from_grpc_status():
    message = "message"
    exception = exceptions.from_grpc_status(grpc.StatusCode.OUT_OF_RANGE, message)
    assert isinstance(exception, exceptions.BadRequest)
    assert isinstance(exception, exceptions.OutOfRange)
    assert exception.code == http.client.BAD_REQUEST
    assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE
    assert exception.message == message
    assert exception.errors == []


@pytest.mark.skipif(grpc is None, reason="No grpc")
def test_from_grpc_status_as_int():
    message = "message"
    exception = exceptions.from_grpc_status(11, message)
    assert isinstance(exception, exceptions.BadRequest)
    assert isinstance(exception, exceptions.OutOfRange)
    assert exception.code == http.client.BAD_REQUEST
    assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE
    assert exception.message == message
    assert exception.errors == []


@pytest.mark.skipif(grpc is None, reason="No grpc")
def test_from_grpc_status_with_errors_and_response():
    message = "message"
    response = mock.sentinel.response
    errors = ["1", "2"]
    exception = exceptions.from_grpc_status(
        grpc.StatusCode.OUT_OF_RANGE, message, errors=errors, response=response
    )

    assert isinstance(exception, exceptions.OutOfRange)
    assert exception.message == message
    assert exception.errors == errors
    assert exception.response == response


@pytest.mark.skipif(grpc is None, reason="No grpc")
def test_from_grpc_status_unknown_code():
    message = "message"
    exception = exceptions.from_grpc_status(grpc.StatusCode.OK, message)
    assert exception.grpc_status_code == grpc.StatusCode.OK
    assert exception.message == message


@pytest.mark.skipif(grpc is None, reason="No grpc")
def test_from_grpc_error():
    message = "message"
    error = mock.create_autospec(grpc.Call, instance=True)
    error.code.return_value = grpc.StatusCode.INVALID_ARGUMENT
    error.details.return_value = message

    exception = exceptions.from_grpc_error(error)

    assert isinstance(exception, exceptions.BadRequest)
    assert isinstance(exception, exceptions.InvalidArgument)
    assert exception.code == http.client.BAD_REQUEST
    assert exception.grpc_status_code == grpc.StatusCode.INVALID_ARGUMENT
    assert exception.message == message
    assert exception.errors == [error]
    assert exception.response == error


@pytest.mark.skipif(grpc is None, reason="No grpc")
def test_from_grpc_error_non_call():
    message = "message"
    error = mock.create_autospec(grpc.RpcError, instance=True)
    error.__str__.return_value = message

    exception = exceptions.from_grpc_error(error)

    assert isinstance(exception, exceptions.GoogleAPICallError)
    assert exception.code is None
    assert exception.grpc_status_code is None
    assert exception.message == message
    assert exception.errors == [error]
    assert exception.response == error


@pytest.mark.skipif(grpc is None, reason="No grpc")
def test_from_grpc_error_bare_call():
    message = "Testing"

    class TestingError(grpc.Call, grpc.RpcError):
        def __init__(self, exception):
            self.exception = exception

        def code(self):
            return self.exception.grpc_status_code

        def details(self):
            return message

    nested_message = "message"
    error = TestingError(exceptions.GoogleAPICallError(nested_message))

    exception = exceptions.from_grpc_error(error)

    assert isinstance(exception, exceptions.GoogleAPICallError)
    assert exception.code is None
    assert exception.grpc_status_code is None
    assert exception.message == message
    assert exception.errors == [error]
    assert exception.response == error
    assert exception.details == []


def create_bad_request_details():
    bad_request_details = error_details_pb2.BadRequest()
    field_violation = bad_request_details.field_violations.add()
    field_violation.field = "document.content"
    field_violation.description = "Must have some text content to annotate."
    status_detail = any_pb2.Any()
    status_detail.Pack(bad_request_details)
    return status_detail


def create_error_info_details():
    info = error_details_pb2.ErrorInfo(
        reason="SERVICE_DISABLED",
        domain="googleapis.com",
        metadata={
            "consumer": "projects/455411330361",
            "service": "translate.googleapis.com",
        },
    )
    status_detail = any_pb2.Any()
    status_detail.Pack(info)
    return status_detail


def test_error_details_from_rest_response():
    bad_request_detail = create_bad_request_details()
    error_info_detail = create_error_info_details()
    status = status_pb2.Status()
    status.code = 3
    status.message = (
        "3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set."
    )
    status.details.append(bad_request_detail)
    status.details.append(error_info_detail)

    # See JSON schema in https://cloud.google.com/apis/design/errors#http_mapping
    http_response = make_response(
        json.dumps(
            {"error": json.loads(json_format.MessageToJson(status, sort_keys=True))}
        ).encode("utf-8")
    )
    exception = exceptions.from_http_response(http_response)
    want_error_details = [
        json.loads(json_format.MessageToJson(bad_request_detail)),
        json.loads(json_format.MessageToJson(error_info_detail)),
    ]
    assert want_error_details == exception.details

    # 404 POST comes from make_response.
    assert str(exception) == (
        "404 POST https://example.com/: 3 INVALID_ARGUMENT:"
        " One of content, or gcs_content_uri must be set."
        " [{'@type': 'type.googleapis.com/google.rpc.BadRequest',"
        " 'fieldViolations': [{'description': 'Must have some text content to annotate.',"
        " 'field': 'document.content'}]},"
        " {'@type': 'type.googleapis.com/google.rpc.ErrorInfo',"
        " 'domain': 'googleapis.com',"
        " 'metadata': {'consumer': 'projects/455411330361',"
        " 'service': 'translate.googleapis.com'},"
        " 'reason': 'SERVICE_DISABLED'}]"
    )


def test_error_details_from_v1_rest_response():
    response = make_response(
        json.dumps(
            {"error": {"message": "\u2019 message", "errors": ["1", "2"]}}
        ).encode("utf-8")
    )
    exception = exceptions.from_http_response(response)
    assert exception.details == []
    assert (
        exception.reason is None
        and exception.domain is None
        and exception.metadata is None
    )


@pytest.mark.skipif(grpc is None, reason="gRPC not importable")
def test_error_details_from_grpc_response():
    status = rpc_status.status_pb2.Status()
    status.code = 3
    status.message = (
        "3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set."
    )
    status_br_detail = create_bad_request_details()
    status_ei_detail = create_error_info_details()
    status.details.append(status_br_detail)
    status.details.append(status_ei_detail)

    # Actualy error doesn't matter as long as its grpc.Call,
    # because from_call is mocked.
    error = mock.create_autospec(grpc.Call, instance=True)
    with mock.patch("grpc_status.rpc_status.from_call") as m:
        m.return_value = status
        exception = exceptions.from_grpc_error(error)

    bad_request_detail = error_details_pb2.BadRequest()
    error_info_detail = error_details_pb2.ErrorInfo()
    status_br_detail.Unpack(bad_request_detail)
    status_ei_detail.Unpack(error_info_detail)
    assert exception.details == [bad_request_detail, error_info_detail]
    assert exception.reason == error_info_detail.reason
    assert exception.domain == error_info_detail.domain
    assert exception.metadata == error_info_detail.metadata


@pytest.mark.skipif(grpc is None, reason="gRPC not importable")
def test_error_details_from_grpc_response_unknown_error():
    status_detail = any_pb2.Any()

    status = rpc_status.status_pb2.Status()
    status.code = 3
    status.message = (
        "3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set."
    )
    status.details.append(status_detail)

    error = mock.create_autospec(grpc.Call, instance=True)
    with mock.patch("grpc_status.rpc_status.from_call") as m:
        m.return_value = status
        exception = exceptions.from_grpc_error(error)
    assert exception.details == [status_detail]
    assert (
        exception.reason is None
        and exception.domain is None
        and exception.metadata is None
    )
