import json
from contextlib import suppress
from unittest.mock import patch

import pytest
from django.utils import timezone
from slack_sdk.web import SlackResponse

from apps.slack.client import SlackClient, server_error_retry_handler
from apps.slack.errors import (
    SlackAPICannotDMBotError,
    SlackAPICantUpdateMessageError,
    SlackAPIChannelArchivedError,
    SlackAPIChannelInactiveError,
    SlackAPIChannelNotFoundError,
    SlackAPIError,
    SlackAPIFetchMembersFailedError,
    SlackAPIInvalidAuthError,
    SlackAPIMessageNotFoundError,
    SlackAPIMethodNotSupportedForChannelTypeError,
    SlackAPIPermissionDeniedError,
    SlackAPIPlanUpgradeRequiredError,
    SlackAPIRatelimitError,
    SlackAPIRestrictedActionError,
    SlackAPIServerError,
    SlackAPITokenError,
    SlackAPIUsergroupNotFoundError,
    SlackAPIUsergroupPaidTeamOnlyError,
    SlackAPIUserNotFoundError,
    SlackAPIViewNotFoundError,
)


@pytest.mark.django_db
@patch(
    "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal",
    return_value={"status": 200, "body": '{"ok": true}', "headers": {}},
)
def test_slack_client_ok(mock_request, monkeypatch, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity)
    client.api_call("auth.test")

    mock_request.assert_called_once()


@pytest.mark.parametrize("status", [500, 503, 504])
@patch.object(
    server_error_retry_handler.interval_calculator,
    "calculate_sleep_duration",
    return_value=0,  # speed up the retries
)
@pytest.mark.django_db
def test_slack_client_unexpected_response(_, monkeypatch, status, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity)

    return_value = {"status": status, "body": "non-json", "headers": {}}
    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal", return_value=return_value
    ) as mock_request:
        with pytest.raises(SlackAPIServerError) as exc_info:
            client.api_call("auth.test")
            assert type(exc_info.value.response) is dict

    assert mock_request.call_count == server_error_retry_handler.max_retry_count + 1


@pytest.mark.parametrize("error", ["internal_error", "fatal_error"])
@patch.object(
    server_error_retry_handler.interval_calculator,
    "calculate_sleep_duration",
    return_value=0,  # speed up the retries
)
@pytest.mark.django_db
def test_slack_client_slack_server_error(_, monkeypatch, error, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity)

    return_value = {"status": 200, "body": json.dumps({"ok": False, "error": error}), "headers": {}}
    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal", return_value=return_value
    ) as mock_request:
        with pytest.raises(SlackAPIServerError) as exc_info:
            client.api_call("auth.test")
            assert type(exc_info.value.response) is dict

    assert mock_request.call_count == server_error_retry_handler.max_retry_count + 1


@pytest.mark.django_db
@patch(
    "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal",
    return_value={"status": 200, "body": '{"ok": false, "error": "random_error_123"}', "headers": {}},
)
def test_slack_client_generic_error(mock_request, monkeypatch, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity)

    with pytest.raises(SlackAPIError) as exc_info:
        client.api_call("auth.test")
        assert type(exc_info.value) is SlackAPIError
        assert type(exc_info.value.response) is SlackResponse

    mock_request.assert_called_once()


@pytest.mark.parametrize(
    "error,error_class",
    [
        ("account_inactive", SlackAPITokenError),
        ("cannot_dm_bot", SlackAPICannotDMBotError),
        ("cant_update_message", SlackAPICantUpdateMessageError),
        ("channel_not_found", SlackAPIChannelNotFoundError),
        ("fatal_error", SlackAPIServerError),
        ("fetch_members_failed", SlackAPIFetchMembersFailedError),
        ("internal_error", SlackAPIServerError),
        ("invalid_auth", SlackAPIInvalidAuthError),
        ("is_archived", SlackAPIChannelArchivedError),
        ("is_inactive", SlackAPIChannelInactiveError),
        ("message_limit_exceeded", SlackAPIRatelimitError),
        ("message_not_found", SlackAPIMessageNotFoundError),
        ("method_not_supported_for_channel_type", SlackAPIMethodNotSupportedForChannelTypeError),
        ("no_such_subteam", SlackAPIUsergroupNotFoundError),
        ("paid_team_only", SlackAPIUsergroupPaidTeamOnlyError),
        ("not_found", SlackAPIViewNotFoundError),
        ("permission_denied", SlackAPIPermissionDeniedError),
        ("plan_upgrade_required", SlackAPIPlanUpgradeRequiredError),
        ("rate_limited", SlackAPIRatelimitError),
        ("ratelimited", SlackAPIRatelimitError),
        ("restricted_action", SlackAPIRestrictedActionError),
        ("token_revoked", SlackAPITokenError),
        ("user_not_found", SlackAPIUserNotFoundError),
    ],
)
@patch.object(
    server_error_retry_handler.interval_calculator,
    "calculate_sleep_duration",
    return_value=0,  # speed up the retries if any
)
@pytest.mark.django_db
def test_slack_client_specific_error(_, error, error_class, monkeypatch, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity)

    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal",
        return_value={"status": 200, "body": json.dumps({"ok": False, "error": error}), "headers": {}},
    ):
        with pytest.raises(SlackAPIError) as exc_info:
            client.api_call("auth.test")
            assert type(exc_info.value) is error_class
            assert type(exc_info.value.response) is SlackResponse


@pytest.mark.parametrize("error", ["ratelimited", "rate_limited", "message_limit_exceeded"])
@pytest.mark.django_db
def test_slack_client_ratelimit(monkeypatch, error, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity)

    return_value = {"status": 429, "body": json.dumps({"ok": False, "error": error}), "headers": {"Retry-After": "1"}}
    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal", return_value=return_value
    ) as mock_request:
        with pytest.raises(SlackAPIRatelimitError):
            client.api_call("auth.test")

    # no slack built-in retries by default
    assert len(mock_request.mock_calls) == 1


@pytest.mark.parametrize("error", ["ratelimited", "rate_limited", "message_limit_exceeded"])
@pytest.mark.django_db
def test_slack_client_ratelimit_enable_retry(monkeypatch, error, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity, enable_ratelimit_retry=True)

    return_value = {"status": 429, "body": json.dumps({"ok": False, "error": error}), "headers": {"Retry-After": "1"}}
    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal", return_value=return_value
    ) as mock_request:
        with pytest.raises(SlackAPIRatelimitError) as exc_info:
            client.api_call("auth.test")

    assert len(mock_request.mock_calls) == 2
    assert exc_info.value.retry_after == 1


@pytest.mark.parametrize("error", ["account_inactive", "token_revoked"])
@pytest.mark.django_db
def test_slack_client_mark_token_revoked(error, monkeypatch, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    _, slack_team_identity = make_organization_with_slack_team_identity()
    client = SlackClient(slack_team_identity)
    assert slack_team_identity.detected_token_revoked is None

    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal",
        return_value={"status": 200, "body": json.dumps({"ok": False, "error": error}), "headers": {}},
    ) as mock_request:
        with pytest.raises(SlackAPITokenError):
            client.api_call("auth.test")

    mock_request.assert_called_once()
    slack_team_identity.refresh_from_db()
    assert slack_team_identity.detected_token_revoked is not None


@pytest.mark.parametrize("error", ["account_inactive", "token_revoked"])
@pytest.mark.django_db
def test_slack_client_cant_unmark_token_revoked(error, monkeypatch, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    now = timezone.now()
    _, slack_team_identity = make_organization_with_slack_team_identity(detected_token_revoked=now)
    client = SlackClient(slack_team_identity)
    assert slack_team_identity.detected_token_revoked == now

    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal",
        return_value={"status": 200, "body": json.dumps({"ok": False, "error": error}), "headers": {}},
    ) as mock_request:
        with pytest.raises(SlackAPITokenError):
            client.api_call("auth.test")

    mock_request.assert_called_once()
    slack_team_identity.refresh_from_db()
    assert slack_team_identity.detected_token_revoked == now


@pytest.mark.parametrize("body", [{"ok": False, "error": "ratelimited"}, {"ok": True}])
@pytest.mark.django_db
def test_slack_client_unmark_token_revoked(body, monkeypatch, make_organization_with_slack_team_identity):
    monkeypatch.undo()  # undo engine.conftest.mock_slack_api_call

    now = timezone.now()
    _, slack_team_identity = make_organization_with_slack_team_identity(detected_token_revoked=now)
    client = SlackClient(slack_team_identity)
    assert slack_team_identity.detected_token_revoked == now

    with patch(
        "slack_sdk.web.base_client.BaseClient._perform_urllib_http_request_internal",
        return_value={"status": 200, "body": json.dumps(body), "headers": {}},
    ) as mock_request:
        with suppress(SlackAPIError):
            client.api_call("auth.test")

    mock_request.assert_called_once()
    slack_team_identity.refresh_from_db()
    assert slack_team_identity.detected_token_revoked is None
