# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::ProcessFeaturedItemService do
  include RoutingHelper

  subject { described_class.new }

  let(:collection) { Fabricate(:remote_collection, uri: 'https://other.example.com/collection/1') }
  let(:position) { 3 }
  let(:account) { Fabricate(:remote_account, uri: 'https://example.com/actor/1') }
  let(:featured_object_uri) { account.uri }
  let(:feature_authorization_uri) { 'https://example.com/auth/1' }
  let(:featured_item_json) do
    {
      '@context' => 'https://www.w3.org/ns/activitystreams',
      'id' => 'https://other.example.com/featured_item/1',
      'type' => 'FeaturedItem',
      'featuredObject' => featured_object_uri,
      'featuredObjectType' => 'Person',
      'featureAuthorization' => feature_authorization_uri,
      'published' => '2026-04-16T01:00:00Z',
    }
  end
  let(:stubbed_service) do
    instance_double(ActivityPub::VerifyFeaturedItemService, call: true)
  end

  before do
    allow(ActivityPub::VerifyFeaturedItemService).to receive(:new).and_return(stubbed_service)
  end

  shared_examples 'non-matching URIs' do
    context "when the item's URI does not match the collection's" do
      let(:collection) { Fabricate(:remote_collection) }

      it 'does not create a collection item and returns `nil`' do
        expect do
          expect(subject.call(collection, object, position:)).to be_nil
        end.to_not change(CollectionItem, :count)
      end
    end

    context 'when the actor URI does not match the approval URI' do
      let(:feature_authorization_uri) { 'https://other.example.com/auth/1' }

      it 'does not create a collection item and returns `nil`' do
        expect do
          expect(subject.call(collection, object, position:)).to be_nil
        end.to_not change(CollectionItem, :count)
      end
    end
  end

  context 'when the collection item is inlined' do
    let(:object) { featured_item_json }

    it_behaves_like 'non-matching URIs'

    context 'when item does not yet exist' do
      context 'when a position is given' do
        it 'creates and verifies the item' do
          expect { subject.call(collection, object, position:) }.to change(collection.collection_items, :count).by(1)

          expect(stubbed_service).to have_received(:call)

          new_item = collection.collection_items.last
          expect(new_item.object_uri).to eq 'https://example.com/actor/1'
          expect(new_item.approval_uri).to be_nil
          expect(new_item.created_at).to eq Time.utc(2026, 4, 16, 1)
          expect(new_item.position).to eq 3
        end
      end

      context 'when no position is given' do
        it 'creates the item' do
          expect { subject.call(collection, object) }.to change(collection.collection_items, :count).by(1)
          new_item = collection.collection_items.last

          expect(new_item.position).to eq 1
        end
      end

      context "when the collection's item count is already at the limit" do
        before do
          stub_const('ActivityPub::ProcessFeaturedCollectionService::ITEMS_LIMIT', 3)
          Fabricate.times(3, :collection_item, collection:)
        end

        it 'does not create an item' do
          expect { subject.call(collection, object) }.to_not change(collection.collection_items, :count)
        end
      end
    end

    context 'when item exists' do
      let!(:collection_item) do
        Fabricate(:collection_item, collection:, uri: featured_item_json['id'], position: 2)
      end

      context 'when no position is given' do
        it 'does not change the position' do
          expect { subject.call(collection, object) }.to_not change(collection.collection_items, :count)

          expect(collection_item.reload.position).to eq 2
        end
      end

      context 'when a different position is given' do
        it 'updates the position' do
          expect { subject.call(collection, object, position:) }.to_not change(collection.collection_items, :count)

          expect(collection_item.reload.position).to eq 3
        end
      end
    end

    context 'when an item exists for a local featured account' do
      let!(:collection_item) do
        Fabricate(:collection_item, collection:, state: :accepted)
      end
      let(:featured_object_uri) { ActivityPub::TagManager.instance.uri_for(collection_item.account) }
      let(:feature_authorization_uri) { ap_account_feature_authorization_url(collection_item.account_id, collection_item) }

      it 'updates the URI of the existing record' do
        expect { subject.call(collection, object, position:) }.to_not change(collection.collection_items, :count)
        expect(collection_item.reload.uri).to eq 'https://other.example.com/featured_item/1'
      end
    end

    context 'when featured object is of an unsupported type' do
      let(:hashtag_json) do
        {
          'id' => 'https://example.com/hashtags/people',
          'type' => 'Hashtag',
          'name' => '#people',
        }
      end
      let(:featured_object_uri) { hashtag_json['id'] }

      before do
        stub_request(:get, featured_object_uri)
          .to_return_json(
            status: 200,
            body: hashtag_json,
            headers: { 'Content-Type' => 'application/activity+json' }
          )
      end

      it 'does not create a collection item and returns `nil`' do
        expect do
          expect(subject.call(collection, object, position:)).to be_nil
        end.to_not change(CollectionItem, :count)
      end
    end

    context 'when featured object cannot be fetched' do
      let(:hashtag_json) do
        {
          'id' => 'https://example.com/hashtags/people',
          'type' => 'Hashtag',
          'name' => '#people',
        }
      end
      let(:featured_object_uri) { hashtag_json['id'] }

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

      it 'does not create a collection item and returns `nil`' do
        expect do
          expect(subject.call(collection, object, position:)).to be_nil
        end.to_not change(CollectionItem, :count)
      end
    end
  end

  context 'when only the id of the collection item is given' do
    let(:object) { featured_item_json['id'] }
    let(:featured_item_request) do
      stub_request(:get, object)
        .to_return_json(
          status: 200,
          body: featured_item_json,
          headers: { 'Content-Type' => 'application/activity+json' }
        )
    end

    before do
      featured_item_request
    end

    it_behaves_like 'non-matching URIs'

    it 'fetches the collection item' do
      expect { subject.call(collection, object, position:) }.to change(collection.collection_items, :count).by(1)

      expect(featured_item_request).to have_been_requested

      new_item = collection.collection_items.last
      expect(new_item.object_uri).to eq 'https://example.com/actor/1'
      expect(new_item.approval_uri).to be_nil
    end
  end
end
