# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Conversation.ParticipationTest do
  use Pleroma.DataCase, async: true
  import Pleroma.Factory
  alias Pleroma.Conversation
  alias Pleroma.Conversation.Participation
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.Web.CommonAPI

  test "getting a participation will also preload things" do
    user = insert(:user)
    other_user = insert(:user)

    {:ok, _activity} =
      CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"})

    [participation] = Participation.for_user(user)

    participation = Participation.get(participation.id, preload: [:conversation])

    assert %Pleroma.Conversation{} = participation.conversation
  end

  test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do
    user = insert(:user)
    other_user = insert(:user)

    {:ok, _} =
      CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"})

    user = User.get_cached_by_id(user.id)
    other_user = User.get_cached_by_id(other_user.id)

    [%{read: true}] = Participation.for_user(user)
    [%{read: false} = participation] = Participation.for_user(other_user)
    assert Participation.unread_count(user) == 0
    assert Participation.unread_count(other_user) == 1

    {:ok, _} =
      CommonAPI.post(other_user, %{
        status: "Hey @#{user.nickname}.",
        visibility: "direct",
        in_reply_to_conversation_id: participation.id
      })

    user = User.get_cached_by_id(user.id)
    other_user = User.get_cached_by_id(other_user.id)

    [%{read: false}] = Participation.for_user(user)
    [%{read: true}] = Participation.for_user(other_user)

    assert Participation.unread_count(user) == 1
    assert Participation.unread_count(other_user) == 0
  end

  test "for a new conversation, it sets the recipents of the participation" do
    user = insert(:user)
    other_user = insert(:user)
    third_user = insert(:user)

    {:ok, activity} =
      CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"})

    user = User.get_cached_by_id(user.id)
    other_user = User.get_cached_by_id(other_user.id)
    [participation] = Participation.for_user(user)
    participation = Pleroma.Repo.preload(participation, :recipients)

    assert length(participation.recipients) == 2
    assert user in participation.recipients
    assert other_user in participation.recipients

    # Mentioning another user in the same conversation will not add a new recipients.

    {:ok, _activity} =
      CommonAPI.post(user, %{
        in_reply_to_status_id: activity.id,
        status: "Hey @#{third_user.nickname}.",
        visibility: "direct"
      })

    [participation] = Participation.for_user(user)
    participation = Pleroma.Repo.preload(participation, :recipients)

    assert length(participation.recipients) == 2
  end

  test "it creates a participation for a conversation and a user" do
    user = insert(:user)
    conversation = insert(:conversation)

    {:ok, %Participation{} = participation} =
      Participation.create_for_user_and_conversation(user, conversation)

    {:ok, participation} = time_travel(participation, -2)

    assert participation.user_id == user.id
    assert participation.conversation_id == conversation.id

    # Creating again returns the same participation
    {:ok, %Participation{} = participation_two} =
      Participation.create_for_user_and_conversation(user, conversation)

    assert participation.id == participation_two.id
    refute participation.updated_at == participation_two.updated_at
  end

  test "recreating an existing participations sets it to unread" do
    participation = insert(:participation, %{read: true})

    {:ok, participation} =
      Participation.create_for_user_and_conversation(
        participation.user,
        participation.conversation
      )

    refute participation.read
  end

  test "it marks a participation as read" do
    participation = insert(:participation, %{updated_at: ~N[2017-07-17 17:09:58], read: false})
    {:ok, updated_participation} = Participation.mark_as_read(participation)

    assert updated_participation.read
    assert :gt = NaiveDateTime.compare(updated_participation.updated_at, participation.updated_at)
  end

  test "it marks a participation as unread" do
    participation = insert(:participation, %{read: true})
    {:ok, participation} = Participation.mark_as_unread(participation)

    refute participation.read
  end

  test "it marks all the user's participations as read" do
    user = insert(:user)
    other_user = insert(:user)
    participation1 = insert(:participation, %{read: false, user: user})
    participation2 = insert(:participation, %{read: false, user: user})
    participation3 = insert(:participation, %{read: false, user: other_user})

    {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user)

    assert Participation.get(participation1.id).read == true
    assert Participation.get(participation2.id).read == true
    assert Participation.get(participation3.id).read == false
  end

  test "gets all the participations for a user, ordered by updated at descending" do
    user = insert(:user)
    {:ok, activity_one} = CommonAPI.post(user, %{status: "x", visibility: "direct"})
    {:ok, activity_two} = CommonAPI.post(user, %{status: "x", visibility: "direct"})

    {:ok, activity_three} =
      CommonAPI.post(user, %{
        status: "x",
        visibility: "direct",
        in_reply_to_status_id: activity_one.id
      })

    # Offset participations because the accuracy of updated_at is down to a second

    for {activity, offset} <- [{activity_two, 1}, {activity_three, 2}] do
      conversation = Conversation.get_for_ap_id(activity.data["context"])
      participation = Participation.for_user_and_conversation(user, conversation)
      updated_at = NaiveDateTime.add(Map.get(participation, :updated_at), offset)

      Ecto.Changeset.change(participation, %{updated_at: updated_at})
      |> Repo.update!()
    end

    assert [participation_one, participation_two] = Participation.for_user(user)

    object2 = Pleroma.Object.normalize(activity_two, fetch: false)
    object3 = Pleroma.Object.normalize(activity_three, fetch: false)

    user = Repo.get(Pleroma.User, user.id)

    assert participation_one.conversation.ap_id == object3.data["context"]
    assert participation_two.conversation.ap_id == object2.data["context"]
    assert participation_one.conversation.users == [user]

    # Pagination
    assert [participation_one] = Participation.for_user(user, %{"limit" => 1})

    assert participation_one.conversation.ap_id == object3.data["context"]

    # With last_activity_id
    assert [participation_one] =
             Participation.for_user_with_last_activity_id(user, %{"limit" => 1})

    assert participation_one.last_activity_id == activity_three.id
  end

  test "Doesn't die when the conversation gets empty" do
    user = insert(:user)

    {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
    [participation] = Participation.for_user_with_last_activity_id(user)

    assert participation.last_activity_id == activity.id

    {:ok, _} = CommonAPI.delete(activity.id, user)

    [] = Participation.for_user_with_last_activity_id(user)
  end

  test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do
    user = insert(:user)
    other_user = insert(:user)

    {:ok, _activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
    [participation] = Participation.for_user_with_last_activity_id(user)

    participation = Repo.preload(participation, :recipients)
    user = User.get_cached_by_id(user.id)

    assert participation.recipients |> length() == 1
    assert user in participation.recipients

    {:ok, participation} = Participation.set_recipients(participation, [other_user.id])

    assert participation.recipients |> length() == 2
    assert user in participation.recipients
    assert other_user in participation.recipients
  end

  describe "blocking" do
    test "when the user blocks a recipient, the existing conversations with them are marked as read" do
      blocker = insert(:user)
      blocked = insert(:user)
      third_user = insert(:user)

      {:ok, _direct1} =
        CommonAPI.post(third_user, %{
          status: "Hi @#{blocker.nickname}",
          visibility: "direct"
        })

      {:ok, _direct2} =
        CommonAPI.post(third_user, %{
          status: "Hi @#{blocker.nickname}, @#{blocked.nickname}",
          visibility: "direct"
        })

      {:ok, _direct3} =
        CommonAPI.post(blocked, %{
          status: "Hi @#{blocker.nickname}",
          visibility: "direct"
        })

      {:ok, _direct4} =
        CommonAPI.post(blocked, %{
          status: "Hi @#{blocker.nickname}, @#{third_user.nickname}",
          visibility: "direct"
        })

      assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] =
               Participation.for_user(blocker)

      assert Participation.unread_count(blocker) == 4

      {:ok, _user_relationship} = User.block(blocker, blocked)

      # The conversations with the blocked user are marked as read
      assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
               Participation.for_user(blocker)

      assert Participation.unread_count(blocker) == 1

      # The conversation is not marked as read for the blocked user
      assert [_, _, %{read: false}] = Participation.for_user(blocked)
      assert Participation.unread_count(blocker) == 1

      # The conversation is not marked as read for the third user
      assert [%{read: false}, _, _] = Participation.for_user(third_user)
      assert Participation.unread_count(third_user) == 1
    end

    test "the new conversation with the blocked user is not marked as unread " do
      blocker = insert(:user)
      blocked = insert(:user)
      third_user = insert(:user)

      {:ok, _user_relationship} = User.block(blocker, blocked)

      # When the blocked user is the author
      {:ok, _direct1} =
        CommonAPI.post(blocked, %{
          status: "Hi @#{blocker.nickname}",
          visibility: "direct"
        })

      assert [%{read: true}] = Participation.for_user(blocker)
      assert Participation.unread_count(blocker) == 0

      # When the blocked user is a recipient
      {:ok, _direct2} =
        CommonAPI.post(third_user, %{
          status: "Hi @#{blocker.nickname}, @#{blocked.nickname}",
          visibility: "direct"
        })

      assert [%{read: true}, %{read: true}] = Participation.for_user(blocker)
      assert Participation.unread_count(blocker) == 0

      assert [%{read: false}, _] = Participation.for_user(blocked)
      assert Participation.unread_count(blocked) == 1
    end

    test "the conversation with the blocked user is not marked as unread on a reply" do
      blocker = insert(:user)
      blocked = insert(:user)
      third_user = insert(:user)

      {:ok, _direct1} =
        CommonAPI.post(blocker, %{
          status: "Hi @#{third_user.nickname}, @#{blocked.nickname}",
          visibility: "direct"
        })

      {:ok, _user_relationship} = User.block(blocker, blocked)
      assert [%{read: true}] = Participation.for_user(blocker)

      assert Participation.unread_count(blocker) == 0
      assert [blocked_participation] = Participation.for_user(blocked)

      # When it's a reply from the blocked user
      {:ok, _direct2} =
        CommonAPI.post(blocked, %{
          status: "reply",
          visibility: "direct",
          in_reply_to_conversation_id: blocked_participation.id
        })

      assert [%{read: true}] = Participation.for_user(blocker)

      assert Participation.unread_count(blocker) == 0
      assert [third_user_participation] = Participation.for_user(third_user)

      # When it's a reply from the third user
      {:ok, _direct3} =
        CommonAPI.post(third_user, %{
          status: "reply",
          visibility: "direct",
          in_reply_to_conversation_id: third_user_participation.id
        })

      assert [%{read: true}] = Participation.for_user(blocker)
      assert Participation.unread_count(blocker) == 0

      # Marked as unread for the blocked user
      assert [%{read: false}] = Participation.for_user(blocked)

      assert Participation.unread_count(blocked) == 1
    end
  end

  test "deletes a conversation" do
    user = insert(:user)
    other_user = insert(:user)

    {:ok, _activity} =
      CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"})

    assert [participation] = Participation.for_user(other_user)
    assert {:ok, _} = Participation.delete(participation)
    assert [] == Participation.for_user(other_user)
  end
end