spec/integration/api/posts_controller_spec.rb
# frozen_string_literal: true

require_relative "api_spec_helper"

describe Api::V1::PostsController do
  let(:auth) {
    FactoryBot.create(
      :auth_with_default_scopes,
      scopes: %w[openid public:read public:modify private:read private:modify],
      user:   FactoryBot.create(:user, profile: FactoryBot.create(:profile_with_image_url))
    )
  }

  let(:auth_public_only) {
    FactoryBot.create(
      :auth_with_default_scopes,
      scopes: %w[openid public:read public:modify]
    )
  }

  let(:auth_read_only) {
    FactoryBot.create(
      :auth_with_default_scopes,
      scopes: %w[openid public:read private:read]
    )
  }

  let(:auth_public_only_read_only) {
    FactoryBot.create(
      :auth_with_default_scopes,
      scopes: %w[openid public:read]
    )
  }

  let(:auth_minimum_scopes) { FactoryBot.create(:auth_with_default_scopes) }
  let!(:access_token) { auth.create_access_token.to_s }
  let!(:access_token_public_only) { auth_public_only.create_access_token.to_s }
  let!(:access_token_read_only) { auth_read_only.create_access_token.to_s }
  let!(:access_token_public_only_read_only) { auth_public_only_read_only.create_access_token.to_s }
  let!(:access_token_minimum_scopes) { auth_minimum_scopes.create_access_token.to_s }
  let(:invalid_token) { SecureRandom.hex(9) }

  before do
    alice.person.profile = FactoryBot.create(:profile_with_image_url)
    bob.person.profile = FactoryBot.create(:profile_with_image_url)
    eve.person.profile = FactoryBot.create(:profile_with_image_url)

    @alice_aspect = alice.aspects.first
    @alice_photo1 = alice.post(:photo, pending: true, user_file: File.open(photo_fixture_name), to: @alice_aspect.id)
    @alice_photo2 = alice.post(:photo, pending: true, user_file: File.open(photo_fixture_name), to: @alice_aspect.id)
    @alice_photo_ids = [@alice_photo1.id.to_s, @alice_photo2.id.to_s]
    @alice_photo_guids = [@alice_photo1.guid, @alice_photo2.guid]
  end

  describe "#show" do
    before do
      @status = alice.post(
        :status_message,
        text:   "hello @{#{bob.diaspora_handle}} and @{#{eve.diaspora_handle}}from Alice!",
        public: true,
        to:     "all"
      )
    end

    context "access simple by post ID" do
      it "gets post" do
        get(
          api_v1_post_path(@status.guid),
          params: {
            access_token: access_token
          }
        )
        expect(response.status).to eq(200)
        post = response_body(response)
        confirm_post_format(post, alice, @status, [bob, eve])

        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end
    end

    context "access full post by post ID" do
      it "gets post" do
        base_params = {status_message: {text: "myText #nsfw"}, public: true}
        poll_params = {poll_question: "something?", poll_answers: %w[yes no maybe]}
        location_params = {location_address: "somewhere", location_coords: "1,2"}
        merged_params = base_params.merge(location_params)
        merged_params = merged_params.merge(poll_params)
        merged_params = merged_params.merge(photos: @alice_photo_ids)
        status_message = StatusMessageCreationService.new(alice).create(merged_params)
        status_message.open_graph_cache = FactoryBot.create(:open_graph_cache, video_url: "http://example.org")
        status_message.o_embed_cache = FactoryBot.create(:o_embed_cache)
        status_message.save

        get(
          api_v1_post_path(status_message.guid),
          params: {
            access_token: access_token
          }
        )
        expect(response.status).to eq(200)
        post = response_body(response)
        confirm_post_format(post, alice, status_message)

        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end
    end

    context "access interacted with post by ID" do
      it "gets post" do
        auth.user.like!(@status)
        auth.user.reshare!(@status)
        auth.user.reports.create!(item: @status, text: "Meh!")
        @status.reload

        get(
          api_v1_post_path(@status.guid),
          params: {
            access_token: access_token
          }
        )
        expect(response.status).to eq(200)
        post = response_body(response)
        confirm_post_format(post, alice, @status, [bob, eve])
        expect(post["own_interaction_state"]["liked"]).to be true
        expect(post["own_interaction_state"]["reshared"]).to be true
        expect(post["own_interaction_state"]["subscribed"]).to be true
        expect(post["own_interaction_state"]["reported"]).to be true

        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end
    end

    context "access reshare style post by post ID" do
      it "gets post" do
        reshare_post = FactoryBot.create(:reshare, root: @status, author: bob.person)
        get(
          api_v1_post_path(reshare_post.guid),
          params: {
            access_token: access_token
          }
        )
        expect(response.status).to eq(200)
        post = response_body(response)
        confirm_reshare_format(post, @status, alice)

        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end
    end

    context "access private post not to reader" do
      it "fails to get post" do
        private_post = alice.post(:status_message, text: "to aspect only", public: false, to: alice.aspects.first.id)
        get(
          api_v1_post_path(private_post.guid),
          params: {
            access_token: access_token
          }
        )
        confirm_api_error(response, 404, "Post with provided guid could not be found")
      end
    end

    context "access private post to reader without private:read scope in token" do
      it "fails to get post" do
        alice_shared_aspect = alice.aspects.create(name: "shared aspect")
        alice.share_with(auth_public_only_read_only.user.person, alice_shared_aspect)
        alice.share_with(auth_read_only.user.person, alice_shared_aspect)

        shared_post = alice.post(:status_message, text: "to aspect only", public: false, to: alice_shared_aspect.id)
        get(
          api_v1_post_path(shared_post.guid),
          params: {
            access_token: access_token_public_only_read_only
          }
        )
        confirm_api_error(response, 404, "Post with provided guid could not be found")

        get(
          api_v1_post_path(shared_post.guid),
          params: {
            access_token: access_token_read_only
          }
        )
        expect(response.status).to eq(200)
      end
    end

    context "access post with invalid id" do
      it "fails to get post" do
        get(
          api_v1_post_path("999_999_999"),
          params: {
            access_token: access_token
          }
        )
        confirm_api_error(response, 404, "Post with provided guid could not be found")
      end
    end

    context "access with invalid token" do
      it "fails" do
        get(
          api_v1_post_path(@status.guid),
          params: {
            access_token: invalid_token
          }
        )
        expect(response.status).to eq(401)
      end
    end
  end

  describe "#create" do
    before do
      @user_photo1 = auth.user.post(:photo, pending: true, user_file: File.open(photo_fixture_name), public: true)
      @user_photo2 = auth.user.post(:photo, pending: true, user_file: File.open(photo_fixture_name), public: true)
      @user_photo3 = auth.user.post(:photo, pending: false, user_file: File.open(photo_fixture_name), public: true)
      @user_photo_ids = [@user_photo1.id.to_s, @user_photo2.id.to_s]
      @user_photo_guids = [@user_photo1.guid, @user_photo2.guid]
    end

    context "when given read-write access token" do
      it "creates a public post" do
        post_for_ref_only = auth.user.post(
          :status_message,
          text:   "Hello this is a public post!",
          public: true
        )

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         "Hello this is a public post!",
            public:       true
          }
        )
        expect(response.status).to eq(200)
        post = response_body(response)
        confirm_post_format(post, auth.user, post_for_ref_only)
        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end

      it "or creates a private post" do
        aspect = auth.user.aspects.create(name: "new aspect")
        post_for_ref_only = auth.user.post(
          :status_message,
          text:       "Hello this is a private post!",
          aspect_ids: [aspect.id]
        )

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         "Hello this is a private post!",
            public:       false,
            aspects:      [aspect.id]
          }
        )
        post = response_body(response)
        expect(response.status).to eq(200)
        confirm_post_format(post, auth.user, post_for_ref_only)
        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end

      it "doesn't creates a private post without private:modify scope in token" do
        aspect = auth.user.aspects.create(name: "new aspect")
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token_public_only,
            body:         "Hello this is a private post!",
            public:       false,
            aspects:      [aspect.id]
          }
        )

        confirm_api_error(response, 422, "Failed to create the post")
      end
    end

    context "with fully populated post" do
      it "creates with photos" do
        message_text = "Post with photos"

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            photos:       @user_photo_guids
          }
        )
        expect(response.status).to eq(200)
        post = response_body(response)

        @user_photo1[:pending] = true
        @user_photo1.save
        @user_photo2[:pending] = true
        @user_photo2.save
        base_params = {status_message: {text: message_text}, public: true}
        merged_params = base_params.merge(photos: @user_photo_ids)
        post_for_ref_only = StatusMessageCreationService.new(auth.user).create(merged_params)

        confirm_post_format(post, auth.user, post_for_ref_only)
        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end

      it "fails to add other's photos" do
        message_text = "Post with photos"

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            photos:       @alice_photo_guids
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails to add non-pending photos" do
        message_text = "Post with photos"

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            photos:       [@user_photo3.guid]
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails to add bad photo guids" do
        message_text = "Post with photos"

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            photos:       ["999_999_999"]
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "creates with poll" do
        message_text = "status with a poll"
        poll_params = {poll_question: "something?", poll_answers: %w[yes no maybe]}
        base_params = {status_message: {text: message_text}, public: true}
        merged_params = base_params.merge(poll_params)
        post_for_ref_only = StatusMessageCreationService.new(auth.user).create(merged_params)

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            poll:         {
              question:     "something?",
              poll_answers: %w[yes no maybe]
            }
          }
        )
        post = response_body(response)
        expect(response.status).to eq(200)
        confirm_post_format(post, auth.user, post_for_ref_only)
        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end

      it "fails poll with no answers" do
        message_text = "status with a poll"
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            poll:         {
              question:     "something?",
              poll_answers: []
            }
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails poll with blank answer" do
        message_text = "status with a poll"
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            poll:         {
              question:     "question",
              poll_answers: ["yes", ""]
            }
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails poll with blank question and message text" do
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         "",
            public:       true,
            poll:         {
              question:     "question",
              poll_answers: %w[yes no]
            }
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "creates with location" do
        message_text = "status with location"
        base_params = {status_message: {text: message_text}, public: true}
        location_params = {location_address: "somewhere", location_coords: "1,2"}
        merged_params = base_params.merge(location_params)
        post_for_ref_only = StatusMessageCreationService.new(auth.user).create(merged_params)

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true,
            location:     {
              address: "somewhere",
              lat:     1,
              lng:     2
            }
          }
        )
        post = response_body(response)
        expect(response.status).to eq(200)
        confirm_post_format(post, auth.user, post_for_ref_only)
        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end

      it "creates with mentions" do
        message_text = "hello @{#{alice.diaspora_handle}} from Bob!"
        post_for_ref_only = auth.user.post(
          :status_message,
          text:   message_text,
          public: true
        )

        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true
          }
        )
        post = response_body(response)
        expect(response.status).to eq(200)
        confirm_post_format(post, auth.user, post_for_ref_only, [alice])
      end
    end

    context "when given NSFW hashtag" do
      it "creates NSFW post" do
        message_text = "hello @{#{alice.diaspora_handle}} from Bob but this is #nsfw!"
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       true
          }
        )
        expect(response.status).to eq(200)
        post = response_body(response)
        expect(post["nsfw"]).to be_truthy
        expect_to_match_json_schema(post.to_json, "#/definitions/post")
      end
    end

    context "when given just photos" do
      it "creates the post" do
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            public:       true,
            photos:       @user_photo_guids
          }
        )
        expect(response.status).to eq(200)
        expect_to_match_json_schema(response.body, "#/definitions/post")
      end

      it "fails to add other's photos" do
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            public:       true,
            photos:       @alice_photo_guids
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails to add non-pending photos" do
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            public:       true,
            photos:       [@user_photo3.guid]
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails to add bad photo guids" do
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            public:       true,
            photos:       ["999_999_999"]
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end
    end

    context "when given bad post" do
      it "fails when no body" do
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            public:       true
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails when no public field and no aspects" do
        message_text = "hello @{#{alice.diaspora_handle}} from Bob!"
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails when private no aspects" do
        message_text = "hello @{#{alice.diaspora_handle}} from Bob!"
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       false
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails when unknown aspect IDs" do
        message_text = "hello @{#{alice.diaspora_handle}} from Bob!"
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            public:       false,
            aspects:      ["-1"]
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end

      it "fails when no public field but aspects" do
        aspect = auth.user.aspects.create(name: "new aspect")
        auth.user.share_with(alice.person, aspect)
        message_text = "hello @{#{alice.diaspora_handle}} from Bob!"
        post(
          api_v1_posts_path,
          params: {
            access_token: access_token,
            body:         message_text,
            aspects:      [aspect.id]
          }
        )
        confirm_api_error(response, 422, "Failed to create the post")
      end
    end

    context "improper credentials" do
      it "fails without modify token" do
        post(
          api_v1_posts_path,
          params: {
            access_token:   access_token_read_only,
            status_message: {text: "Hello this is a post!"},
            public:         true
          }
        )
        expect(response.status).to eq(403)
      end

      it "fails without invalid token" do
        post(
          api_v1_posts_path,
          params: {
            access_token:   invalid_token,
            status_message: {text: "Hello this is a post!"},
            public:         true
          }
        )
        expect(response.status).to eq(401)
      end
    end
  end

  describe "#destroy" do
    context "when given read-write access token" do
      it "attempts to destroy the post" do
        @status = auth.user.post(
          :status_message,
          text:   "hello",
          public: true,
          to:     "all"
        )
        delete(
          api_v1_post_path(@status.guid),
          params: {access_token: access_token}
        )
        expect(response.status).to eq(204)
      end
    end

    context "when given read only access token" do
      it "doesn't delete the post" do
        @status = auth.user.post(
          :status_message,
          text:   "hello",
          public: true
        )
        delete(
          api_v1_post_path(@status.guid),
          params: {access_token: access_token_read_only}
        )

        expect(response.status).to eq(403)
      end
    end

    context "when given invalid token" do
      it "doesn't delete the post" do
        @status = auth.user.post(
          :status_message,
          text:   "hello",
          public: true
        )
        delete(
          api_v1_post_path(@status.guid),
          params: {access_token: invalid_token}
        )

        expect(response.status).to eq(401)
      end
    end

    context "when post is private but no private:modify scope in token" do
      it "doesn't delete the post" do
        aspect = auth_public_only.user.aspects.create(name: "new aspect")
        @status = auth_public_only.user.post(
          :status_message,
          text:    "hello",
          aspects: [aspect.id]
        )
        delete(
          api_v1_post_path(@status.guid),
          params: {access_token: access_token_public_only}
        )

        expect(response.status).to eq(403)
      end
    end

    context "when given invalid Post ID" do
      it "doesn't delete a post" do
        delete(
          api_v1_post_path("999_999_999"),
          params: {access_token: access_token}
        )
        confirm_api_error(response, 404, "Post with provided guid could not be found")
      end
    end

    context "when PostID refers to another user's post" do
      it "fails to delete post" do
        status = alice.post(
          :status_message,
          text:   "hello",
          public: true,
          to:     "all"
        )

        delete(
          api_v1_post_path(status.guid),
          params: {access_token: access_token}
        )
        confirm_api_error(response, 403, "Not allowed to delete the post")
      end
    end
  end

  def response_body(response)
    JSON.parse(response.body)
  end

  private

  # rubocop:disable Metrics/AbcSize
  def confirm_post_format(post, user, reference_post, mentions=[])
    confirm_post_top_level(post, reference_post)
    confirm_person_format(post["author"], user)
    confirm_interactions(post["interaction_counters"], reference_post)
    confirm_own_interaction_state(post["own_interaction_state"], reference_post)

    mentions.each do |mention|
      post_mentions = post["mentioned_people"]
      post_mention = post_mentions.find {|m| m["guid"] == mention.guid }
      confirm_person_format(post_mention, mention)
    end

    confirm_poll(post["poll"], reference_post.poll, false) if reference_post.poll
    confirm_location(post["location"], reference_post.location) if reference_post.location
    confirm_photos(post["photos"], reference_post.photos) if reference_post.photos
    confirm_open_graph_object(post["open_graph_object"], reference_post.open_graph_cache)
    confirm_oembed(post["oembed"], reference_post.o_embed_cache)
  end

  def confirm_post_top_level(post, reference_post)
    expect(post.has_key?("guid")).to be_truthy
    expect(post.has_key?("created_at")).to be_truthy
    expect(post["created_at"]).not_to be_nil
    expect(post["title"]).to eq(reference_post.message.title)
    expect(post["body"]).to eq(reference_post.message.plain_text_for_json)
    expect(post["post_type"]).to eq(reference_post.post_type)
    expect(post["provider_display_name"]).to eq(reference_post.provider_display_name)
    expect(post["public"]).to eq(reference_post.public)
    expect(post["nsfw"]).to eq(!!reference_post.nsfw) # rubocop:disable Style/DoubleNegation
  end

  def confirm_interactions(interactions, reference_post)
    expect(interactions["comments"]).to eq(reference_post.comments_count)
    expect(interactions["likes"]).to eq(reference_post.likes_count)
    expect(interactions["reshares"]).to eq(reference_post.reshares_count)
  end

  def confirm_own_interaction_state(state, reference_post)
    expect(state["liked"]).to eq(reference_post.likes.where(author: auth.user.person).exists?)
    expect(state["reshared"]).to eq(reference_post.reshares.where(author: auth.user.person).exists?)
    expect(state["subscribed"]).to eq(reference_post.participations.where(author: auth.user.person).exists?)
    expect(state["reported"]).to eq(reference_post.reports.where(user: auth.user).exists?)
  end

  def confirm_person_format(post_person, user)
    expect(post_person["guid"]).to eq(user.guid)
    expect(post_person["diaspora_id"]).to eq(user.diaspora_handle)
    expect(post_person["name"]).to eq(user.name)
    expect(post_person["avatar"]).to eq(user.profile.image_url(size: :thumb_medium))
  end

  def confirm_poll(post_poll, ref_poll, expected_participation)
    return unless ref_poll

    expect(post_poll.has_key?("guid")).to be_truthy
    expect(post_poll["participation_count"]).to eq(ref_poll.participation_count)
    expect(post_poll["already_participated"]).to eq(expected_participation)
    expect(post_poll["question"]).to eq(ref_poll.question)

    answers = post_poll["poll_answers"]
    answers.each do |answer|
      actual_answer = ref_poll.poll_answers.find {|a| a[:answer] == answer["answer"] }
      expect(answer["answer"]).to eq(actual_answer[:answer])
      expect(answer["vote_count"]).to eq(actual_answer[:vote_count])
    end
  end

  def confirm_location(location, ref_location)
    expect(location["address"]).to eq(ref_location[:address])
    expect(location["lat"]).to eq(ref_location[:lat].to_f)
    expect(location["lng"]).to eq(ref_location[:lng].to_f)
  end

  def confirm_photos(photos, ref_photos)
    expect(photos.size).to eq(ref_photos.size)
    photos.each do |photo|
      expect(photo["dimensions"].has_key?("height")).to be_truthy
      expect(photo["dimensions"].has_key?("height")).to be_truthy
      expect(photo["sizes"]["small"]).to be_truthy
      expect(photo["sizes"]["medium"]).to be_truthy
      expect(photo["sizes"]["large"]).to be_truthy
    end
  end

  def confirm_open_graph_object(object, ref_cache)
    return unless ref_cache

    expect(object["type"]).to eq(ref_cache.ob_type)
    expect(object["url"]).to eq(ref_cache.url)
    expect(object["title"]).to eq(ref_cache.title)
    expect(object["image"]).to eq(ref_cache.image)
    expect(object["description"]).to eq(ref_cache.description)
    expect(object["video_url"]).to eq(ref_cache.video_url)
  end

  def confirm_oembed(response, ref_cache)
    return unless ref_cache

    expect(response).to eq(ref_cache.data)
    expect(response["trusted_endpoint_url"]).to_not be_nil
  end

  def confirm_reshare_format(post, root_post, root_poster)
    root = post["root"]
    expect(root.has_key?("guid")).to be_truthy
    expect(root["guid"]).to eq(root_post[:guid])
    expect(root.has_key?("created_at")).to be_truthy
    confirm_person_format(root["author"], root_poster)
  end
  # rubocop:enable Metrics/AbcSize
end
