RSpec.describe Flipper::Gates::PercentageOfActors do
  let(:feature_name) { :search }

  subject do
    described_class.new
  end

  def context(percentage_of_actors_value, feature = feature_name, actors = nil)
    Flipper::FeatureCheckContext.new(
      feature_name: feature,
      values: Flipper::GateValues.new(percentage_of_actors: percentage_of_actors_value),
      actors: Array(actors) || [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
    )
  end

  describe '#open?' do
    context 'when compared against two features' do
      let(:percentage) { 0.05 }
      let(:percentage_as_integer) { percentage * 100 }
      let(:number_of_actors) { 10_000 }

      let(:actors) do
        (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new(n.to_s)) }
      end

      let(:feature_one_enabled_actors) do
        actors.select { |actor| subject.open? context(percentage_as_integer, :name_one, actor) }
      end

      let(:feature_two_enabled_actors) do
        actors.select { |actor| subject.open? context(percentage_as_integer, :name_two, actor) }
      end

      it 'does not enable both features for same set of actors' do
        expect(feature_one_enabled_actors).not_to eq(feature_two_enabled_actors)
      end

      it 'enables feature for accurate number of actors for each feature' do
        margin_of_error = 0.02 * number_of_actors # 2 percent margin of error
        expected_enabled_size = number_of_actors * percentage

        [
          feature_one_enabled_actors.size,
          feature_two_enabled_actors.size,
        ].each do |actual_enabled_size|
          expect(actual_enabled_size).to be_within(margin_of_error).of(expected_enabled_size)
        end
      end
    end

    context "with an array of actors" do
      let(:percentage) { 0.05 }
      let(:percentage_as_integer) { percentage * 100 }
      let(:number_of_actors) { 3_000 }

      let(:user_actors) do
        (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("User;#{n}")) }
      end

      let(:team_actors) do
        (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("Team;#{n}")) }
      end

      let(:org_actors) do
        (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("Org;#{n}")) }
      end

      let(:actors) { user_actors + team_actors + org_actors }

      let(:feature_one_enabled_actors) do
        actors.each_slice(3).select do |group|
          context = context(percentage_as_integer, :name_one, group)
          subject.open?(context)
        end.flatten
      end

      let(:feature_two_enabled_actors) do
        actors.each_slice(3).select do |group|
          context = context(percentage_as_integer, :name_two, group)
          subject.open?(context)
        end.flatten
      end

      it 'does not enable both features for same set of actors' do
        expect(feature_one_enabled_actors).not_to eq(feature_two_enabled_actors)
      end

      it 'enables feature for accurate number of actors for each feature' do
        margin_of_error = 0.02 * actors.size # 2 percent margin of error
        expected_enabled_size = actors.size * percentage

        [
          feature_one_enabled_actors.size,
          feature_two_enabled_actors.size,
        ].each do |size|
          expect(size).to be_within(margin_of_error).of(expected_enabled_size)
        end
      end

      it "is consistent regardless of order of actors" do
        actors = user_actors.first(10)
        results = 100.times.map { |n| subject.open?(context(75, :some_feature, actors.shuffle)) }
        expect(results.uniq).to eq([true])
      end
    end

    context 'for fractional percentage' do
      let(:decimal) { 0.001 }
      let(:percentage) { decimal * 100 }
      let(:number_of_actors) { 10_000 }

      let(:actors) do
        (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new(n.to_s)) }
      end

      subject { described_class.new }

      it 'enables feature for accurate number of actors' do
        margin_of_error = 0.02 * number_of_actors
        expected_open_count = number_of_actors * decimal

        open_count = actors.select do |actor|
          context = context(percentage, :feature, [actor])
          subject.open?(context)
        end.size

        expect(open_count).to be_within(margin_of_error).of(expected_open_count)
      end
    end
  end
end
