import json
from unittest.mock import patch

import pytest
import requests
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from rest_framework.views import APIView

from apps.mobile_app.views import PROXY_REQUESTS_TIMEOUT, MobileAppGatewayView
from common.cloud_auth_api.client import CloudAuthApiClient, CloudAuthApiException

DOWNSTREAM_BACKEND = "incident"
MOCK_DOWNSTREAM_URL = "https://mockdownstream.com"
MOCK_DOWNSTREAM_INCIDENT_API_URL = "https://mockdownstreamincidentapi.com"
MOCK_DOWNSTREAM_HEADERS = {"Authorization": "Bearer mock_auth_token"}
MOCK_DOWNSTREAM_RESPONSE_DATA = {"foo": "bar"}

MOCK_AUTH_TOKEN = "mncn,zxcnv,mznxcv"


@pytest.fixture(autouse=True)
def enable_mobile_app_gateway(settings):
    settings.MOBILE_APP_GATEWAY_ENABLED = True
    settings.GRAFANA_CLOUD_AUTH_API_URL = "asdfasdf"
    settings.GRAFANA_CLOUD_AUTH_API_SYSTEM_TOKEN = "zxcvzx"


@pytest.fixture()
def make_organization_with_mobile_app_auth_token(
    make_organization_and_user_with_mobile_app_auth_token,
):
    def _make_organization_with_mobile_app_auth_token(*args, grafana_incident_enabled=True, **kwargs):
        org, user, auth_token = make_organization_and_user_with_mobile_app_auth_token(*args, **kwargs)
        org.is_grafana_incident_enabled = grafana_incident_enabled
        org.save()
        return org, user, auth_token

    return _make_organization_with_mobile_app_auth_token


class MockResponse:
    def __init__(self, status_code=status.HTTP_200_OK, data=MOCK_DOWNSTREAM_RESPONSE_DATA):
        self.status_code = status_code
        self.data = data

    def raise_for_status(self):
        if self.status_code >= status.HTTP_400_BAD_REQUEST:
            raise requests.exceptions.HTTPError()

    def json(self):
        return self.data


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
@pytest.mark.parametrize("path", ["", "thing", "thing/123", "thing/123/otherthing", "thing/123/otherthing/456"])
def test_mobile_app_gateway_properly_proxies_paths(
    _mock_get_downstream_headers,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
    path,
):
    mock_requests.post.return_value = MockResponse()

    org, _, auth_token = make_organization_with_mobile_app_auth_token()
    org.grafana_incident_backend_url = MOCK_DOWNSTREAM_INCIDENT_API_URL
    org.save()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": path})

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == status.HTTP_200_OK
    assert response.json() == MOCK_DOWNSTREAM_RESPONSE_DATA

    mock_requests.post.assert_called_once_with(
        f"{MOCK_DOWNSTREAM_INCIDENT_API_URL}/{path}",
        data=b"",
        params={},
        headers=MOCK_DOWNSTREAM_HEADERS,
        timeout=PROXY_REQUESTS_TIMEOUT,
    )


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
@pytest.mark.parametrize("method", APIView.http_method_names)
def test_mobile_app_gateway_supports_all_methods(
    _mock_get_downstream_headers,
    _mock_get_downstream_url,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
    method,
):
    mock_http_verb_method = getattr(mock_requests, method.lower())
    mock_http_verb_method.return_value = MockResponse()

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    response = client.generic(method.upper(), url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == status.HTTP_200_OK

    mock_http_verb_method.assert_called_once()


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
def test_mobile_app_gateway_proxies_query_params(
    _mock_get_downstream_headers,
    _mock_get_downstream_url,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
):
    mock_requests.post.return_value = MockResponse()

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    response = client.post(f"{url}?foo=bar&baz=hello", HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == status.HTTP_200_OK

    mock_requests.post.assert_called_once_with(
        MOCK_DOWNSTREAM_URL,
        data=b"",
        params={"foo": "bar", "baz": "hello"},
        headers=MOCK_DOWNSTREAM_HEADERS,
        timeout=PROXY_REQUESTS_TIMEOUT,
    )


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
@pytest.mark.parametrize(
    "upstream_request_body",
    [
        None,
        {},
        {"vegetable": "potato", "fruit": "apple"},
    ],
)
def test_mobile_app_gateway_properly_proxies_request_body(
    _mock_get_downstream_headers,
    _mock_get_downstream_url,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
    upstream_request_body,
):
    mock_requests.post.return_value = MockResponse()

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})
    data = json.dumps(upstream_request_body)

    response = client.post(
        url,
        data=data,
        content_type="application/json",
        HTTP_AUTHORIZATION=auth_token,
    )
    assert response.status_code == status.HTTP_200_OK

    mock_requests.post.assert_called_once_with(
        MOCK_DOWNSTREAM_URL,
        data=data.encode("utf-8"),
        params={},
        headers=MOCK_DOWNSTREAM_HEADERS,
        timeout=PROXY_REQUESTS_TIMEOUT,
    )


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
@pytest.mark.parametrize(
    "downstream_backend,expected_status",
    [
        ("incident", status.HTTP_200_OK),
        ("foo", status.HTTP_404_NOT_FOUND),
    ],
)
def test_mobile_app_gateway_supported_downstream_backends(
    _mock_get_downstream_headers,
    _mock_get_downstream_url,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
    downstream_backend,
    expected_status,
):
    mock_requests.post.return_value = MockResponse()

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse(
        "mobile_app:gateway", kwargs={"downstream_backend": downstream_backend, "downstream_path": "test/123"}
    )

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == expected_status


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests.post")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
@pytest.mark.parametrize(
    "ExceptionClass,exception_args,expected_status",
    [
        (requests.exceptions.ConnectionError, (), status.HTTP_502_BAD_GATEWAY),
        (requests.exceptions.HTTPError, (), status.HTTP_502_BAD_GATEWAY),
        (requests.exceptions.TooManyRedirects, (), status.HTTP_502_BAD_GATEWAY),
        (requests.exceptions.Timeout, (), status.HTTP_504_GATEWAY_TIMEOUT),
        (requests.exceptions.JSONDecodeError, ("", "", 5), status.HTTP_400_BAD_REQUEST),
        (CloudAuthApiException, (403, "http://example.com"), status.HTTP_502_BAD_GATEWAY),
    ],
)
def test_mobile_app_gateway_catches_errors_from_downstream_server(
    _mock_get_downstream_headers,
    _mock_get_downstream_url,
    mock_requests_post,
    make_organization_with_mobile_app_auth_token,
    ExceptionClass,
    exception_args,
    expected_status,
):
    def _raise_exception(*args, **kwargs):
        raise ExceptionClass(*exception_args)

    mock_requests_post.side_effect = _raise_exception

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == expected_status


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests.post")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
def test_mobile_app_gateway_catches_and_forwards_error_from_downstream_server(
    _mock_get_downstream_headers,
    _mock_get_downstream_url,
    mock_requests_post,
    make_organization_with_mobile_app_auth_token,
):
    mock_requests_post.return_value = MockResponse(status_code=status.HTTP_400_BAD_REQUEST, data={"error": "foo"})

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    # forward status and response from downstream
    assert response.status_code == status.HTTP_400_BAD_REQUEST
    assert response.json() == {"error": "foo"}


@pytest.mark.django_db
def test_mobile_app_gateway_not_found_if_incident_disabled(
    make_organization_with_mobile_app_auth_token,
):
    _, _, auth_token = make_organization_with_mobile_app_auth_token(grafana_incident_enabled=False)

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == status.HTTP_404_NOT_FOUND


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
def test_mobile_app_gateway_mobile_app_auth_token(
    _mock_get_downstream_headers,
    _mock_get_downstream_url,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
):
    mock_requests.post.return_value = MockResponse()

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    response = client.post(url, HTTP_AUTHORIZATION="potato")
    assert response.status_code == status.HTTP_403_FORBIDDEN

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == status.HTTP_200_OK


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_headers", return_value=MOCK_DOWNSTREAM_HEADERS)
def test_mobile_app_gateway_incident_api_url(
    _mock_get_downstream_headers,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
):
    mock_incident_backend_url = "https://mockincidentbackend.com"
    mock_requests.post.return_value = MockResponse()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    # Organization has no incident backend URL saved
    organization, _, auth_token = make_organization_with_mobile_app_auth_token()
    assert organization.grafana_incident_backend_url is None

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == status.HTTP_400_BAD_REQUEST

    # Organization already has incident backend URL saved
    organization, _, auth_token = make_organization_with_mobile_app_auth_token()
    organization.grafana_incident_backend_url = mock_incident_backend_url
    organization.save()

    response = client.post(url, HTTP_AUTHORIZATION=auth_token)
    assert response.status_code == status.HTTP_200_OK


@pytest.mark.django_db
@patch("apps.mobile_app.views.requests")
@patch("apps.mobile_app.views.MobileAppGatewayView._get_auth_token", return_value=MOCK_AUTH_TOKEN)
@patch("apps.mobile_app.views.MobileAppGatewayView._get_downstream_url", return_value=MOCK_DOWNSTREAM_URL)
def test_mobile_app_gateway_proxies_headers(
    _mock_get_downstream_url,
    _mock_get_auth_token,
    mock_requests,
    make_organization_with_mobile_app_auth_token,
):
    mock_requests.post.return_value = MockResponse()

    _, _, auth_token = make_organization_with_mobile_app_auth_token()

    client = APIClient()
    url = reverse("mobile_app:gateway", kwargs={"downstream_backend": DOWNSTREAM_BACKEND, "downstream_path": "test"})

    content_type_header = "foo/bar"
    response = client.post(url, HTTP_AUTHORIZATION=auth_token, headers={"Content-Type": content_type_header})
    assert response.status_code == status.HTTP_200_OK

    mock_requests.post.assert_called_once_with(
        MOCK_DOWNSTREAM_URL,
        data=b"",
        params={},
        headers={
            "Authorization": f"Bearer {MOCK_AUTH_TOKEN}",
            "Content-Type": content_type_header,
        },
        timeout=PROXY_REQUESTS_TIMEOUT,
    )


@pytest.mark.django_db
@patch("apps.mobile_app.views.CloudAuthApiClient.request_signed_token", return_value=MOCK_AUTH_TOKEN)
def test_mobile_app_gateway_properly_generates_an_auth_token(
    mock_request_signed_token,
    make_organization,
    make_user_for_organization,
):
    stack_id = 895
    organization = make_organization(stack_id=stack_id)
    user = make_user_for_organization(organization)

    auth_token = MobileAppGatewayView._get_auth_token(DOWNSTREAM_BACKEND, user)

    assert auth_token == f"{stack_id}:{MOCK_AUTH_TOKEN}"
    mock_request_signed_token.assert_called_once_with(user, [CloudAuthApiClient.Scopes.INCIDENT_WRITE])
