# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::VerifyQuoteService do
  subject { described_class.new }

  let(:account) { Fabricate(:account, domain: 'a.example.com') }
  let(:quoted_account) { Fabricate(:account, domain: 'b.example.com') }
  let(:quoted_status) { Fabricate(:status, account: quoted_account) }
  let(:status) { Fabricate(:status, account: account) }
  let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri_record) }

  shared_examples 'common behavior' do
    context 'with an unfetchable approval URI' do
      let(:approval_uri) { 'https://b.example.com/approvals/1234' }

      before do
        stub_request(:get, approval_uri)
          .to_return(status: 404)
      end

      context 'with an already-fetched post' do
        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to change(quote, :state).to('rejected')
        end
      end

      context 'with an already-verified quote' do
        let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri_record, state: :accepted) }

        it 'rejects the quote' do
          expect { subject.call(quote, approval_uri_arg) }
            .to change(quote, :state).to('revoked')
        end
      end
    end

    context 'with an approval URI' do
      let(:approval_uri) { 'https://b.example.com/approvals/1234' }

      let(:approval_type) { 'QuoteAuthorization' }
      let(:approval_id) { approval_uri }
      let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(quoted_account) }
      let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(status) }
      let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(quoted_status) }

      let(:json) do
        {
          '@context': [
            'https://www.w3.org/ns/activitystreams',
            {
              QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
              gts: 'https://gotosocial.org/ns#',
              interactionPolicy: {
                '@id': 'gts:interactionPolicy',
                '@type': '@id',
              },
              interactingObject: {
                '@id': 'gts:interactingObject',
                '@type': '@id',
              },
              interactionTarget: {
                '@id': 'gts:interactionTarget',
                '@type': '@id',
              },
            },
          ],
          type: approval_type,
          id: approval_id,
          attributedTo: approval_attributed_to,
          interactingObject: approval_interacting_object,
          interactionTarget: approval_interaction_target,
        }.with_indifferent_access
      end

      before do
        stub_request(:get, approval_uri)
          .to_return(status: 200, body: json.to_json, headers: { 'Content-Type': 'application/activity+json' })
      end

      context 'with a valid activity for already-fetched posts' do
        it 'updates the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to change(quote, :state).to('accepted')

          expect(a_request(:get, approval_uri))
            .to have_been_made.once
        end
      end

      context 'with a valid activity for a post that cannot be fetched but is passed as fetched_quoted_object' do
        let(:quoted_status) { nil }

        let(:approval_interaction_target) { 'https://b.example.com/unknown-quoted' }
        let(:prefetched_object) do
          {
            '@context': 'https://www.w3.org/ns/activitystreams',
            type: 'Note',
            id: 'https://b.example.com/unknown-quoted',
            to: 'https://www.w3.org/ns/activitystreams#Public',
            attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account),
            content: 'previously unknown post',
          }.with_indifferent_access
        end

        before do
          stub_request(:get, 'https://b.example.com/unknown-quoted')
            .to_return(status: 404)
        end

        it 'updates the status' do
          expect { subject.call(quote, approval_uri_arg, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted', prefetched_quoted_object: prefetched_object) }
            .to change(quote, :state).to('accepted')

          expect(a_request(:get, approval_uri))
            .to have_been_made.once

          expect(quote.reload.quoted_status.content).to eq 'previously unknown post'
        end
      end

      context 'with a valid activity for a post that cannot be fetched but is inlined' do
        let(:quoted_status) { nil }

        let(:approval_interaction_target) do
          {
            type: 'Note',
            id: 'https://b.example.com/unknown-quoted',
            to: 'https://www.w3.org/ns/activitystreams#Public',
            attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account),
            content: 'previously unknown post',
          }
        end

        before do
          stub_request(:get, 'https://b.example.com/unknown-quoted')
            .to_return(status: 404)
        end

        it 'updates the status' do
          expect { subject.call(quote, approval_uri_arg, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted') }
            .to change(quote, :state).to('accepted')

          expect(a_request(:get, approval_uri))
            .to have_been_made.once

          expect(quote.reload.quoted_status.content).to eq 'previously unknown post'
        end
      end

      context 'with a valid activity for a post that cannot be fetched and is inlined from an untrusted source' do
        let(:quoted_status) { nil }

        let(:approval_interaction_target) do
          {
            type: 'Note',
            id: 'https://example.com/unknown-quoted',
            to: 'https://www.w3.org/ns/activitystreams#Public',
            attributedTo: ActivityPub::TagManager.instance.uri_for(account),
            content: 'previously unknown post',
          }
        end

        before do
          stub_request(:get, 'https://example.com/unknown-quoted')
            .to_return(status: 404)
        end

        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg, fetchable_quoted_uri: 'https://example.com/unknown-quoted') }
            .to not_change(quote, :state)
            .and not_change(quote, :quoted_status)

          expect(a_request(:get, approval_uri))
            .to have_been_made.once
        end
      end

      context 'with a valid activity for already-fetched posts, with a pre-fetched approval' do
        it 'updates the status without fetching the activity' do
          expect { subject.call(quote, approval_uri_arg, prefetched_approval: JSON.generate(json)) }
            .to change(quote, :state).to('accepted')

          expect(a_request(:get, approval_uri))
            .to_not have_been_made
        end
      end

      context 'with an unverifiable approval' do
        let(:approval_uri) { 'https://evil.com/approvals/1234' }

        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to_not change(quote, :state)
        end
      end

      context 'with an invalid approval document because of a mismatched ID' do
        let(:approval_id) { 'https://evil.com/approvals/1234' }

        it 'does not accept the quote' do
          # NOTE: maybe we want to skip that instead of rejecting it?
          expect { subject.call(quote, approval_uri_arg) }
            .to change(quote, :state).to('rejected')
        end
      end

      context 'with an approval from the wrong account' do
        let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(Fabricate(:account, domain: 'b.example.com')) }

        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to_not change(quote, :state)
        end
      end

      context 'with an approval for the wrong quoted post' do
        let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: quoted_account)) }

        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to_not change(quote, :state)
        end
      end

      context 'with an approval for the wrong quote post' do
        let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: account)) }

        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to_not change(quote, :state)
        end
      end

      context 'with an approval of the wrong type' do
        let(:approval_type) { 'ReplyAuthorization' }

        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to_not change(quote, :state)
        end
      end
    end

    context 'with fast-track authorizations' do
      let(:approval_uri) { nil }

      context 'without any fast-track condition' do
        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to_not change(quote, :state)
        end
      end

      context 'when the account and the quoted account are the same' do
        let(:quoted_account) { account }

        it 'updates the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to change(quote, :state).to('accepted')
        end
      end

      context 'when the account is mentioned by the quoted post' do
        before do
          quoted_status.mentions << Mention.new(account: account)
        end

        it 'does not update the status' do
          expect { subject.call(quote, approval_uri_arg) }
            .to_not change(quote, :state).from('pending')
        end
      end
    end
  end

  context 'when approval URI is passed as argument' do
    let(:approval_uri_arg) { approval_uri }
    let(:approval_uri_record) { nil }

    it_behaves_like 'common behavior'
  end

  context 'when approval URI is stored in the record (legacy)' do
    let(:approval_uri_arg) { nil }
    let(:approval_uri_record) { approval_uri }

    it_behaves_like 'common behavior'
  end
end
