import pytest

from funkwhale_api.users.oauth import permissions, scopes


@pytest.mark.parametrize(
    "required_scope, request_scopes, expected",
    [
        (None, {}, True),
        ("write:profile", {"write"}, True),
        ("write:profile", {"read"}, False),
        ("write:profile", {"read:profile"}, False),
        ("write:profile", {"write:profile"}, True),
        ("read:profile", {"read"}, True),
        ("read:profile", {"write"}, False),
        ("read:profile", {"read:profile"}, True),
        ("read:profile", {"write:profile"}, False),
        ("write:profile", {"write"}, True),
        ("write:profile", {"read:profile"}, False),
        ("write:profile", {"write:profile"}, True),
        ("write:profile", {"write"}, True),
        ("write:profile", {"read:profile"}, False),
        ("write:profile", {"write:profile"}, True),
        ("write:profile", {"write"}, True),
        ("write:profile", {"read:profile"}, False),
        ("write:profile", {"write:profile"}, True),
    ],
)
def test_should_allow(required_scope, request_scopes, expected):
    assert (
        permissions.should_allow(
            required_scope=required_scope, request_scopes=request_scopes
        )
        is expected
    )


@pytest.mark.parametrize("method", ["OPTIONS", "HEAD"])
def test_scope_permission_safe_methods(method, mocker, factories):
    view = mocker.Mock(required_scope="write:profile", anonymous_policy=False)
    request = mocker.Mock(method=method)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) is True


@pytest.mark.parametrize(
    "policy, preference, expected",
    [
        (True, False, True),
        (False, False, False),
        ("setting", True, False),
        ("setting", False, True),
    ],
)
def test_scope_permission_anonymous_policy(
    policy, preference, expected, preferences, mocker, anonymous_user
):
    preferences["common__api_authentication_required"] = preference
    view = mocker.Mock(
        required_scope="libraries", anonymous_policy=policy, anonymous_scopes=set()
    )
    request = mocker.Mock(method="GET", user=anonymous_user, actor=None, scopes=None)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) is expected


def test_scope_permission_dict_no_required(mocker, anonymous_user):
    view = mocker.Mock(
        required_scope={"read": None, "write": "write:profile"},
        anonymous_policy=True,
        action="read",
        anonymous_scopes=set(),
    )
    request = mocker.Mock(method="GET", user=anonymous_user, actor=None, scopes=None)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) is True


@pytest.mark.parametrize(
    "required_scope, method, action, expected_scope",
    [
        ("profile", "GET", "read", "read:profile"),
        ("profile", "POST", "write", "write:profile"),
        ({"read": "read:profile"}, "GET", "read", "read:profile"),
        ({"write": "write:profile"}, "POST", "write", "write:profile"),
    ],
)
def test_scope_permission_user(
    required_scope, method, action, expected_scope, mocker, factories
):
    user = factories["users.User"]()
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method=method, user=user, actor=None, scopes=None)
    view = mocker.Mock(
        required_scope=required_scope, anonymous_policy=False, action=action
    )

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) == should_allow.return_value

    should_allow.assert_called_once_with(
        required_scope=expected_scope,
        request_scopes=scopes.get_from_permissions(**user.get_permissions()),
    )


def test_scope_permission_token(mocker, factories):
    token = factories["users.AccessToken"](
        scope="write:profile read:playlists",
        application__scope="write:profile read:playlists",
    )
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", auth=token)
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) == should_allow.return_value

    should_allow.assert_called_once_with(
        required_scope="write:profile",
        request_scopes={"write:profile", "read:playlists"},
    )


def test_scope_permission_request_scopes(mocker, factories):
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", scopes=["write:profile", "read:playlists"])
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)
    p = permissions.ScopePermission()

    assert p.has_permission(request, view) == should_allow.return_value

    should_allow.assert_called_once_with(
        required_scope="write:profile",
        request_scopes={"write:profile", "read:playlists"},
    )


def test_scope_permission_actor(mocker, factories, anonymous_user):
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(
        method="POST",
        actor=factories["federation.Actor"](),
        user=anonymous_user,
        scopes=None,
    )
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)
    p = permissions.ScopePermission()

    assert p.has_permission(request, view) == should_allow.return_value

    should_allow.assert_called_once_with(
        required_scope="write:profile", request_scopes=scopes.FEDERATION_REQUEST_SCOPES
    )


def test_scope_permission_token_anonymous_user_auth_required(
    mocker, factories, anonymous_user, preferences
):
    preferences["common__api_authentication_required"] = True
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", user=anonymous_user, actor=None, scopes=None)
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) is False

    should_allow.assert_not_called()


def test_scope_permission_token_anonymous_user_auth_not_required(
    mocker, factories, anonymous_user, preferences
):
    preferences["common__api_authentication_required"] = False
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", user=anonymous_user, actor=None, scopes=None)
    view = mocker.Mock(
        required_scope="profile", anonymous_policy="setting", anonymous_scopes=set()
    )

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) == should_allow.return_value

    should_allow.assert_called_once_with(
        required_scope="write:profile", request_scopes=scopes.ANONYMOUS_SCOPES
    )


def test_scope_permission_token_expired(mocker, factories, now):
    token = factories["users.AccessToken"](
        scope="profile:write playlists:read", expires=now
    )
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", auth=token)
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) is False

    should_allow.assert_not_called()


def test_scope_permission_token_no_user(mocker, factories, now):
    token = factories["users.AccessToken"](
        scope="profile:write playlists:read", user=None
    )
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", auth=token)
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) is False

    should_allow.assert_not_called()


def test_scope_permission_token_honor_app_scopes(mocker, factories, now):
    # token contains read access, but app scope only allows profile:write
    token = factories["users.AccessToken"](
        scope="write:profile read", application__scope="write:profile"
    )
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", auth=token)
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)

    p = permissions.ScopePermission()

    assert p.has_permission(request, view) == should_allow.return_value

    should_allow.assert_called_once_with(
        required_scope="write:profile", request_scopes={"write:profile"}
    )


def test_scope_permission_token_honor_allowed_app_scopes(mocker, factories, now):
    mocker.patch.object(scopes, "OAUTH_APP_SCOPES", {"read:profile"})
    token = factories["users.AccessToken"](
        scope="write:profile read:profile read",
        application__scope="write:profile read:profile read",
    )
    should_allow = mocker.patch.object(permissions, "should_allow")
    request = mocker.Mock(method="POST", auth=token)
    view = mocker.Mock(required_scope="profile", anonymous_policy=False)
    p = permissions.ScopePermission()

    assert p.has_permission(request, view) == should_allow.return_value

    should_allow.assert_called_once_with(
        required_scope="write:profile", request_scopes={"read:profile"}
    )
