import io
import uuid

import pytest
from django.core.paginator import Paginator
from django.urls import reverse
from django.utils import timezone

from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import (
    actors,
    contexts,
    jsonld,
    keys,
    models,
    serializers,
    utils,
)
from funkwhale_api.moderation import serializers as moderation_serializers
from funkwhale_api.music import licenses


def test_actor_serializer_from_ap(db):
    private, public = keys.get_key_pair()
    actor_url = "https://test.federation/actor"
    payload = {
        "@context": jsonld.get_default_context(),
        "id": actor_url,
        "type": "Person",
        "outbox": "https://test.com/outbox",
        "inbox": "https://test.com/inbox",
        "following": "https://test.com/following",
        "followers": "https://test.com/followers",
        "preferredUsername": "test",
        "name": "Test",
        "summary": "Hello world",
        "manuallyApprovesFollowers": True,
        "url": "http://hello.world/path",
        "publicKey": {
            "publicKeyPem": public.decode("utf-8"),
            "owner": actor_url,
            "id": actor_url + "#main-key",
        },
        "endpoints": {"sharedInbox": "https://noop.url/federation/shared/inbox"},
        "icon": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": "https://image.example/image.png",
        },
    }

    serializer = serializers.ActorSerializer(data=payload)
    assert serializer.is_valid(raise_exception=True)
    actor = serializer.save()

    assert actor.fid == actor_url
    assert actor.url == payload["url"]
    assert actor.inbox_url == payload["inbox"]
    assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"]
    assert actor.outbox_url == payload["outbox"]
    assert actor.following_url == payload["following"]
    assert actor.followers_url == payload["followers"]
    assert actor.followers_url == payload["followers"]
    assert actor.type == "Person"
    assert actor.preferred_username == payload["preferredUsername"]
    assert actor.name == payload["name"]
    assert actor.summary_obj.text == payload["summary"]
    assert actor.summary_obj.content_type == "text/html"
    assert actor.fid == actor_url
    assert actor.manually_approves_followers is True
    assert actor.private_key is None
    assert actor.public_key == payload["publicKey"]["publicKeyPem"]
    assert actor.domain_id == "test.federation"
    assert actor.attachment_icon.url == payload["icon"]["url"]
    assert actor.attachment_icon.mimetype == payload["icon"]["mediaType"]


def test_actor_serializer_from_ap_no_icon_mediaType(db):
    private, public = keys.get_key_pair()
    actor_url = "https://test.federation/actor"
    payload = {
        "@context": jsonld.get_default_context(),
        "id": actor_url,
        "type": "Person",
        "inbox": "https://test.com/inbox",
        "following": "https://test.com/following",
        "followers": "https://test.com/followers",
        "preferredUsername": "test",
        "manuallyApprovesFollowers": True,
        "url": "http://hello.world/path",
        "publicKey": {
            "publicKeyPem": public.decode("utf-8"),
            "owner": actor_url,
            "id": actor_url + "#main-key",
        },
        "endpoints": {"sharedInbox": "https://noop.url/federation/shared/inbox"},
        "icon": {"type": "Image", "url": "https://image.example/image.png"},
    }

    serializer = serializers.ActorSerializer(data=payload)
    assert serializer.is_valid(raise_exception=True)
    actor = serializer.save()

    assert actor.attachment_icon.url == payload["icon"]["url"]
    assert actor.attachment_icon.mimetype is None


def test_actor_serializer_only_mandatory_field_from_ap(db):
    payload = {
        "@context": jsonld.get_default_context(),
        "id": "https://test.federation/user",
        "type": "Person",
        "following": "https://test.federation/user/following",
        "followers": "https://test.federation/user/followers",
        "inbox": "https://test.federation/user/inbox",
        "outbox": "https://test.federation/user/outbox",
        "preferredUsername": "user",
    }

    serializer = serializers.ActorSerializer(data=payload)
    assert serializer.is_valid(raise_exception=True)

    actor = serializer.build()

    assert actor.fid == payload["id"]
    assert actor.inbox_url == payload["inbox"]
    assert actor.outbox_url == payload["outbox"]
    assert actor.followers_url == payload["followers"]
    assert actor.following_url == payload["following"]
    assert actor.preferred_username == payload["preferredUsername"]
    assert actor.domain.pk == "test.federation"
    assert actor.type == "Person"
    assert actor.manually_approves_followers is None


def test_actor_serializer_to_ap(factories):
    expected = {
        "@context": jsonld.get_default_context(),
        "id": "https://test.federation/user",
        "type": "Person",
        "following": "https://test.federation/user/following",
        "followers": "https://test.federation/user/followers",
        "inbox": "https://test.federation/user/inbox",
        "outbox": "https://test.federation/user/outbox",
        "preferredUsername": "user",
        "name": "Real User",
        "url": [{"type": "Link", "href": "https://test.url", "mediaType": "text/html"}],
        "manuallyApprovesFollowers": False,
        "publicKey": {
            "id": "https://test.federation/user#main-key",
            "owner": "https://test.federation/user",
            "publicKeyPem": "yolo",
        },
        "endpoints": {"sharedInbox": "https://test.federation/inbox"},
    }
    ac = models.Actor.objects.create(
        fid=expected["id"],
        inbox_url=expected["inbox"],
        outbox_url=expected["outbox"],
        url=expected["url"][0]["href"],
        shared_inbox_url=expected["endpoints"]["sharedInbox"],
        followers_url=expected["followers"],
        following_url=expected["following"],
        public_key=expected["publicKey"]["publicKeyPem"],
        preferred_username=expected["preferredUsername"],
        name=expected["name"],
        domain=models.Domain.objects.create(pk="test.domain"),
        type="Person",
        manually_approves_followers=False,
        attachment_icon=factories["common.Attachment"](),
    )

    content = common_utils.attach_content(
        ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"}
    )
    expected["summary"] = content.rendered
    expected["icon"] = {
        "type": "Image",
        "mediaType": "image/jpeg",
        "url": utils.full_url(ac.attachment_icon.file.url),
    }
    serializer = serializers.ActorSerializer(ac)

    assert serializer.data == expected


def test_webfinger_serializer():
    expected = {
        "subject": "acct:service@test.federation",
        "links": [
            {
                "rel": "self",
                "href": "https://test.federation/federation/instance/actor",
                "type": "application/activity+json",
            }
        ],
        "aliases": ["https://test.federation/federation/instance/actor"],
    }
    actor = models.Actor(
        fid=expected["links"][0]["href"],
        preferred_username="service",
        domain=models.Domain(pk="test.federation"),
    )
    serializer = serializers.ActorWebfingerSerializer(actor)

    assert serializer.data == expected


def test_follow_serializer_to_ap(factories):
    follow = factories["federation.Follow"](local=True)
    serializer = serializers.FollowSerializer(follow)

    expected = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id(),
        "type": "Follow",
        "actor": follow.actor.fid,
        "object": follow.target.fid,
    }

    assert serializer.data == expected


def test_follow_serializer_save(factories):
    actor = factories["federation.Actor"]()
    target = factories["federation.Actor"]()

    data = {
        "id": "https://test.follow",
        "type": "Follow",
        "actor": actor.fid,
        "object": target.fid,
    }
    serializer = serializers.FollowSerializer(data=data)

    assert serializer.is_valid(raise_exception=True)

    follow = serializer.save()

    assert follow.pk is not None
    assert follow.actor == actor
    assert follow.target == target
    assert follow.approved is None


def test_follow_serializer_save_validates_on_context(factories):
    actor = factories["federation.Actor"]()
    target = factories["federation.Actor"]()
    impostor = factories["federation.Actor"]()

    data = {
        "id": "https://test.follow",
        "type": "Follow",
        "actor": actor.fid,
        "object": target.fid,
    }
    serializer = serializers.FollowSerializer(
        data=data, context={"follow_actor": impostor, "follow_target": impostor}
    )

    assert serializer.is_valid() is False

    assert "actor" in serializer.errors
    assert "object" in serializer.errors


def test_accept_follow_serializer_representation(factories):
    follow = factories["federation.Follow"](approved=None)

    expected = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id() + "/accept",
        "type": "Accept",
        "actor": follow.target.fid,
        "object": serializers.FollowSerializer(follow).data,
    }

    serializer = serializers.AcceptFollowSerializer(follow)

    assert serializer.data == expected


def test_accept_follow_serializer_save(factories):
    follow = factories["federation.Follow"](approved=None)
    factories["audio.Channel"](actor=follow.target)

    data = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id() + "/accept",
        "type": "Accept",
        "actor": follow.target.fid,
        "object": serializers.FollowSerializer(follow).data,
    }

    serializer = serializers.AcceptFollowSerializer(data=data)
    assert serializer.is_valid(raise_exception=True)
    serializer.save()

    follow.refresh_from_db()

    assert follow.approved is True


def test_accept_follow_serializer_validates_on_context(factories):
    follow = factories["federation.Follow"](approved=None)
    impostor = factories["federation.Actor"]()
    data = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id() + "/accept",
        "type": "Accept",
        "actor": impostor.url,
        "object": serializers.FollowSerializer(follow).data,
    }

    serializer = serializers.AcceptFollowSerializer(
        data=data, context={"follow_actor": impostor, "follow_target": impostor}
    )

    assert serializer.is_valid() is False
    assert "actor" in serializer.errors["object"]
    assert "object" in serializer.errors["object"]


def test_reject_follow_serializer_representation(factories):
    follow = factories["federation.Follow"](approved=None)

    expected = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id() + "/reject",
        "type": "Reject",
        "actor": follow.target.fid,
        "object": serializers.FollowSerializer(follow).data,
    }

    serializer = serializers.RejectFollowSerializer(follow)

    assert serializer.data == expected


def test_undo_follow_serializer_representation(factories):
    follow = factories["federation.Follow"](approved=True)

    expected = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id() + "/undo",
        "type": "Undo",
        "actor": follow.actor.fid,
        "object": serializers.FollowSerializer(follow).data,
    }

    serializer = serializers.UndoFollowSerializer(follow)

    assert serializer.data == expected


@pytest.mark.parametrize(
    (
        "followed_name",
        "follow_factory",
    ),
    [("audio.Channel", "federation.Follow"), ("users.User", "federation.Follow")],
)
def test_undo_follow_serializer_save(factories, followed_name, follow_factory):
    follow = factories[follow_factory](approved=True)
    factories[followed_name](actor=follow.target)

    data = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id() + "/undo",
        "type": "Undo",
        "actor": follow.actor.fid,
        "object": serializers.FollowSerializer(follow).data,
    }

    serializer = serializers.UndoFollowSerializer(data=data)
    assert serializer.is_valid(raise_exception=True)
    serializer.save()
    if followed_name == "audio.Channel":
        with pytest.raises(models.Follow.DoesNotExist):
            follow.refresh_from_db()
    else:
        with pytest.raises(models.Follow.DoesNotExist):
            follow.refresh_from_db()


def test_undo_follow_serializer_validates_on_context(factories):
    follow = factories["federation.Follow"](approved=True)
    impostor = factories["federation.Actor"]()
    data = {
        "@context": jsonld.get_default_context(),
        "id": follow.get_federation_id() + "/undo",
        "type": "Undo",
        "actor": impostor.url,
        "object": serializers.FollowSerializer(follow).data,
    }

    serializer = serializers.UndoFollowSerializer(
        data=data, context={"follow_actor": impostor, "follow_target": impostor}
    )

    assert serializer.is_valid() is False
    assert "actor" in serializer.errors["object"]
    assert "object" in serializer.errors["object"]


def test_paginated_collection_serializer(factories):
    uploads = factories["music.Upload"].create_batch(size=5)
    actor = factories["federation.Actor"](local=True)

    conf = {
        "id": "https://test.federation/test",
        "items": uploads,
        "item_serializer": serializers.UploadSerializer,
        "actor": actor,
        "page_size": 2,
    }
    expected = {
        "@context": jsonld.get_default_context(),
        "type": "Collection",
        "id": conf["id"],
        "attributedTo": actor.fid,
        "totalItems": len(uploads),
        "current": conf["id"] + "?page=1",
        "last": conf["id"] + "?page=3",
        "first": conf["id"] + "?page=1",
    }

    serializer = serializers.PaginatedCollectionSerializer(conf)

    assert serializer.data == expected


def test_paginated_collection_serializer_validation():
    data = {
        "@context": jsonld.get_default_context(),
        "type": "Collection",
        "id": "https://test.federation/test",
        "totalItems": 5,
        "actor": "http://test.actor",
        "attributedTo": "http://test.actor",
        "first": "https://test.federation/test?page=1",
        "last": "https://test.federation/test?page=1",
        "items": [],
    }

    serializer = serializers.PaginatedCollectionSerializer(data=data)

    assert serializer.is_valid(raise_exception=True) is True
    assert serializer.validated_data["totalItems"] == 5
    assert serializer.validated_data["id"] == data["id"]


def test_collection_page_serializer_validation():
    base = "https://test.federation/test"
    data = {
        "@context": jsonld.get_default_context(),
        "type": "CollectionPage",
        "id": base + "?page=2",
        "totalItems": 5,
        "actor": "https://test.actor",
        "attributedTo": "https://test.actor",
        "items": [],
        "first": "https://test.federation/test?page=1",
        "last": "https://test.federation/test?page=3",
        "prev": base + "?page=1",
        "next": base + "?page=3",
        "partOf": base,
    }

    serializer = serializers.CollectionPageSerializer(data=data)

    assert serializer.is_valid(raise_exception=True) is True
    assert serializer.validated_data["totalItems"] == 5
    assert serializer.validated_data["id"] == data["id"]
    assert serializer.validated_data["attributedTo"] == data["actor"]
    assert serializer.validated_data["items"] == []
    assert serializer.validated_data["prev"] == data["prev"]
    assert serializer.validated_data["next"] == data["next"]
    assert serializer.validated_data["partOf"] == data["partOf"]


def test_collection_page_serializer_can_validate_child():
    data = {
        "@context": jsonld.get_default_context(),
        "type": "CollectionPage",
        "id": "https://test.page?page=2",
        "attributedTo": "https://test.actor",
        "first": "https://test.page?page=1",
        "last": "https://test.page?page=3",
        "partOf": "https://test.page",
        "totalItems": 1,
        "items": [{"in": "valid"}],
    }

    serializer = serializers.CollectionPageSerializer(
        data=data, context={"item_serializer": serializers.UploadSerializer}
    )

    # child are validated but not included in data if not valid
    assert serializer.is_valid(raise_exception=True) is True
    assert len(serializer.validated_data["items"]) == 0


def test_collection_page_serializer(factories):
    uploads = factories["music.Upload"].create_batch(size=5)
    actor = factories["federation.Actor"](local=True)

    conf = {
        "id": "https://test.federation/test",
        "item_serializer": serializers.UploadSerializer,
        "actor": actor,
        "page": Paginator(uploads, 2).page(2),
    }
    expected = {
        "@context": jsonld.get_default_context(),
        "type": "CollectionPage",
        "id": conf["id"] + "?page=2",
        "attributedTo": actor.fid,
        "totalItems": len(uploads),
        "partOf": conf["id"],
        "prev": conf["id"] + "?page=1",
        "next": conf["id"] + "?page=3",
        "first": conf["id"] + "?page=1",
        "last": conf["id"] + "?page=3",
        "items": [
            conf["item_serializer"](
                i, context={"actor": actor, "include_ap_context": False}
            ).data
            for i in conf["page"].object_list
        ],
    }

    serializer = serializers.CollectionPageSerializer(conf)

    assert serializer.data == expected


def test_music_library_serializer_to_ap(factories):
    library = factories["music.Library"](privacy_level="everyone")
    # pending, errored and skippednot included
    factories["music.Upload"](import_status="pending")
    factories["music.Upload"](import_status="errored")
    factories["music.Upload"](import_status="finished")
    serializer = serializers.LibrarySerializer(library)
    expected = {
        "@context": jsonld.get_default_context(),
        "audience": "https://www.w3.org/ns/activitystreams#Public",
        "type": "Library",
        "id": library.fid,
        "name": library.name,
        "attributedTo": library.actor.fid,
        "totalItems": 0,
        "current": library.fid + "?page=1",
        "last": library.fid + "?page=1",
        "first": library.fid + "?page=1",
    }

    assert serializer.data == expected


def test_music_library_serializer_from_public(factories, mocker):
    actor = factories["federation.Actor"]()
    retrieve = mocker.patch(
        "funkwhale_api.federation.utils.retrieve_ap_object", return_value=actor
    )
    data = {
        "@context": jsonld.get_default_context(),
        "audience": "https://www.w3.org/ns/activitystreams#Public",
        "name": "Hello",
        "type": "Library",
        "id": "https://library.id",
        "attributedTo": actor.fid,
        "totalItems": 12,
        "first": "https://library.id?page=1",
        "last": "https://library.id?page=2",
    }
    serializer = serializers.LibrarySerializer(data=data)

    assert serializer.is_valid(raise_exception=True)

    library = serializer.save()

    assert library.actor == actor
    assert library.fid == data["id"]
    assert library.uploads_count == data["totalItems"]
    assert library.privacy_level == "everyone"
    assert library.name == "Hello"

    retrieve.assert_called_once_with(
        actor.fid,
        actor=None,
        queryset=actor.__class__,
        serializer_class=serializers.ActorSerializer,
    )


def test_music_library_serializer_from_private(factories, mocker):
    actor = factories["federation.Actor"]()
    retrieve = mocker.patch(
        "funkwhale_api.federation.utils.retrieve_ap_object", return_value=actor
    )
    data = {
        "@context": jsonld.get_default_context(),
        "audience": "",
        "name": "Hello",
        "type": "Library",
        "id": "https://library.id",
        "attributedTo": actor.fid,
        "totalItems": 12,
        "first": "https://library.id?page=1",
        "last": "https://library.id?page=2",
    }
    serializer = serializers.LibrarySerializer(data=data)

    assert serializer.is_valid(raise_exception=True)

    library = serializer.save()

    assert library.actor == actor
    assert library.fid == data["id"]
    assert library.uploads_count == data["totalItems"]
    assert library.privacy_level == "me"
    assert library.name == "Hello"
    retrieve.assert_called_once_with(
        actor.fid,
        actor=None,
        queryset=actor.__class__,
        serializer_class=serializers.ActorSerializer,
    )


def test_music_library_serializer_from_ap_update(factories, mocker):
    actor = factories["federation.Actor"]()
    library = factories["music.Library"]()

    data = {
        "@context": jsonld.get_default_context(),
        "audience": "https://www.w3.org/ns/activitystreams#Public",
        "name": "Hello",
        "type": "Library",
        "id": library.fid,
        "attributedTo": actor.fid,
        "totalItems": 12,
        "first": "https://library.id?page=1",
        "last": "https://library.id?page=2",
    }
    serializer = serializers.LibrarySerializer(library, data=data)

    assert serializer.is_valid(raise_exception=True)

    serializer.save()
    library.refresh_from_db()

    assert library.uploads_count == data["totalItems"]
    assert library.privacy_level == "everyone"
    assert library.name == "Hello"


def test_activity_pub_artist_serializer_to_ap(factories):
    content = factories["common.Content"]()
    artist = factories["music.Artist"](
        description=content, attributed=True, set_tags=["Punk", "Rock"], with_cover=True
    )
    expected = {
        "@context": jsonld.get_default_context(),
        "type": "Artist",
        "id": artist.fid,
        "name": artist.name,
        "musicbrainzId": artist.mbid,
        "published": artist.creation_date.isoformat(),
        "attributedTo": artist.attributed_to.fid,
        "mediaType": "text/html",
        "content": common_utils.render_html(content.text, content.content_type),
        "image": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": utils.full_url(artist.attachment_cover.file.url),
        },
        "tag": [
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }
    serializer = serializers.ArtistSerializer(artist)

    assert serializer.data == expected


def test_activity_pub_artist_serializer_from_ap_create(factories, faker, now, mocker):
    actor = factories["federation.Actor"]()
    mocker.patch(
        "funkwhale_api.federation.utils.retrieve_ap_object", return_value=actor
    )
    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Artist",
        "id": "https://test.artist",
        "name": "Art",
        "musicbrainzId": faker.uuid4(),
        "published": now.isoformat(),
        "attributedTo": actor.fid,
        "content": "Summary",
        "image": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": "https://attachment.file",
        },
        "tag": [
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }
    serializer = serializers.ArtistSerializer(data=payload)
    assert serializer.is_valid(raise_exception=True) is True

    artist = serializer.save()

    assert artist.fid == payload["id"]
    assert artist.attributed_to == actor
    assert artist.name == payload["name"]
    assert str(artist.mbid) == payload["musicbrainzId"]
    assert artist.description.text == payload["content"]
    assert artist.description.content_type == "text/html"
    assert artist.attachment_cover.url == payload["image"]["url"]
    assert artist.attachment_cover.mimetype == payload["image"]["mediaType"]
    assert artist.get_tags() == ["Punk", "Rock"]


def test_activity_pub_artist_serializer_from_ap_update(factories, faker, now, mocker):
    artist = factories["music.Artist"]()
    actor = factories["federation.Actor"]()
    mocker.patch(
        "funkwhale_api.federation.utils.retrieve_ap_object", return_value=actor
    )
    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Artist",
        "id": artist.fid,
        "name": "Art",
        "musicbrainzId": faker.uuid4(),
        "published": now.isoformat(),
        "attributedTo": actor.fid,
        "content": "Summary",
        "image": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": "https://attachment.file",
        },
        "tag": [
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }
    serializer = serializers.ArtistSerializer(artist, data=payload)
    assert serializer.is_valid(raise_exception=True) is True
    serializer.save()
    artist.refresh_from_db()

    assert artist.attributed_to == actor
    assert artist.name == payload["name"]
    assert str(artist.mbid) == payload["musicbrainzId"]
    assert artist.description.text == payload["content"]
    assert artist.description.content_type == "text/html"
    assert artist.attachment_cover.url == payload["image"]["url"]
    assert artist.attachment_cover.mimetype == payload["image"]["mediaType"]
    assert artist.get_tags() == ["Punk", "Rock"]


def test_activity_pub_album_serializer_to_ap(factories):
    content = factories["common.Content"]()
    album = factories["music.Album"](
        description=content, attributed=True, set_tags=["Punk", "Rock"], with_cover=True
    )

    expected = {
        "@context": jsonld.get_default_context(),
        "type": "Album",
        "id": album.fid,
        "name": album.title,
        "image": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": utils.full_url(album.attachment_cover.file.url),
        },
        "musicbrainzId": album.mbid,
        "published": album.creation_date.isoformat(),
        "released": album.release_date.isoformat(),
        "artist_credit": serializers.ArtistCreditSerializer(
            album.artist_credit.all(),
            many=True,
            context={"include_ap_context": False},
        ).data,
        "attributedTo": album.attributed_to.fid,
        "mediaType": "text/html",
        "content": common_utils.render_html(content.text, content.content_type),
        "tag": [
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }
    serializer = serializers.AlbumSerializer(album)

    assert serializer.data == expected


def test_activity_pub_album_serializer_to_ap_channel_artist(factories):
    channel = factories["audio.Channel"]()
    album = factories["music.Album"](
        artist_credit__artist=channel.artist,
    )

    serializer = serializers.AlbumSerializer(album)
    assert serializer.data["artist_credit"] == [
        {
            "type": "ArtistCredit",
            "id": album.artist_credit.all()[0].fid,
            "artist": {
                "type": "Artist",
                "id": album.artist_credit.all()[0].artist.fid,
                "name": album.artist_credit.all()[0].artist.name,
                "published": album.artist_credit.all()[
                    0
                ].artist.creation_date.isoformat(),
                "musicbrainzId": str(album.artist_credit.all()[0].artist.mbid),
                "attributedTo": album.artist_credit.all()[0].artist.attributed_to.fid,
                "tag": [],
                "image": None,
            },
            "joinphrase": "",
            "credit": album.artist_credit.all()[0].credit,
            "index": None,
            "published": album.artist_credit.all()[0].creation_date.isoformat(),
        }
    ]


def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
    actor = factories["federation.Actor"]()
    artist = factories["music.Artist"]()
    artist_credit = factories["music.ArtistCredit"](artist=artist)

    released = faker.date_object()
    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Album",
        "id": "https://album.example",
        "name": faker.sentence(),
        "image": {"type": "Link", "mediaType": "image/jpeg", "href": faker.url()},
        "musicbrainzId": faker.uuid4(),
        "published": now.isoformat(),
        "released": released.isoformat(),
        "artist_credit": serializers.ArtistCreditSerializer(
            [artist_credit], many=True, context={"include_ap_context": False}
        ).data,
        "attributedTo": actor.fid,
        "tag": [
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }
    serializer = serializers.AlbumSerializer(data=payload)
    assert serializer.is_valid(raise_exception=True) is True

    album = serializer.save()

    assert album.title == payload["name"]
    assert str(album.mbid) == payload["musicbrainzId"]
    assert album.release_date == released
    assert album.artist_credit.all()[0].artist == artist
    assert album.attachment_cover.url == payload["image"]["href"]
    assert album.attachment_cover.mimetype == payload["image"]["mediaType"]
    assert sorted(album.tagged_items.values_list("tag__name", flat=True)) == [
        "Punk",
        "Rock",
    ]


def test_activity_pub_album_serializer_from_ap_create_channel_artist(
    factories, faker, now, mocker
):
    actor = factories["federation.Actor"]()
    channel = factories["audio.Channel"]()
    ac = factories["music.ArtistCredit"](artist=channel.artist)

    released = faker.date_object()
    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Album",
        "id": "https://album.example",
        "name": faker.sentence(),
        "published": now.isoformat(),
        "released": released.isoformat(),
        "artist_credit": [
            {
                "artist": {
                    "type": "Artist",
                    "mediaType": "text/plain",
                    "content": "Artist summary",
                    "id": "http://hello.artist",
                    "name": "John Smith",
                    "musicbrainzId": str(uuid.uuid4()),
                    "published": now.isoformat(),
                    "attributedTo": "https://cover.image/album-artist.pn",
                    "tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
                    "image": {
                        "type": "Image",
                        "url": "https://cover.image/album-artist.png",
                        "mediaType": "image/png",
                    },
                },
                "joinphrase": "",
                "name": "John Smith",
                "published": now.isoformat(),
                "id": "http://hello.artistcredit",
            }
        ],
        "attributedTo": actor.fid,
    }

    mocker.patch.object(utils, "retrieve_ap_object", return_value=ac)
    serializer = serializers.AlbumSerializer(data=payload)
    assert serializer.is_valid(raise_exception=True) is True

    album = serializer.save()

    assert album.artist_credit.all()[0].artist == channel.artist


def test_activity_pub_album_serializer_from_ap_update(factories, faker):
    album = factories["music.Album"](attributed=True)
    released = faker.date_object()
    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Album",
        "id": album.fid,
        "name": faker.sentence(),
        "image": {"type": "Link", "mediaType": "image/jpeg", "href": faker.url()},
        "musicbrainzId": faker.uuid4(),
        "published": album.creation_date.isoformat(),
        "released": released.isoformat(),
        "artist_credit": serializers.ArtistCreditSerializer(
            album.artist_credit.all(),
            many=True,
            context={"include_ap_context": False},
        ).data,
        "attributedTo": album.attributed_to.fid,
        "tag": [
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }
    serializer = serializers.AlbumSerializer(album, data=payload)
    assert serializer.is_valid(raise_exception=True) is True

    serializer.save()

    album.refresh_from_db()

    assert album.title == payload["name"]
    assert str(album.mbid) == payload["musicbrainzId"]
    assert album.release_date == released
    assert album.attachment_cover.url == payload["image"]["href"]
    assert album.attachment_cover.mimetype == payload["image"]["mediaType"]
    assert sorted(album.tagged_items.values_list("tag__name", flat=True)) == [
        "Punk",
        "Rock",
    ]


def test_activity_pub_track_serializer_to_ap(factories):
    content = factories["common.Content"]()
    track = factories["music.Track"](
        description=content,
        license="cc-by-4.0",
        copyright="test",
        disc_number=3,
        attributed=True,
        set_tags=["Punk", "Rock"],
        with_cover=True,
    )
    expected = {
        "@context": jsonld.get_default_context(),
        "published": track.creation_date.isoformat(),
        "type": "Track",
        "musicbrainzId": track.mbid,
        "id": track.fid,
        "name": track.title,
        "position": track.position,
        "disc": track.disc_number,
        "license": track.license.conf["identifiers"][0],
        "copyright": "test",
        "artist_credit": serializers.ArtistCreditSerializer(
            track.artist_credit.all(),
            many=True,
            context={"include_ap_context": False},
        ).data,
        "album": serializers.AlbumSerializer(
            track.album, context={"include_ap_context": False}
        ).data,
        "attributedTo": track.attributed_to.fid,
        "mediaType": "text/html",
        "content": common_utils.render_html(content.text, content.content_type),
        "tag": [
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
        "image": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": utils.full_url(track.attachment_cover.file.url),
        },
    }
    serializer = serializers.TrackSerializer(track)

    assert serializer.data == expected


def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
    add_tags = mocker.patch("funkwhale_api.tags.models.add_tags")
    track_attributed_to = factories["federation.Actor"]()
    album_attributed_to = factories["federation.Actor"]()
    album_artist_attributed_to = factories["federation.Actor"]()
    artist_attributed_to = factories["federation.Actor"]()

    activity = factories["federation.Activity"]()
    published = timezone.now()
    released = timezone.now().date()
    data = {
        "@context": jsonld.get_default_context(),
        "type": "Track",
        "id": "http://hello.track",
        "published": published.isoformat(),
        "musicbrainzId": str(uuid.uuid4()),
        "name": "Black in back",
        "position": 5,
        "disc": 1,
        "content": "Hello there",
        "attributedTo": track_attributed_to.fid,
        "image": {
            "type": "Image",
            "url": "https://cover.image/track.png",
            "mediaType": "image/png",
        },
        "album": {
            "type": "Album",
            "id": "http://hello.album",
            "name": "Purple album",
            "musicbrainzId": str(uuid.uuid4()),
            "published": published.isoformat(),
            "released": released.isoformat(),
            "content": "Album summary",
            "mediaType": "text/markdown",
            "attributedTo": album_attributed_to.fid,
            "image": {
                "type": "Link",
                "href": "https://cover.image/test.png",
                "mediaType": "image/png",
            },
            "tag": [{"type": "Hashtag", "name": "AlbumTag"}],
            "artist_credit": [
                {
                    "artist": {
                        "type": "Artist",
                        "mediaType": "text/plain",
                        "content": "Artist summary",
                        "id": "http://hello.artist",
                        "name": "John Smith",
                        "musicbrainzId": str(uuid.uuid4()),
                        "published": published.isoformat(),
                        "attributedTo": album_artist_attributed_to.fid,
                        "tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
                        "image": {
                            "type": "Image",
                            "url": "https://cover.image/album-artist.png",
                            "mediaType": "image/png",
                        },
                    },
                    "joinphrase": "",
                    "credit": "John Smith Credit",
                    "published": published.isoformat(),
                    "id": "http://hello.artistcredit",
                }
            ],
        },
        "artist_credit": [
            {
                "artist": {
                    "type": "Artist",
                    "id": "http://hello.trackartist",
                    "name": "Bob Smith",
                    "mediaType": "text/plain",
                    "content": "Other artist summary",
                    "musicbrainzId": str(uuid.uuid4()),
                    "attributedTo": artist_attributed_to.fid,
                    "published": published.isoformat(),
                    "tag": [{"type": "Hashtag", "name": "ArtistTag"}],
                    "image": {
                        "type": "Image",
                        "url": "https://cover.image/artist.png",
                        "mediaType": "image/png",
                    },
                },
                "joinphrase": "",
                "credit": "Credit Name",
                "published": published.isoformat(),
                "id": "http://hello.artistcredit",
            }
        ],
        "tag": [
            {"type": "Hashtag", "name": "#Hello"},
            {"type": "Hashtag", "name": "World"},
        ],
    }
    serializer = serializers.TrackSerializer(data=data, context={"activity": activity})
    assert serializer.is_valid(raise_exception=True)

    track = serializer.save()
    album = track.album
    artist = track.artist_credit.all()[0].artist
    album_artist = track.album.artist_credit.all()[0].artist

    assert track.from_activity == activity
    assert track.fid == data["id"]
    assert track.title == data["name"]
    assert track.position == data["position"]
    assert track.disc_number == data["disc"]
    assert track.creation_date == published
    assert track.attributed_to == track_attributed_to
    assert str(track.mbid) == data["musicbrainzId"]
    assert track.description.text == data["content"]
    assert track.description.content_type == "text/html"
    assert track.attachment_cover.url == data["image"]["url"]
    assert track.attachment_cover.mimetype == data["image"]["mediaType"]

    assert album.from_activity == activity
    assert album.attachment_cover.url == data["album"]["image"]["href"]
    assert album.attachment_cover.mimetype == data["album"]["image"]["mediaType"]
    assert album.title == data["album"]["name"]
    assert album.fid == data["album"]["id"]
    assert str(album.mbid) == data["album"]["musicbrainzId"]
    assert album.creation_date == published
    assert album.release_date == released
    assert album.attributed_to == album_attributed_to
    assert album.description.text == data["album"]["content"]
    assert album.description.content_type == data["album"]["mediaType"]

    assert artist.from_activity == activity
    assert artist.name == data["artist_credit"][0]["artist"]["name"]
    assert track.artist_credit.all()[0].credit == data["artist_credit"][0]["credit"]
    assert (
        album.artist_credit.all()[0].credit
        == data["album"]["artist_credit"][0]["credit"]
    )

    assert artist.fid == data["artist_credit"][0]["artist"]["id"]
    assert str(artist.mbid) == data["artist_credit"][0]["artist"]["musicbrainzId"]
    assert artist.creation_date == published
    assert artist.attributed_to == artist_attributed_to
    assert artist.description.text == data["artist_credit"][0]["artist"]["content"]
    assert (
        artist.description.content_type
        == data["artist_credit"][0]["artist"]["mediaType"]
    )
    assert (
        artist.attachment_cover.url
        == data["artist_credit"][0]["artist"]["image"]["url"]
    )
    assert (
        artist.attachment_cover.mimetype
        == data["artist_credit"][0]["artist"]["image"]["mediaType"]
    )

    assert album_artist.from_activity == activity
    assert album_artist.name == data["album"]["artist_credit"][0]["artist"]["name"]
    assert album_artist.fid == data["album"]["artist_credit"][0]["artist"]["id"]
    assert (
        str(album_artist.mbid)
        == data["album"]["artist_credit"][0]["artist"]["musicbrainzId"]
    )
    assert album_artist.creation_date == published
    assert album_artist.attributed_to == album_artist_attributed_to
    assert (
        album_artist.description.text
        == data["album"]["artist_credit"][0]["artist"]["content"]
    )
    assert (
        album_artist.description.content_type
        == data["album"]["artist_credit"][0]["artist"]["mediaType"]
    )
    assert (
        album_artist.attachment_cover.url
        == data["album"]["artist_credit"][0]["artist"]["image"]["url"]
    )
    assert (
        album_artist.attachment_cover.mimetype
        == data["album"]["artist_credit"][0]["artist"]["image"]["mediaType"]
    )

    add_tags.assert_any_call(track, *["Hello", "World"])
    add_tags.assert_any_call(album, *["AlbumTag"])
    add_tags.assert_any_call(album_artist, *["AlbumArtistTag"])
    add_tags.assert_any_call(artist, *["ArtistTag"])


def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker, faker):
    set_tags = mocker.patch("funkwhale_api.tags.models.set_tags")
    content = factories["common.Content"]()
    track_attributed_to = factories["federation.Actor"]()
    track = factories["music.Track"](description=content)

    published = timezone.now()
    data = {
        "@context": jsonld.get_default_context(),
        "type": "Track",
        "id": track.fid,
        "published": published.isoformat(),
        "musicbrainzId": str(uuid.uuid4()),
        "name": "Black in back",
        "position": 5,
        "disc": 2,
        "content": "hello there",
        "attributedTo": track_attributed_to.fid,
        "album": serializers.AlbumSerializer(track.album).data,
        "artist_credit": serializers.ArtistCreditSerializer(
            track.artist_credit.all(), many=True
        ).data,
        "image": {"type": "Image", "mediaType": "image/jpeg", "url": faker.url()},
        "tag": [
            {"type": "Hashtag", "name": "#Hello"},
            # Ensure we can handle tags without a leading #
            {"type": "Hashtag", "name": "World"},
        ],
    }
    serializer = serializers.TrackSerializer(track, data=data)
    assert serializer.is_valid(raise_exception=True)

    serializer.save()
    track.refresh_from_db()

    assert track.fid == data["id"]
    assert track.title == data["name"]
    assert track.position == data["position"]
    assert track.disc_number == data["disc"]
    assert track.attributed_to == track_attributed_to
    assert track.description.content_type == "text/html"
    assert track.description.text == "hello there"
    assert str(track.mbid) == data["musicbrainzId"]
    assert track.attachment_cover.url == data["image"]["url"]
    assert track.attachment_cover.mimetype == data["image"]["mediaType"]

    set_tags.assert_called_once_with(track, *["Hello", "World"])

    with pytest.raises(content.DoesNotExist):
        content.refresh_from_db()


def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
    activity = factories["federation.Activity"]()
    library = factories["music.Library"]()

    published = timezone.now()
    updated = timezone.now()
    released = timezone.now().date()
    data = {
        "@context": jsonld.get_default_context(),
        "type": "Audio",
        "id": "https://track.file",
        "name": "Ignored",
        "published": published.isoformat(),
        "updated": updated.isoformat(),
        "duration": 43,
        "bitrate": 42,
        "size": 66,
        "url": {"href": "https://audio.file", "type": "Link", "mediaType": "audio/mp3"},
        "library": library.fid,
        "track": {
            "type": "Track",
            "id": "http://hello.track",
            "published": published.isoformat(),
            "musicbrainzId": str(uuid.uuid4()),
            "name": "Black in back",
            "position": 5,
            "album": {
                "type": "Album",
                "id": "http://hello.album",
                "name": "Purple album",
                "musicbrainzId": str(uuid.uuid4()),
                "published": published.isoformat(),
                "released": released.isoformat(),
                "image": {
                    "type": "Link",
                    "href": "https://cover.image/test.png",
                    "mediaType": "image/png",
                },
                "artist_credit": [
                    {
                        "artist": {
                            "type": "Artist",
                            "id": "http://hello.artist",
                            "name": "John Smith",
                            "musicbrainzId": str(uuid.uuid4()),
                            "published": published.isoformat(),
                        },
                        "joinphrase": "",
                        "credit": "John Smith Credit",
                        "published": published.isoformat(),
                        "id": "http://hello.artistcredit",
                    }
                ],
            },
            "artist_credit": [
                {
                    "artist": {
                        "type": "Artist",
                        "id": "http://hello.trackartist",
                        "name": "Bob Smith",
                        "musicbrainzId": str(uuid.uuid4()),
                        "published": published.isoformat(),
                    },
                    "joinphrase": "",
                    "credit": "Bob Smith Credit",
                    "published": published.isoformat(),
                    "id": "http://hello.artistcredit",
                }
            ],
        },
    }
    r_mock.get(data["track"]["album"]["image"]["href"], body=io.BytesIO(b"coucou"))

    serializer = serializers.UploadSerializer(data=data, context={"activity": activity})
    assert serializer.is_valid(raise_exception=True)
    track_create = mocker.spy(serializers.TrackSerializer, "create")
    upload = serializer.save()

    assert upload.track.from_activity == activity
    assert upload.from_activity == activity
    assert track_create.call_count == 1
    assert upload.fid == data["id"]
    assert upload.track.fid == data["track"]["id"]
    assert upload.duration == data["duration"]
    assert upload.size == data["size"]
    assert upload.bitrate == data["bitrate"]
    assert upload.source == data["url"]["href"]
    assert upload.mimetype == data["url"]["mediaType"]
    assert upload.creation_date == published
    assert upload.import_status == "finished"
    assert upload.modification_date == updated


def test_activity_pub_upload_serializer_from_ap_update(factories, mocker, now, r_mock):
    library = factories["music.Library"]()
    upload = factories["music.Upload"](library=library, track__album__with_cover=True)
    data = {
        "@context": jsonld.get_default_context(),
        "type": "Audio",
        "id": upload.fid,
        "name": "Ignored",
        "published": now.isoformat(),
        "updated": now.isoformat(),
        "duration": 42,
        "bitrate": 42,
        "size": 66,
        "url": {
            "href": "https://audio.file/url",
            "type": "Link",
            "mediaType": "audio/mp3",
        },
        "library": library.fid,
        "track": serializers.TrackSerializer(upload.track).data,
    }
    r_mock.get(data["track"]["album"]["image"]["url"], body=io.BytesIO(b"coucou"))

    serializer = serializers.UploadSerializer(upload, data=data)
    assert serializer.is_valid(raise_exception=True)
    serializer.save()
    upload.refresh_from_db()

    assert upload.fid == data["id"]
    assert upload.duration == data["duration"]
    assert upload.size == data["size"]
    assert upload.bitrate == data["bitrate"]
    assert upload.source == data["url"]["href"]
    assert upload.mimetype == data["url"]["mediaType"]


def test_activity_pub_upload_serializer_validtes_library_actor(factories, mocker):
    library = factories["music.Library"]()
    usurpator = factories["federation.Actor"]()

    serializer = serializers.UploadSerializer(data={}, context={"actor": usurpator})

    with pytest.raises(serializers.serializers.ValidationError):
        serializer.validate_library(library.fid)


def test_activity_pub_audio_serializer_to_ap(factories):
    upload = factories["music.Upload"](
        mimetype="audio/mp3",
        bitrate=42,
        duration=43,
        size=44,
        library__privacy_level="everyone",
    )
    expected = {
        "@context": jsonld.get_default_context(),
        "type": "Audio",
        "id": upload.fid,
        "name": upload.track.full_name,
        "published": upload.creation_date.isoformat(),
        "updated": upload.modification_date.isoformat(),
        "duration": upload.duration,
        "bitrate": upload.bitrate,
        "size": upload.size,
        "to": contexts.AS.Public,
        "attributedTo": upload.library.actor.fid,
        "url": [
            {
                "href": utils.full_url(upload.listen_url_no_download),
                "type": "Link",
                "mediaType": "audio/mp3",
            },
            {
                "type": "Link",
                "mediaType": "text/html",
                "href": utils.full_url(upload.track.get_absolute_url()),
            },
        ],
        "library": upload.library.fid,
        "track": serializers.TrackSerializer(
            upload.track, context={"include_ap_context": False}
        ).data,
    }

    serializer = serializers.UploadSerializer(upload)

    assert serializer.data == expected


def test_local_actor_serializer_to_ap(factories, settings):
    expected = {
        "@context": jsonld.get_default_context(),
        "id": "https://test.federation/user",
        "type": "Person",
        "following": "https://test.federation/user/following",
        "followers": "https://test.federation/user/followers",
        "inbox": "https://test.federation/user/inbox",
        "outbox": "https://test.federation/user/outbox",
        "preferredUsername": "user",
        "name": "Real User",
        "summary": "Hello world",
        "manuallyApprovesFollowers": False,
        "publicKey": {
            "id": "https://test.federation/user#main-key",
            "owner": "https://test.federation/user",
            "publicKeyPem": "yolo",
        },
        "endpoints": {"sharedInbox": "https://test.federation/inbox"},
    }
    ac = models.Actor.objects.create(
        fid=expected["id"],
        inbox_url=expected["inbox"],
        outbox_url=expected["outbox"],
        shared_inbox_url=expected["endpoints"]["sharedInbox"],
        followers_url=expected["followers"],
        following_url=expected["following"],
        public_key=expected["publicKey"]["publicKeyPem"],
        preferred_username=expected["preferredUsername"],
        name=expected["name"],
        domain=models.Domain.objects.create(pk="test.federation"),
        type="Person",
        manually_approves_followers=False,
        attachment_icon=factories["common.Attachment"](),
    )
    content = common_utils.attach_content(
        ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"}
    )
    user = factories["users.User"]()
    user.actor = ac
    user.save()
    ac.refresh_from_db()
    expected["summary"] = content.rendered
    expected["url"] = [
        {
            "type": "Link",
            "href": "https://{}/@{}".format(
                settings.FUNKWHALE_HOSTNAME, ac.preferred_username
            ),
            "mediaType": "text/html",
        }
    ]
    expected["icon"] = {
        "type": "Image",
        "mediaType": "image/jpeg",
        "url": utils.full_url(ac.attachment_icon.file.url),
    }
    serializer = serializers.ActorSerializer(ac)

    assert serializer.data == expected


def test_activity_serializer_validate_recipients_empty(db):
    s = serializers.BaseActivitySerializer()

    with pytest.raises(serializers.serializers.ValidationError):
        s.validate_recipients({}, {})

    with pytest.raises(serializers.serializers.ValidationError):
        s.validate_recipients({"to": []}, {})

    with pytest.raises(serializers.serializers.ValidationError):
        s.validate_recipients({"cc": []}, {})


def test_activity_serializer_validate_recipients_context(db):
    s = serializers.BaseActivitySerializer(context={"recipients": ["dummy"]})

    assert s.validate_recipients({}, {}) is None


def test_track_serializer_update_license(factories):
    licenses.load(licenses.LICENSES)

    obj = factories["music.Track"](license=None)

    serializer = serializers.TrackSerializer(obj)
    serializer.update(obj, {"license": "http://creativecommons.org/licenses/by/2.0/"})

    obj.refresh_from_db()

    assert obj.license_id == "cc-by-2.0"


def test_channel_actor_serializer(factories):
    channel = factories["audio.Channel"](
        actor__attachment_icon=None,
        artist__with_cover=True,
        artist__set_tags=["punk", "rock"],
    )

    serializer = serializers.ActorSerializer(channel.actor)
    expected_url = [
        {
            "type": "Link",
            "href": channel.actor.get_absolute_url(),
            "mediaType": "text/html",
        },
        {
            "type": "Link",
            "href": channel.get_rss_url(),
            "mediaType": "application/rss+xml",
        },
    ]
    expected_icon = {
        "type": "Image",
        "mediaType": channel.artist.attachment_cover.mimetype,
        "url": channel.artist.attachment_cover.download_url_original,
    }
    assert serializer.data["url"] == expected_url
    assert serializer.data["icon"] == expected_icon
    assert serializer.data["attributedTo"] == channel.attributed_to.fid
    assert serializer.data["category"] == channel.artist.content_category
    assert serializer.data["tag"] == [
        {"type": "Hashtag", "name": "#punk"},
        {"type": "Hashtag", "name": "#rock"},
    ]


def test_channel_actor_serializer_from_ap_create(mocker, factories):
    domain = factories["federation.Domain"](name="test.pod")
    attributed_to = factories["federation.Actor"](domain=domain)
    get_actor = mocker.patch.object(actors, "get_actor", return_value=attributed_to)
    actor_data = {
        "@context": jsonld.get_default_context(),
        "followers": "https://test.pod/federation/actors/mychannel/followers",
        "preferredUsername": "mychannel",
        "id": "https://test.pod/federation/actors/mychannel",
        "endpoints": {"sharedInbox": "https://test.pod/federation/shared/inbox"},
        "name": "mychannel",
        "following": "https://test.pod/federation/actors/mychannel/following",
        "outbox": "https://test.pod/federation/actors/mychannel/outbox",
        "url": [
            {
                "mediaType": "text/html",
                "href": "https://test.pod/channels/mychannel",
                "type": "Link",
            },
            {
                "mediaType": "application/rss+xml",
                "href": "https://test.pod/api/v1/channels/mychannel/rss",
                "type": "Link",
            },
        ],
        "type": "Person",
        "category": "podcast",
        "attributedTo": attributed_to.fid,
        "manuallyApprovesFollowers": False,
        "inbox": "https://test.pod/federation/actors/mychannel/inbox",
        "icon": {
            "mediaType": "image/jpeg",
            "type": "Image",
            "url": "https://test.pod/media/attachments/dd/ce/b2/nosmile.jpeg",
        },
        "summary": "<p>content</p>",
        "publicKey": {
            "owner": "https://test.pod/federation/actors/mychannel",
            "publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\n+KwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
            "id": "https://test.pod/federation/actors/mychannel#main-key",
        },
        "tag": [
            {"type": "Hashtag", "name": "#Indie"},
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }

    serializer = serializers.ActorSerializer(data=actor_data)
    assert serializer.is_valid(raise_exception=True) is True
    actor = serializer.save()

    get_actor.assert_called_once_with(actor_data["attributedTo"])
    assert actor.preferred_username == actor_data["preferredUsername"]
    assert actor.fid == actor_data["id"]
    assert actor.name == actor_data["name"]
    assert actor.type == actor_data["type"]
    assert actor.public_key == actor_data["publicKey"]["publicKeyPem"]
    assert actor.outbox_url == actor_data["outbox"]
    assert actor.inbox_url == actor_data["inbox"]
    assert actor.shared_inbox_url == actor_data["endpoints"]["sharedInbox"]
    assert actor.channel.attributed_to == attributed_to
    assert actor.channel.rss_url == actor_data["url"][1]["href"]
    assert actor.channel.artist.attributed_to == attributed_to
    assert actor.channel.artist.content_category == actor_data["category"]
    assert actor.channel.artist.name == actor_data["name"]
    assert actor.channel.artist.get_tags() == ["Indie", "Punk", "Rock"]
    assert actor.channel.artist.description.text == actor_data["summary"]
    assert actor.channel.artist.description.content_type == "text/html"
    assert actor.channel.artist.attachment_cover.url == actor_data["icon"]["url"]
    assert (
        actor.channel.artist.attachment_cover.mimetype
        == actor_data["icon"]["mediaType"]
    )
    assert actor.channel.library.fid is not None
    assert actor.channel.library.actor == attributed_to
    assert actor.channel.library.privacy_level == "everyone"
    assert actor.channel.library.name == actor_data["name"]


def test_channel_actor_serializer_from_ap_update(mocker, factories):
    domain = factories["federation.Domain"](name="test.pod")
    attributed_to = factories["federation.Actor"](domain=domain)
    actor = factories["federation.Actor"](domain=domain)
    channel = factories["audio.Channel"](actor=actor, attributed_to=attributed_to)
    get_actor = mocker.patch.object(actors, "get_actor", return_value=attributed_to)
    library = channel.library
    actor_data = {
        "@context": jsonld.get_default_context(),
        "followers": "https://test.pod/federation/actors/mychannel/followers",
        "preferredUsername": "mychannel",
        "id": actor.fid,
        "endpoints": {"sharedInbox": "https://test.pod/federation/shared/inbox"},
        "name": "mychannel",
        "following": "https://test.pod/federation/actors/mychannel/following",
        "outbox": "https://test.pod/federation/actors/mychannel/outbox",
        "url": [
            {
                "mediaType": "text/html",
                "href": "https://test.pod/channels/mychannel",
                "type": "Link",
            },
            {
                "mediaType": "application/rss+xml",
                "href": "https://test.pod/api/v1/channels/mychannel/rss",
                "type": "Link",
            },
        ],
        "type": "Person",
        "category": "podcast",
        "attributedTo": attributed_to.fid,
        "manuallyApprovesFollowers": False,
        "inbox": "https://test.pod/federation/actors/mychannel/inbox",
        "icon": {
            "mediaType": "image/jpeg",
            "type": "Image",
            "url": "https://test.pod/media/attachments/dd/ce/b2/nosmile.jpeg",
        },
        "summary": "<p>content</p>",
        "publicKey": {
            "owner": "https://test.pod/federation/actors/mychannel",
            "publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\n+KwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
            "id": "https://test.pod/federation/actors/mychannel#main-key",
        },
        "tag": [
            {"type": "Hashtag", "name": "#Indie"},
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }

    serializer = serializers.ActorSerializer(data=actor_data)
    assert serializer.is_valid(raise_exception=True) is True
    serializer.save()
    channel.refresh_from_db()
    get_actor.assert_called_once_with(actor_data["attributedTo"])
    assert channel.actor == actor
    assert channel.attributed_to == attributed_to
    assert channel.rss_url == actor_data["url"][1]["href"]
    assert channel.artist.attributed_to == attributed_to
    assert channel.artist.content_category == actor_data["category"]
    assert channel.artist.name == actor_data["name"]
    assert channel.artist.get_tags() == ["Indie", "Punk", "Rock"]
    assert channel.artist.description.text == actor_data["summary"]
    assert channel.artist.description.content_type == "text/html"
    assert channel.artist.attachment_cover.url == actor_data["icon"]["url"]
    assert channel.artist.attachment_cover.mimetype == actor_data["icon"]["mediaType"]
    assert channel.library.actor == attributed_to
    assert channel.library.privacy_level == library.privacy_level
    assert channel.library.name == library.name


def test_channel_actor_outbox_serializer(factories):
    channel = factories["audio.Channel"]()
    uploads = factories["music.Upload"].create_batch(
        5,
        track__artist_credit__artist=channel.artist,
        library=channel.library,
        import_status="finished",
    )

    expected = {
        "@context": jsonld.get_default_context(),
        "type": "OrderedCollection",
        "id": channel.actor.outbox_url,
        "attributedTo": channel.actor.fid,
        "totalItems": len(uploads),
        "first": channel.actor.outbox_url + "?page=1",
        "last": channel.actor.outbox_url + "?page=1",
        "current": channel.actor.outbox_url + "?page=1",
    }

    serializer = serializers.ChannelOutboxSerializer(channel)

    assert serializer.data == expected


def test_channel_upload_serializer(factories):
    channel = factories["audio.Channel"](library__privacy_level="everyone")
    content = factories["common.Content"]()
    cover = factories["common.Attachment"]()
    upload = factories["music.Upload"](
        playable=True,
        bitrate=543,
        size=543,
        duration=54,
        library=channel.library,
        import_status="finished",
        track__set_tags=["Punk"],
        track__attachment_cover=cover,
        track__description=content,
        track__disc_number=3,
        track__position=12,
        track__license="cc0-1.0",
        track__copyright="Copyright something",
        track__album__set_tags=["Rock"],
        track__artist_credit__artist__set_tags=["Indie"],
        track__artist_credit__artist__local=True,
    )

    expected = {
        "@context": jsonld.get_default_context(),
        "type": "Audio",
        "id": upload.fid,
        "name": upload.track.title,
        "summary": "#Indie #Punk #Rock",
        "attributedTo": channel.actor.fid,
        "published": upload.creation_date.isoformat(),
        "mediaType": "text/html",
        "content": common_utils.render_html(content.text, content.content_type),
        "to": "https://www.w3.org/ns/activitystreams#Public",
        "position": upload.track.position,
        "duration": upload.duration,
        "album": upload.track.album.fid,
        "disc": upload.track.disc_number,
        "copyright": upload.track.copyright,
        "license": upload.track.local_license["identifiers"][0],
        "url": [
            {
                "type": "Link",
                "mediaType": "text/html",
                "href": utils.full_url(upload.track.get_absolute_url()),
            },
            {
                "type": "Link",
                "mediaType": upload.mimetype,
                "href": utils.full_url(upload.listen_url_no_download),
                "bitrate": upload.bitrate,
                "size": upload.size,
            },
        ],
        "image": {
            "type": "Image",
            "url": upload.track.attachment_cover.download_url_original,
            "mediaType": upload.track.attachment_cover.mimetype,
        },
        "tag": [
            {"type": "Hashtag", "name": "#Indie"},
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
    }

    serializer = serializers.ChannelUploadSerializer(upload)

    assert serializer.data == expected


def test_channel_upload_serializer_from_ap_create(factories, now, mocker):
    channel = factories["audio.Channel"](library__privacy_level="everyone")
    album = factories["music.Album"](artist_credit__artist=channel.artist)
    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Audio",
        "id": "https://test.pod/uuid",
        "name": "My test track",
        "summary": "#Indie #Punk #Rock",
        "attributedTo": channel.actor.fid,
        "published": now.isoformat(),
        "mediaType": "text/html",
        "content": "<p>Hello</p>",
        "duration": 543,
        "position": 4,
        "disc": 2,
        "album": album.fid,
        "to": "https://www.w3.org/ns/activitystreams#Public",
        "copyright": "Copyright test",
        "license": "http://creativecommons.org/publicdomain/zero/1.0/",
        "url": [
            {
                "type": "Link",
                "mediaType": "text/html",
                "href": "https://test.pod/track",
            },
            {
                "type": "Link",
                "mediaType": "audio/mpeg",
                "href": "https://test.pod/file.mp3",
                "bitrate": 192000,
                "size": 15492738,
            },
        ],
        "tag": [
            {"type": "Hashtag", "name": "#Indie"},
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
        "image": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": "https://image.example/image.png",
        },
    }
    mocker.patch.object(utils, "retrieve_ap_object", return_value=album)

    serializer = serializers.ChannelUploadSerializer(
        data=payload, context={"channel": channel}
    )
    assert serializer.is_valid(raise_exception=True) is True

    upload = serializer.save(channel=channel)

    assert upload.library == channel.library
    assert upload.import_status == "finished"
    assert upload.creation_date == now
    assert upload.fid == payload["id"]
    assert upload.source == payload["url"][1]["href"]
    assert upload.mimetype == payload["url"][1]["mediaType"]
    assert upload.size == payload["url"][1]["size"]
    assert upload.bitrate == payload["url"][1]["bitrate"]
    assert upload.duration == payload["duration"]
    assert upload.track.artist_credit.all()[0].artist == channel.artist
    assert upload.track.position == payload["position"]
    assert upload.track.disc_number == payload["disc"]
    assert upload.track.attributed_to == channel.attributed_to
    assert upload.track.title == payload["name"]
    assert upload.track.creation_date == now
    assert upload.track.description.content_type == payload["mediaType"]
    assert upload.track.description.text == payload["content"]
    assert upload.track.fid == payload["id"]
    assert upload.track.license.pk == "cc0-1.0"
    assert upload.track.copyright == payload["copyright"]
    assert upload.track.get_tags() == ["Indie", "Punk", "Rock"]
    assert upload.track.attachment_cover.mimetype == payload["image"]["mediaType"]
    assert upload.track.attachment_cover.url == payload["image"]["url"]
    assert upload.track.album == album


def test_channel_upload_serializer_from_ap_update(factories, now, mocker):
    channel = factories["audio.Channel"](library__privacy_level="everyone")
    album = factories["music.Album"](artist_credit__artist=channel.artist)
    upload = factories["music.Upload"](
        track__album=album, track__artist_credit__artist=channel.artist
    )

    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Audio",
        "id": upload.fid,
        "name": "Hello there",
        "attributedTo": channel.actor.fid,
        "published": now.isoformat(),
        "mediaType": "text/html",
        "content": "<p>Hello</p>",
        "duration": 543,
        "position": 4,
        "disc": 2,
        "album": album.fid,
        "to": "https://www.w3.org/ns/activitystreams#Public",
        "copyright": "Copyright test",
        "license": "http://creativecommons.org/publicdomain/zero/1.0/",
        "url": [
            {
                "type": "Link",
                "mediaType": "text/html",
                "href": "https://test.pod/track",
            },
            {
                "type": "Link",
                "mediaType": "audio/mpeg",
                "href": "https://test.pod/file.mp3",
                "bitrate": 192000,
                "size": 15492738,
            },
        ],
        "tag": [
            {"type": "Hashtag", "name": "#Indie"},
            {"type": "Hashtag", "name": "#Punk"},
            {"type": "Hashtag", "name": "#Rock"},
        ],
        "image": {
            "type": "Image",
            "mediaType": "image/jpeg",
            "url": "https://image.example/image.png",
        },
    }
    mocker.patch.object(utils, "retrieve_ap_object", return_value=album)

    serializer = serializers.ChannelUploadSerializer(
        data=payload, context={"channel": channel}
    )
    assert serializer.is_valid(raise_exception=True) is True

    serializer.save(channel=channel)
    upload.refresh_from_db()

    assert upload.library == channel.library
    assert upload.import_status == "finished"
    assert upload.creation_date == now
    assert upload.fid == payload["id"]
    assert upload.source == payload["url"][1]["href"]
    assert upload.mimetype == payload["url"][1]["mediaType"]
    assert upload.size == payload["url"][1]["size"]
    assert upload.bitrate == payload["url"][1]["bitrate"]
    assert upload.duration == payload["duration"]
    assert upload.track.artist_credit.all()[0].artist == channel.artist
    assert upload.track.position == payload["position"]
    assert upload.track.disc_number == payload["disc"]
    assert upload.track.attributed_to == channel.attributed_to
    assert upload.track.title == payload["name"]
    assert upload.track.creation_date == now
    assert upload.track.description.content_type == payload["mediaType"]
    assert upload.track.description.text == payload["content"]
    assert upload.track.fid == payload["id"]
    assert upload.track.license.pk == "cc0-1.0"
    assert upload.track.copyright == payload["copyright"]
    assert upload.track.get_tags() == ["Indie", "Punk", "Rock"]
    assert upload.track.attachment_cover.mimetype == payload["image"]["mediaType"]
    assert upload.track.attachment_cover.url == payload["image"]["url"]
    assert upload.track.album == album


def test_channel_create_upload_serializer(factories):
    channel = factories["audio.Channel"]()
    upload = factories["music.Upload"](
        playable=True, library=channel.library, import_status="finished"
    )

    expected = {
        "@context": jsonld.get_default_context(),
        "type": "Create",
        "id": utils.full_url(
            reverse("federation:music:uploads-activity", kwargs={"uuid": upload.uuid})
        ),
        "actor": upload.library.channel.actor.fid,
        "object": serializers.ChannelUploadSerializer(
            upload, context={"include_ap_context": False}
        ).data,
    }

    serializer = serializers.ChannelCreateUploadSerializer(upload)

    assert serializer.data == expected


def test_report_serializer_from_ap_create(factories, faker, now, mocker):
    actor = factories["federation.Actor"]()
    obj = factories["music.Artist"](local=True)
    payload = {
        "@context": jsonld.get_default_context(),
        "type": "Flag",
        "id": "https://test.report",
        "actor": actor.fid,
        "content": "hello world",
        "object": [obj.fid],
        "tag": [{"type": "Hashtag", "name": "#offensive_content"}],
    }
    serializer = serializers.FlagSerializer(data=payload, context={"actor": actor})
    assert serializer.is_valid(raise_exception=True) is True

    report = serializer.save()

    assert report.fid == payload["id"]
    assert report.summary == payload["content"]
    assert report.submitter == actor
    assert report.target == obj
    assert report.target_state == moderation_serializers.get_target_state(obj)
    assert report.target_owner == moderation_serializers.get_target_owner(obj)
    assert report.type == "offensive_content"


def test_report_serializer_to_ap(factories):
    report = factories["moderation.Report"](local=True)
    expected = {
        "@context": jsonld.get_default_context(),
        "type": "Flag",
        "id": report.fid,
        "actor": actors.get_service_actor().fid,
        "content": report.summary,
        "object": [report.target.fid],
        "tag": [{"type": "Hashtag", "name": f"#{report.type}"}],
    }
    serializer = serializers.FlagSerializer(report)
    assert serializer.data == expected


def test_artist_credit_serializer_to_ap(factories):
    ac = factories["music.ArtistCredit"](artist__local=True)
    serializer = serializers.ArtistCreditSerializer(ac)

    expected = {
        "@context": jsonld.get_default_context(),
        "id": ac.get_federation_id(),
        "type": "ArtistCredit",
        "artist": serializers.ArtistSerializer(
            ac.artist, context={"include_ap_context": False}
        ).data,
        "joinphrase": ac.joinphrase,
        "credit": ac.credit,
        "index": ac.index,
        "published": ac.creation_date.isoformat(),
    }

    assert serializer.data == expected
