import datetime

import pytest
from django.urls import reverse

from funkwhale_api.federation import api_serializers, serializers, tasks, views


def test_user_can_list_their_library_follows(factories, logged_in_api_client):
    # followed by someont else
    factories["federation.LibraryFollow"]()
    follow = factories["federation.LibraryFollow"](
        actor__user=logged_in_api_client.user
    )
    url = reverse("api:v1:federation:library-follows-list")
    response = logged_in_api_client.get(url)

    assert response.data["count"] == 1
    assert response.data["results"][0]["uuid"] == str(follow.uuid)


def test_user_can_fetch_library_using_url(mocker, factories, logged_in_api_client):
    library = factories["music.Library"]()
    mocked_retrieve = mocker.patch(
        "funkwhale_api.federation.utils.retrieve_ap_object", return_value=library
    )
    url = reverse("api:v1:federation:libraries-fetch")
    response = logged_in_api_client.post(url, {"fid": library.fid})
    assert mocked_retrieve.call_count == 1
    args = mocked_retrieve.call_args
    assert args[0] == (library.fid,)
    assert args[1]["queryset"].model == views.MusicLibraryViewSet.queryset.model
    assert args[1]["serializer_class"] == serializers.LibrarySerializer
    assert response.status_code == 200
    assert response.data["results"] == [api_serializers.LibrarySerializer(library).data]


def test_user_can_schedule_library_scan(mocker, factories, logged_in_api_client):
    actor = logged_in_api_client.user.create_actor()
    library = factories["music.Library"](privacy_level="everyone")

    schedule_scan = mocker.patch(
        "funkwhale_api.music.models.Library.schedule_scan", return_value=True
    )
    url = reverse("api:v1:federation:libraries-scan", kwargs={"uuid": library.uuid})

    response = logged_in_api_client.post(url)

    assert response.status_code == 200

    schedule_scan.assert_called_once_with(actor=actor)


def test_can_follow_library(factories, logged_in_api_client, mocker):
    dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
    actor = logged_in_api_client.user.create_actor()
    library = factories["music.Library"]()
    url = reverse("api:v1:federation:library-follows-list")
    response = logged_in_api_client.post(url, {"target": library.uuid})

    assert response.status_code == 201

    follow = library.received_follows.latest("id")

    assert follow.approved is None
    assert follow.actor == actor

    dispatch.assert_called_once_with({"type": "Follow"}, context={"follow": follow})


def test_can_undo_library_follow(factories, logged_in_api_client, mocker):
    dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
    actor = logged_in_api_client.user.create_actor()
    follow = factories["federation.LibraryFollow"](actor=actor)
    delete = mocker.patch.object(follow.__class__, "delete")
    url = reverse(
        "api:v1:federation:library-follows-detail", kwargs={"uuid": follow.uuid}
    )
    response = logged_in_api_client.delete(url)

    assert response.status_code == 204

    delete.assert_called_once_with()
    dispatch.assert_called_once_with(
        {"type": "Undo", "object": {"type": "Follow"}}, context={"follow": follow}
    )


@pytest.mark.parametrize("action", ["accept", "reject"])
def test_user_cannot_edit_someone_else_library_follow(
    factories, logged_in_api_client, action
):
    logged_in_api_client.user.create_actor()
    follow = factories["federation.LibraryFollow"]()
    url = reverse(
        f"api:v1:federation:library-follows-{action}",
        kwargs={"uuid": follow.uuid},
    )
    response = logged_in_api_client.post(url)

    assert response.status_code == 404


@pytest.mark.parametrize("action,expected", [("accept", True), ("reject", False)])
def test_user_can_accept_or_reject_own_follows(
    factories, logged_in_api_client, action, expected, mocker
):
    mocked_dispatch = mocker.patch(
        "funkwhale_api.federation.activity.OutboxRouter.dispatch"
    )
    actor = logged_in_api_client.user.create_actor()
    follow = factories["federation.LibraryFollow"](target__actor=actor)
    url = reverse(
        f"api:v1:federation:library-follows-{action}",
        kwargs={"uuid": follow.uuid},
    )
    response = logged_in_api_client.post(url)

    assert response.status_code == 204

    follow.refresh_from_db()

    assert follow.approved is expected

    mocked_dispatch.assert_called_once_with(
        {"type": action.title()}, context={"follow": follow}
    )


def test_user_can_list_inbox_items(factories, logged_in_api_client):
    actor = logged_in_api_client.user.create_actor()
    ii = factories["federation.InboxItem"](
        activity__type="Follow", actor=actor, type="to"
    )

    factories["federation.InboxItem"](activity__type="Follow", actor=actor, type="cc")
    factories["federation.InboxItem"](activity__type="Follow", type="to")

    url = reverse("api:v1:federation:inbox-list")

    response = logged_in_api_client.get(url)

    assert response.status_code == 200
    assert response.data == {
        "count": 1,
        "results": [api_serializers.InboxItemSerializer(ii).data],
        "next": None,
        "previous": None,
    }


def test_user_can_update_read_status_of_inbox_item(factories, logged_in_api_client):
    actor = logged_in_api_client.user.create_actor()
    ii = factories["federation.InboxItem"](
        activity__type="Follow", actor=actor, type="to"
    )

    url = reverse("api:v1:federation:inbox-detail", kwargs={"pk": ii.pk})

    response = logged_in_api_client.patch(url, {"is_read": True})
    assert response.status_code == 200

    ii.refresh_from_db()

    assert ii.is_read is True


def test_can_detail_fetch(logged_in_api_client, factories):
    actor = logged_in_api_client.user.create_actor()
    fetch = factories["federation.Fetch"](url="http://test.object", actor=actor)
    url = reverse("api:v1:federation:fetches-detail", kwargs={"pk": fetch.pk})

    response = logged_in_api_client.get(url)

    expected = api_serializers.FetchSerializer(fetch).data

    assert response.status_code == 200
    assert response.data == expected


def test_user_can_list_domains(factories, api_client, preferences):
    preferences["common__api_authentication_required"] = False
    allowed = factories["federation.Domain"]()
    factories["moderation.InstancePolicy"](
        actor=None, for_domain=True, block_all=True
    ).target_domain
    url = reverse("api:v1:federation:domains-list")
    response = api_client.get(url)

    expected = {
        "count": 1,
        "next": None,
        "previous": None,
        "results": [api_serializers.DomainSerializer(allowed).data],
    }
    assert response.data == expected


def test_can_retrieve_actor(factories, api_client, preferences):
    preferences["common__api_authentication_required"] = False
    actor = factories["federation.Actor"]()
    url = reverse(
        "api:v1:federation:actors-detail", kwargs={"full_username": actor.full_username}
    )
    response = api_client.get(url)

    expected = api_serializers.FullActorSerializer(actor).data
    assert response.data == expected


def test_can_retrieve_local_actor_with_allow_list_enabled(
    factories, api_client, preferences
):
    preferences["common__api_authentication_required"] = False
    preferences["moderation__allow_list_enabled"] = True
    actor = factories["federation.Actor"](local=True)
    url = reverse(
        "api:v1:federation:actors-detail", kwargs={"full_username": actor.full_username}
    )
    response = api_client.get(url)

    expected = api_serializers.FullActorSerializer(actor).data
    assert response.data == expected


@pytest.mark.parametrize(
    "object_id, expected_url",
    [
        ("https://fetch.url", "https://fetch.url"),
        ("name@domain.tld", "webfinger://name@domain.tld"),
        ("@name@domain.tld", "webfinger://name@domain.tld"),
    ],
)
def test_can_fetch_using_url_synchronous(
    object_id, expected_url, factories, logged_in_api_client, mocker, settings
):
    settings.FEDERATION_SYNCHRONOUS_FETCH = True
    actor = logged_in_api_client.user.create_actor()

    def fake_task(fetch_id):
        actor.fetches.filter(id=fetch_id).update(status="finished")

    fetch_task = mocker.patch.object(tasks, "fetch", side_effect=fake_task)

    url = reverse("api:v1:federation:fetches-list")
    data = {"object": object_id}
    response = logged_in_api_client.post(url, data)
    assert response.status_code == 201

    fetch = actor.fetches.latest("id")

    assert fetch.status == "finished"
    assert fetch.url == expected_url
    assert response.data == api_serializers.FetchSerializer(fetch).data
    fetch_task.assert_called_once_with(fetch_id=fetch.pk)


def test_fetch_duplicate(factories, logged_in_api_client, settings, now):
    object_id = "http://example.test"
    settings.FEDERATION_DUPLICATE_FETCH_DELAY = 60
    actor = logged_in_api_client.user.create_actor()
    duplicate = factories["federation.Fetch"](
        actor=actor,
        status="finished",
        url=object_id,
        creation_date=now - datetime.timedelta(seconds=59),
    )
    url = reverse("api:v1:federation:fetches-list")
    data = {"object": object_id}
    response = logged_in_api_client.post(url, data)
    assert response.status_code == 201
    assert response.data == api_serializers.FetchSerializer(duplicate).data


def test_fetch_duplicate_bypass_with_force(
    factories, logged_in_api_client, mocker, settings, now
):
    fetch_task = mocker.patch.object(tasks, "fetch")
    object_id = "http://example.test"
    settings.FEDERATION_DUPLICATE_FETCH_DELAY = 60
    actor = logged_in_api_client.user.create_actor()
    duplicate = factories["federation.Fetch"](
        actor=actor,
        status="finished",
        url=object_id,
        creation_date=now - datetime.timedelta(seconds=59),
    )
    url = reverse("api:v1:federation:fetches-list")
    data = {"object": object_id, "force": True}
    response = logged_in_api_client.post(url, data)

    fetch = actor.fetches.latest("id")
    assert fetch != duplicate
    assert response.status_code == 201
    assert response.data == api_serializers.FetchSerializer(fetch).data
    fetch_task.assert_called_once_with(fetch_id=fetch.pk)


def test_library_follow_get_all(factories, logged_in_api_client):
    actor = logged_in_api_client.user.create_actor()
    library = factories["music.Library"]()
    follow = factories["federation.LibraryFollow"](target=library, actor=actor)
    factories["federation.LibraryFollow"]()
    factories["music.Library"]()
    url = reverse("api:v1:federation:library-follows-all")
    response = logged_in_api_client.get(url)

    assert response.status_code == 200
    assert response.data == {
        "results": [
            {
                "uuid": str(follow.uuid),
                "library": str(library.uuid),
                "approved": follow.approved,
            }
        ],
        "count": 1,
    }


def test_user_follow_get_all(factories, logged_in_api_client):
    actor = logged_in_api_client.user.create_actor()
    target_actor = factories["federation.Actor"]()
    follow = factories["federation.Follow"](target=target_actor, actor=actor)
    factories["federation.Follow"]()
    url = reverse("api:v1:federation:user-follows-all")
    response = logged_in_api_client.get(url)

    assert response.status_code == 200
    assert response.data == {
        "results": [
            {
                "uuid": str(follow.uuid),
                "actor": str(target_actor.fid),
                "approved": follow.approved,
            }
        ],
        "count": 1,
    }


def test_user_follow_retrieve(factories, logged_in_api_client):
    actor = logged_in_api_client.user.create_actor()
    target_actor = factories["federation.Actor"]()
    follow = factories["federation.Follow"](target=target_actor, actor=actor)
    factories["federation.Follow"]()
    url = reverse("api:v1:federation:user-follows-detail", kwargs={"uuid": follow.uuid})
    response = logged_in_api_client.get(url)

    assert response.status_code == 200


def test_user_can_list_their_received_follows(factories, logged_in_api_client):
    # followed by someont else
    factories["federation.Follow"]()
    follow = factories["federation.Follow"](actor__user=logged_in_api_client.user)
    url = reverse("api:v1:federation:user-follows-list")
    response = logged_in_api_client.get(url)

    assert response.data["count"] == 1
    assert response.data["results"][0]["uuid"] == str(follow.uuid)


def test_can_follow_user_actor(factories, logged_in_api_client, mocker):
    dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
    actor = logged_in_api_client.user.create_actor()
    target_actor = factories["federation.Actor"]()
    url = reverse("api:v1:federation:user-follows-list")
    response = logged_in_api_client.post(url, {"target": target_actor.fid})

    assert response.status_code == 201

    follow = target_actor.received_follows.latest("id")

    assert follow.approved is None
    assert follow.actor == actor

    dispatch.assert_called_once_with({"type": "Follow"}, context={"follow": follow})


def test_can_undo_user_follow(factories, logged_in_api_client, mocker):
    dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
    actor = logged_in_api_client.user.create_actor()
    follow = factories["federation.Follow"](actor=actor)
    delete = mocker.patch.object(follow.__class__, "delete")
    url = reverse("api:v1:federation:user-follows-detail", kwargs={"uuid": follow.uuid})
    response = logged_in_api_client.delete(url)

    assert response.status_code == 204

    delete.assert_called_once_with()
    dispatch.assert_called_once_with(
        {"type": "Undo", "object": {"type": "Follow"}}, context={"follow": follow}
    )


@pytest.mark.parametrize("action", ["accept", "reject"])
def test_user_cannot_edit_someone_else_user_follow(
    factories, logged_in_api_client, action
):
    logged_in_api_client.user.create_actor()
    follow = factories["federation.Follow"]()
    url = reverse(
        f"api:v1:federation:user-follows-{action}",
        kwargs={"uuid": follow.uuid},
    )
    response = logged_in_api_client.post(url)

    assert response.status_code == 404


@pytest.mark.parametrize("action,expected", [("accept", True), ("reject", False)])
def test_user_can_accept_or_reject_own_received_follows(
    factories, logged_in_api_client, action, expected, mocker
):
    mocked_dispatch = mocker.patch(
        "funkwhale_api.federation.activity.OutboxRouter.dispatch"
    )
    actor = logged_in_api_client.user.create_actor()
    follow = factories["federation.Follow"](target=actor)
    url = reverse(
        f"api:v1:federation:user-follows-{action}",
        kwargs={"uuid": follow.uuid},
    )
    response = logged_in_api_client.post(url)

    assert response.status_code == 204

    follow.refresh_from_db()

    assert follow.approved is expected

    mocked_dispatch.assert_called_once_with(
        {"type": action.title()}, context={"follow": follow}
    )
