defmodule Mobilizon.Web.Resolvers.GroupTest do
  use Mobilizon.Web.ConnCase
  use Oban.Testing, repo: Mobilizon.Storage.Repo

  import Mobilizon.Factory

  alias Mobilizon.Actors.{Actor, Follower}
  alias Mobilizon.GraphQL.AbsintheHelpers

  @non_existent_username "nonexistent"
  @new_group_params %{name: "new group", preferredUsername: "new_group"}

  setup %{conn: conn} do
    user = insert(:user)
    actor = insert(:actor, user: user)

    {:ok, conn: conn, actor: actor, user: user}
  end

  describe "create_group/3" do
    @create_group_mutation """
    mutation CreateGroup(
    $preferredUsername: String!
    $name: String!
    $summary: String
    $avatar: MediaInput
    $banner: MediaInput
    ) {
      createGroup(
        preferredUsername: $preferredUsername
        name: $name
        summary: $summary
        banner: $banner
        avatar: $avatar
      ) {
        preferredUsername
        type
        banner {
          id
          url
        }
      }
    }
    """

    test "creates a group and check a group with this name does not already exist",
         %{conn: conn, user: user} do
      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @create_group_mutation,
          variables: @new_group_params
        )

      assert res["errors"] == nil

      assert res["data"]["createGroup"]["preferredUsername"] ==
               @new_group_params.preferredUsername

      assert res["data"]["createGroup"]["type"] == "GROUP"

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @create_group_mutation,
          variables: @new_group_params
        )

      assert hd(res["errors"])["message"] ==
               "A profile or group with that name already exists"
    end

    test "doesn't creates a group if the username doesn't match the requirements",
         %{conn: conn, user: user} do
      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @create_group_mutation,
          variables: Map.put(@new_group_params, :preferredUsername, "no@way")
        )

      assert hd(res["errors"])["message"] == [
               "Username must only contain alphanumeric lowercased characters and underscores."
             ]
    end
  end

  describe "list groups" do
    @list_groups_query """
    {
        groups {
            elements {
              preferredUsername,
            },
            total
        }
      }
    """

    test "list_groups/3 doesn't returns all groups if not authenticated", %{conn: conn} do
      insert(:group, visibility: :public)
      insert(:group, visibility: :unlisted)
      insert(:group, visibility: :private)

      res = AbsintheHelpers.graphql_query(conn, query: @list_groups_query)

      assert hd(res["errors"])["message"] == "You need to be logged in"
    end

    test "list_groups/3 doesn't return all groups if not a moderator", %{conn: conn} do
      insert(:group, visibility: :public)
      insert(:group, visibility: :unlisted)
      insert(:group, visibility: :private)
      user = insert(:user)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(query: @list_groups_query)

      assert hd(res["errors"])["message"] == "You don't have permission to do this"
    end

    test "list_groups/3 returns all groups if a moderator", %{conn: conn} do
      group_1 = insert(:group, visibility: :public)
      group_2 = insert(:group, visibility: :unlisted)
      group_3 = insert(:group, visibility: :private)
      user = insert(:user, role: :moderator)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(query: @list_groups_query)

      assert res["data"]["groups"]["total"] == 3

      assert res["data"]["groups"]["elements"]
             |> Enum.map(& &1["preferredUsername"])
             |> MapSet.new() ==
               [group_1, group_2, group_3] |> Enum.map(& &1.preferred_username) |> MapSet.new()
    end
  end

  describe "find a group" do
    @group_query """
      query Group($preferredUsername: String!) {
        group(preferredUsername: $preferredUsername) {
          preferredUsername
        }
      }
    """

    @group_with_member_query """
      query Group($preferredUsername: String!) {
        group(preferredUsername: $preferredUsername) {
            preferredUsername,
            members {
              total,
              elements {
                role,
                actor {
                  preferredUsername
                }
              }
            }
        }
      }
    """

    test "find_group/3 returns a group by its username", %{conn: conn, actor: actor, user: user} do
      user2 = insert(:user)
      insert(:actor, user: user2)
      group = insert(:group)
      insert(:member, parent: group, actor: actor, role: :administrator)
      insert(:member, parent: group, role: :member)

      # Unlogged
      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @group_with_member_query,
          variables: %{
            preferredUsername: group.preferred_username
          }
        )

      assert hd(res["errors"])["message"] ==
               "Not authorized to access object member"

      # Login with non-member
      res =
        conn
        |> auth_conn(user2)
        |> AbsintheHelpers.graphql_query(
          query: @group_query,
          variables: %{
            preferredUsername: group.preferred_username
          }
        )

      assert res["errors"] == nil

      assert res["data"]["group"]["preferredUsername"] ==
               group.preferred_username

      # Login with member
      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @group_with_member_query,
          variables: %{
            preferredUsername: group.preferred_username,
            actorId: actor.id
          }
        )

      assert res["errors"] == nil

      assert res["data"]["group"]["members"]["total"] == 2

      admin =
        res["data"]["group"]["members"]["elements"]
        |> Enum.find(&(&1["role"] == "ADMINISTRATOR"))

      assert admin["actor"]["preferredUsername"] ==
               actor.preferred_username

      # Non existent username
      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @group_query,
          variables: %{preferredUsername: @non_existent_username}
        )

      assert res["data"]["group"] == nil

      assert hd(res["errors"])["message"] == "Group not found"
    end

    test "find_group doesn't list group members access if group is private", %{
      conn: conn,
      actor: actor
    } do
      group = insert(:group, visibility: :private)
      insert(:member, parent: group, actor: actor, role: :administrator)

      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @group_with_member_query,
          variables: %{
            preferredUsername: group.preferred_username
          }
        )

      assert hd(res["errors"])["message"] ==
               "Not authorized to access object member"
    end
  end

  describe "update a group" do
    @update_group_mutation """
      mutation UpdateGroup(
      $id: ID!
      $name: String
      $summary: String
      $avatar: MediaInput
      $banner: MediaInput
      $visibility: GroupVisibility
      $physicalAddress: AddressInput
      ) {
        updateGroup(
          id: $id
          name: $name
          summary: $summary
          banner: $banner
          avatar: $avatar
          visibility: $visibility
          physicalAddress: $physicalAddress
        ) {
          id
          preferredUsername
          name
          summary
          visibility
          avatar {
            url
          }
          banner {
            url
          }
        }
      }
    """
    @new_group_name "new name for group"

    test "update_group/3 updates a group", %{conn: conn, user: user, actor: actor} do
      group = insert(:group)
      insert(:member, parent: group, actor: actor, role: :administrator)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @update_group_mutation,
          variables: %{
            id: group.id,
            name: @new_group_name,
            visibility: "UNLISTED"
          }
        )

      assert is_nil(res["errors"])
      assert res["data"]["updateGroup"]["name"] == @new_group_name
      assert res["data"]["updateGroup"]["visibility"] == "UNLISTED"
    end

    test "update_group/3 requires to be logged-in to update a group", %{conn: conn} do
      group = insert(:group)

      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @update_group_mutation,
          variables: %{id: group.id, name: @new_group_name}
        )

      assert hd(res["errors"])["message"] == "You need to be logged in"
    end

    test "update_group/3 requires to be an admin of the group to update a group", %{
      conn: conn,
      actor: actor
    } do
      group = insert(:group)
      insert(:member, parent: group, actor: actor, role: :administrator)
      user = insert(:user)
      actor2 = insert(:actor, user: user)

      # Actor not member
      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @update_group_mutation,
          variables: %{id: group.id, name: @new_group_name}
        )

      assert hd(res["errors"])["message"] == "Profile is not administrator for the group"

      # Actor member but not admin
      insert(:member, parent: group, actor: actor2, role: :moderator)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @update_group_mutation,
          variables: %{id: group.id, name: @new_group_name}
        )

      assert hd(res["errors"])["message"] == "Profile is not administrator for the group"
    end
  end

  describe "delete a group" do
    @delete_group_mutation """
    mutation DeleteGroup($groupId: ID!) {
      deleteGroup(
        groupId: $groupId
      ) {
          id
        }
      }
    """

    test "delete_group/3 deletes a group", %{conn: conn, user: user, actor: actor} do
      group = insert(:group)
      insert(:member, parent: group, actor: actor, role: :administrator)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @delete_group_mutation,
          variables: %{groupId: group.id}
        )

      assert res["errors"] == nil
      assert res["data"]["deleteGroup"]["id"] == to_string(group.id)

      assert_enqueued(
        worker: Mobilizon.Service.Workers.Background,
        args: %{
          "actor_id" => group.id,
          "author_id" => actor.id,
          "op" => "delete_actor",
          "reserve_username" => true,
          "suspension" => false
        }
      )

      # Can't be used right now, probably because we try to run a transaction in a Oban Job while using Ecto Sandbox

      # assert %{success: 1, snoozed: 0, failure: 0, discard: 0, cancelled: 0} == Oban.drain_queue(queue: :background)

      # res =
      #   conn
      #   |> auth_conn(user)
      #   |> AbsintheHelpers.graphql_query(
      #     query: @delete_group_mutation,
      #     variables: %{groupId: group.id}
      #   )

      # assert res["data"] == "tt"
      # assert hd(json_response(res, 200)["errors"])["message"] =~ "not found"
    end

    test "delete_group/3 should check user authentication", %{conn: conn, actor: actor} do
      group = insert(:group)
      insert(:member, parent: group, actor: actor, role: :member)

      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @delete_group_mutation,
          variables: %{groupId: group.id}
        )

      assert hd(res["errors"])["message"] == "You need to be logged in"
    end

    test "delete_group/3 should check the actor is owned by the user", %{
      conn: conn,
      user: user,
      actor: actor
    } do
      group = insert(:group)
      insert(:member, parent: group, actor: actor, role: :member)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @delete_group_mutation,
          variables: %{groupId: group.id}
        )

      assert hd(res["errors"])["message"] ==
               "Current profile is not an administrator of the selected group"
    end

    test "delete_group/3 should check the actor is a member of this group", %{
      conn: conn,
      user: user
    } do
      group = insert(:group)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @delete_group_mutation,
          variables: %{groupId: group.id}
        )

      assert hd(res["errors"])["message"] =~ "not a member"
    end

    test "delete_group/3 should check the actor is an administrator of this group", %{
      conn: conn,
      user: user,
      actor: actor
    } do
      group = insert(:group)
      insert(:member, parent: group, actor: actor, role: :member)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @delete_group_mutation,
          variables: %{groupId: group.id}
        )

      assert hd(res["errors"])["message"] =~ "not an administrator"
    end
  end

  describe "follow a group" do
    @follow_group_mutation """
    mutation FollowGroup($groupId: ID!, $notify: Boolean) {
      followGroup(groupId: $groupId, notify: $notify) {
        id
      }
    }
    """

    test "when not authenticated", %{conn: conn, user: _user} do
      %Actor{type: :Group} = group = insert(:group)

      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @follow_group_mutation,
          variables: %{groupId: group.id}
        )

      assert hd(res["errors"])["message"] == "You need to be logged in"
    end

    test "when group doesn't exist", %{conn: conn, user: user} do
      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @follow_group_mutation,
          variables: %{groupId: "89542"}
        )

      assert hd(res["errors"])["message"] == "Group not found"
    end

    test "success", %{conn: conn, user: user} do
      %Actor{type: :Group} = group = insert(:group)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @follow_group_mutation,
          variables: %{groupId: group.id}
        )

      assert res["errors"] == nil
    end
  end

  describe "unfollow a group" do
    @unfollow_group_mutation """
    mutation UnfollowGroup($groupId: ID!) {
      unfollowGroup(groupId: $groupId) {
        id
      }
    }
    """

    test "when not authenticated", %{conn: conn, user: _user} do
      %Actor{type: :Group} = group = insert(:group)

      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @unfollow_group_mutation,
          variables: %{groupId: group.id}
        )

      assert hd(res["errors"])["message"] == "You need to be logged in"
    end

    test "when group doesn't exist", %{conn: conn, user: user} do
      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @unfollow_group_mutation,
          variables: %{groupId: "89542"}
        )

      assert hd(res["errors"])["message"] == "Group not found"
    end

    test "when the profile is not following the group", %{conn: conn, user: user} do
      %Actor{type: :Group} = group = insert(:group)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @unfollow_group_mutation,
          variables: %{groupId: group.id}
        )

      assert hd(res["errors"])["message"] =~ "Could not unfollow actor: you are not following"
    end

    test "success", %{conn: conn, user: user, actor: actor} do
      %Actor{type: :Group} = group = insert(:group)

      Mobilizon.Actors.follow(group, actor)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @unfollow_group_mutation,
          variables: %{groupId: group.id}
        )

      assert res["errors"] == nil
      assert Mobilizon.Actors.get_follower_by_followed_and_following(group, actor) == nil
    end
  end

  describe "update a group follow" do
    @update_group_follow_mutation """
    mutation UpdateGroupFollow($followId: ID!, $notify: Boolean) {
      updateGroupFollow(followId: $followId, notify: $notify) {
        id
        notify
      }
    }
    """
    test "when not authenticated", %{conn: conn, user: _user, actor: actor} do
      %Actor{type: :Group} = group = insert(:group)
      follow = insert(:follower, target_actor: group, actor: actor)

      res =
        conn
        |> AbsintheHelpers.graphql_query(
          query: @update_group_follow_mutation,
          variables: %{followId: follow.id}
        )

      assert hd(res["errors"])["message"] == "You need to be logged in"
    end

    test "when follow doesn't exist", %{conn: conn, user: user} do
      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @update_group_follow_mutation,
          variables: %{followId: "d7c83493-e4a0-42a2-a15d-a469e955e80a"}
        )

      assert hd(res["errors"])["message"] == "Follow not found"
    end

    test "when follow does not match the current actor", %{conn: conn, user: user} do
      %Actor{type: :Group} = group = insert(:group)
      follow = insert(:follower, target_actor: group)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @update_group_follow_mutation,
          variables: %{followId: follow.id}
        )

      assert hd(res["errors"])["message"] == "Follow does not match your account"
    end

    test "success", %{conn: conn, user: user, actor: actor} do
      %Actor{type: :Group} = group = insert(:group)
      follow = insert(:follower, target_actor: group, actor: actor)

      assert %Follower{notify: true} =
               Mobilizon.Actors.get_follower_by_followed_and_following(group, actor)

      res =
        conn
        |> auth_conn(user)
        |> AbsintheHelpers.graphql_query(
          query: @update_group_follow_mutation,
          variables: %{followId: follow.id, notify: false}
        )

      assert res["errors"] == nil
      assert res["data"]["updateGroupFollow"]["notify"] == false

      assert %Follower{notify: false} =
               Mobilizon.Actors.get_follower_by_followed_and_following(group, actor)
    end
  end
end