require "spec_helper"

RSpec.describe "Rollout" do
  before do
    @redis   = Redis.new
    @rollout = Rollout.new(@redis)
  end

  describe "when a group is activated" do
    before do
      @rollout.define_group(:fivesonly) { |user| user.id == 5 }
      @rollout.activate_group(:chat, :fivesonly)
    end

    it "the feature is active for users for which the block evaluates to true" do
      expect(@rollout).to be_active(:chat, double(id: 5))
    end

    it "is not active for users for which the block evaluates to false" do
      expect(@rollout).not_to be_active(:chat, double(id: 1))
    end

    it "is not active if a group is found in Redis but not defined in Rollout" do
      @rollout.activate_group(:chat, :fake)
      expect(@rollout).not_to be_active(:chat, double(id: 1))
    end
  end

  describe "a non-user group" do
    before do
      @rollout.define_group(:arbitrary_group) { result }
      @rollout.activate_group(:chat, :arbitrary_group)
    end

    context "when the result is true" do
      let(:result) { true }

      it "the feature is active" do
        expect(@rollout).to be_active(:chat)
      end
    end

    context "when the result is false" do
      let(:result) { false }

      it "the feature is active" do
        expect(@rollout).not_to be_active(:chat)
      end
    end
  end

  describe "the default all group" do
    before do
      @rollout.activate_group(:chat, :all)
    end

    it "evaluates to true no matter what" do
      expect(@rollout).to be_active(:chat, double(id: 0))
    end
  end

  describe "deactivating a group" do
    before do
      @rollout.define_group(:fivesonly) { |user| user.id == 5 }
      @rollout.activate_group(:chat, :all)
      @rollout.activate_group(:chat, :some)
      @rollout.activate_group(:chat, :fivesonly)
      @rollout.deactivate_group(:chat, :all)
      @rollout.deactivate_group(:chat, "some")
    end

    it "deactivates the rules for that group" do
      expect(@rollout).not_to be_active(:chat, double(id: 10))
    end

    it "leaves the other groups active" do
      expect(@rollout.get(:chat).groups).to eq [:fivesonly]
    end

    it "leaves the other groups active using sets" do
      @options = @rollout.instance_variable_get("@options")
      @options[:use_sets] = true
      expect(@rollout.get(:chat).groups).to eq [:fivesonly].to_set
    end
  end

  describe "deactivating a feature completely" do
    before do
      @rollout.define_group(:fivesonly) { |user| user.id == 5 }
      @rollout.activate_group(:chat, :all)
      @rollout.activate_group(:chat, :fivesonly)
      @rollout.activate_user(:chat, double(id: 51))
      @rollout.activate_percentage(:chat, 100)
      @rollout.activate(:chat)
      @rollout.deactivate(:chat)
    end

    it "removes all of the groups" do
      expect(@rollout).not_to be_active(:chat, double(id: 0))
    end

    it "removes all of the users" do
      expect(@rollout).not_to be_active(:chat, double(id: 51))
    end

    it "removes the percentage" do
      expect(@rollout).not_to be_active(:chat, double(id: 24))
    end

    it "removes globally" do
      expect(@rollout).not_to be_active(:chat)
    end
  end

  describe "activating a specific user" do
    before do
      @rollout.activate_user(:chat, double(id: 42))
    end

    it "is active for that user" do
      expect(@rollout).to be_active(:chat, double(id: 42))
    end

    it "remains inactive for other users" do
      expect(@rollout).not_to be_active(:chat, double(id: 24))
    end
  end

  describe "activating a specific user by ID" do
    before do
      @rollout.activate_user(:chat, 42)
    end

    it "is active for that user" do
      expect(@rollout).to be_active(:chat, double(id: 42))
    end

    it "remains inactive for other users" do
      expect(@rollout).not_to be_active(:chat, double(id: 24))
    end
  end

  describe "activating a specific user with a string id" do
    before do
      @rollout.activate_user(:chat, double(id: "user-72"))
    end

    it "is active for that user" do
      expect(@rollout).to be_active(:chat, double(id: "user-72"))
    end

    it "remains inactive for other users" do
      expect(@rollout).not_to be_active(:chat, double(id: "user-12"))
    end
  end

  describe "activating a group of users" do
    context "specified by user objects" do
      let(:users) { [double(id: 1), double(id: 2), double(id: 3)] }

      before { @rollout.activate_users(:chat, users) }

      it "is active for the given users" do
        users.each { |user| expect(@rollout).to be_active(:chat, user) }
      end

      it "remains inactive for other users" do
        expect(@rollout).not_to be_active(:chat, double(id: 4))
      end
    end

    context "specified by user ids" do
      let(:users) { [1, 2, 3] }

      before { @rollout.activate_users(:chat, users) }

      it "is active for the given users" do
        users.each { |user| expect(@rollout).to be_active(:chat, user) }
      end

      it "remains inactive for other users" do
        expect(@rollout).not_to be_active(:chat, 4)
      end
    end
  end

  describe "deactivating a specific user" do
    before do
      @rollout.activate_user(:chat, double(id: 42))
      @rollout.activate_user(:chat, double(id: 4242))
      @rollout.activate_user(:chat, double(id: 24))
      @rollout.deactivate_user(:chat, double(id: 42))
      @rollout.deactivate_user(:chat, double(id: "4242"))
    end

    it "that user should no longer be active" do
      expect(@rollout).not_to be_active(:chat, double(id: 42))
    end

    it "remains active for other active users" do
      @options = @rollout.instance_variable_get("@options")
      @options[:use_sets] = false
      expect(@rollout.get(:chat).users).to eq %w(24)
    end

    it "remains active for other active users using sets" do
      @options = @rollout.instance_variable_get("@options")
      @options[:use_sets] = true

      expect(@rollout.get(:chat).users).to eq %w(24).to_set
    end
  end

  describe "deactivating a group of users" do
    context "specified by user objects" do
      let(:active_users) { [double(id: 1), double(id: 2)] }
      let(:inactive_users) { [double(id: 3), double(id: 4)] }

      before do
        @rollout.activate_users(:chat, active_users + inactive_users)
        @rollout.deactivate_users(:chat, inactive_users)
      end

      it "is active for the active users" do
        active_users.each { |user| expect(@rollout).to be_active(:chat, user) }
      end

      it "is not active for inactive users" do
        inactive_users.each { |user| expect(@rollout).not_to be_active(:chat, user) }
      end
    end

    context "specified by user ids" do
      let(:active_users) { [1, 2] }
      let(:inactive_users) { [3, 4] }

      before do
        @rollout.activate_users(:chat, active_users + inactive_users)
        @rollout.deactivate_users(:chat, inactive_users)
      end

      it "is active for the active users" do
        active_users.each { |user| expect(@rollout).to be_active(:chat, user) }
      end

      it "is not active for inactive users" do
        inactive_users.each { |user| expect(@rollout).not_to be_active(:chat, user) }
      end
    end
  end

  describe "activating a feature globally" do
    before do
      @rollout.activate(:chat)
    end

    it "activates the feature" do
      expect(@rollout).to be_active(:chat)
    end

    it "sets @data to empty hash" do
      expect(@rollout.get(:chat).data).to eq({})
    end
  end

  describe "activating a feature for a percentage of users" do
    before do
      @rollout.activate_percentage(:chat, 20)
    end

    it "activates the feature for that percentage of the users" do
      expect((1..100).select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(20)
    end
  end

  describe "activating a feature for a percentage of users" do
    before do
      @rollout.activate_percentage(:chat, 20)
    end

    it "activates the feature for that percentage of the users" do
      expect((1..200).select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(4).of(40)
    end
  end

  describe "activating a feature for a percentage of users" do
    before do
      @rollout.activate_percentage(:chat, 5)
    end

    it "activates the feature for that percentage of the users" do
      expect((1..100).select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(5)
    end
  end

  describe "activating a feature for a percentage of users" do
    before do
      @rollout.activate_percentage(:chat, 0.1)
    end

    it "activates the feature for that percentage of the users" do
      expect((1..10_000).to_set.select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(10)
    end
  end

  describe "activating a feature for a percentage of users" do
    before do
      @rollout.activate_percentage(:chat, 20)
      @rollout.activate_percentage(:beta, 20)
      @options = @rollout.instance_variable_get("@options")
    end

    it "activates the feature for a random set of users when opt is set" do
      @options[:randomize_percentage] = true
      chat_users = (1..100).select { |id| @rollout.active?(:chat, double(id: id)) }
      beta_users = (1..100).select { |id| @rollout.active?(:beta, double(id: id)) }
      expect(chat_users).not_to eq beta_users
    end
    it "activates the feature for the same set of users when opt is not set" do
      @options[:randomize_percentage] = false
      chat_users = (1..100).select { |id| @rollout.active?(:chat, double(id: id)) }
      beta_users = (1..100).select { |id| @rollout.active?(:beta, double(id: id)) }
      expect(chat_users).to eq beta_users
    end
  end

  describe "activating a feature for a group as a string" do
    before do
      @rollout.define_group(:admins) { |user| user.id == 5 }
      @rollout.activate_group(:chat, "admins")
    end

    it "the feature is active for users for which the block evaluates to true" do
      expect(@rollout).to be_active(:chat, double(id: 5))
    end

    it "is not active for users for which the block evaluates to false" do
      expect(@rollout).not_to be_active(:chat, double(id: 1))
    end
  end

  describe "deactivating the percentage of users" do
    before do
      @rollout.activate_percentage(:chat, 100)
      @rollout.deactivate_percentage(:chat)
    end

    it "becomes inactivate for all users" do
      expect(@rollout).not_to be_active(:chat, double(id: 24))
    end
  end

  describe "deactivating the feature globally" do
    before do
      @rollout.activate(:chat)
      @rollout.deactivate(:chat)
    end

    it "becomes inactivate" do
      expect(@rollout).not_to be_active(:chat)
    end
  end

  describe "setting a feature on" do
    before do
      @rollout.set(:chat, true)
    end

    it "becomes activated" do
      expect(@rollout).to be_active(:chat)
    end
  end

  describe "setting a feature off" do
    before do
      @rollout.set(:chat, false)
    end

    it "becomes inactivated" do
      expect(@rollout).not_to be_active(:chat)
    end
  end

  describe "deleting a feature" do
    before do
      @rollout.set(:chat, true)
    end

    context "when feature was passed as string" do
      it "should be removed from features list" do
        expect(@rollout.features.size).to eq 1
        @rollout.delete('chat')
        expect(@rollout.features.size).to eq 0
      end
    end

    it "should be removed from features list" do
      expect(@rollout.features.size).to eq 1
      @rollout.delete(:chat)
      expect(@rollout.features.size).to eq 0
    end

    it "should have metadata cleared" do
      expect(@rollout.get(:chat).percentage).to eq 100
      @rollout.delete(:chat)
      expect(@rollout.get(:chat).percentage).to eq 0
    end
  end

  describe "keeps a list of features" do
    it "saves the feature" do
      @rollout.activate(:chat)
      expect(@rollout.features).to be_include(:chat)
    end

    it "does not contain doubles" do
      @rollout.activate(:chat)
      @rollout.activate(:chat)
      expect(@rollout.features.size).to eq(1)
    end

    it "does not contain doubles when using string" do
      @rollout.activate(:chat)
      @rollout.activate("chat")
      expect(@rollout.features.size).to eq(1)
    end
  end

  describe "#get" do
    before do
      @rollout.activate_percentage(:chat, 10)
      @rollout.activate_group(:chat, :caretakers)
      @rollout.activate_group(:chat, :greeters)
      @rollout.activate(:signup)
      @rollout.activate_user(:chat, double(id: 42))
    end

    it "returns the feature object" do
      feature = @rollout.get(:chat)
      expect(feature.groups).to eq [:caretakers, :greeters]
      expect(feature.percentage).to eq 10
      expect(feature.users).to eq %w(42)
      expect(feature.to_hash).to eq(
        groups: [:caretakers, :greeters],
        percentage: 10,
        users: %w(42)
      )

      feature = @rollout.get(:signup)
      expect(feature.groups).to be_empty
      expect(feature.users).to be_empty
      expect(feature.percentage).to eq(100)
    end

    it "returns the feature objects using sets" do
      @options = @rollout.instance_variable_get("@options")
      @options[:use_sets] = true

      feature = @rollout.get(:chat)
      expect(feature.groups).to eq [:caretakers, :greeters].to_set
      expect(feature.percentage).to eq 10
      expect(feature.users).to eq %w(42).to_set
      expect(feature.to_hash).to eq(
        groups: [:caretakers, :greeters].to_set,
        percentage: 10,
        users: %w(42).to_set
      )

      feature = @rollout.get(:signup)
      expect(feature.groups).to be_empty
      expect(feature.users).to be_empty
      expect(feature.percentage).to eq(100)
    end
  end

  describe "#clear" do
    let(:features) { %w(signup beta alpha gm) }

    before do
      features.each { |f| @rollout.activate(f) }

      @rollout.clear!
    end

    it "each feature is cleared" do
      features.each do |feature|
        expect(@rollout.get(feature).to_hash).to eq(
          percentage: 0,
          users: [],
          groups: []
        )
      end
    end

    it "each feature is cleared with sets" do
      @options = @rollout.instance_variable_get("@options")
      @options[:use_sets] = true
      features.each do |feature|
        expect(@rollout.get(feature).to_hash).to eq(
          percentage: 0,
          users: Set.new,
          groups: Set.new
        )
      end
    end

    it "removes all features" do
      expect(@rollout.features).to be_empty
    end
  end

  describe "#feature_states" do
    let(:user_double) { double(id: 7) }

    before do
      @rollout.activate(:chat)
      @rollout.activate_user(:video, user_double)
      @rollout.deactivate(:vr)
    end

    it "returns a hash" do
      expect(@rollout.feature_states).to be_a(Hash)
    end

    context "with user argument" do
      it "maps active feature as true" do
        state = @rollout.feature_states(user_double)[:video]
        expect(state).to eq(true)
      end

      it "maps inactive feature as false" do
        state = @rollout.feature_states[:vr]
        expect(state).to eq(false)
      end
    end

    context "with no argument" do
      it "maps active feature as true" do
        state = @rollout.feature_states[:chat]
        expect(state).to eq(true)
      end

      it "maps inactive feature as false" do
        state = @rollout.feature_states[:video]
        expect(state).to eq(false)
      end
    end
  end

  describe "#active_features" do
    let(:user_double) { double(id: 19) }

    before do
      @rollout.activate(:chat)
      @rollout.activate_user(:video, user_double)
      @rollout.deactivate(:vr)
    end

    it "returns an array" do
      expect(@rollout.active_features).to be_a(Array)
    end

    context "with user argument" do
      it "includes active feature" do
        features = @rollout.active_features(user_double)
        expect(features).to include(:video)
        expect(features).to include(:chat)
      end

      it "excludes inactive feature" do
        features = @rollout.active_features(user_double)
        expect(features).to_not include(:vr)
      end
    end

    context "with no argument" do
      it "includes active feature" do
        features = @rollout.active_features
        expect(features).to include(:chat)
      end

      it "excludes inactive feature" do
        features = @rollout.active_features
        expect(features).to_not include(:video)
      end
    end
  end

  describe "#user_in_active_users?" do
    it "returns true if activated for user" do
      @rollout.activate_user(:chat, double(id: 5))
      expect(@rollout.user_in_active_users?(:chat, "5")).to eq(true)
    end

    it "returns false if activated for group" do
      @rollout.activate_group(:chat, :all)
      expect(@rollout.user_in_active_users?(:chat, "5")).to eq(false)
    end
  end

  describe "#set_feature_data" do
    before do
      @rollout.set_feature_data(:chat, description: 'foo', release_date: 'bar')
    end

    it 'sets the data attribute on feature' do
      expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
    end

    it 'updates a data attribute' do
      @rollout.set_feature_data(:chat, description: 'baz')
      expect(@rollout.get(:chat).data).to include('description' => 'baz', 'release_date' => 'bar')
    end

    it 'only sets data on specified feature' do
      @rollout.set_feature_data(:talk, image_url: 'kittens.png')
      expect(@rollout.get(:chat).data).not_to include('image_url' => 'kittens.png')
      expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
    end

    it 'does not modify @data if param is nil' do
      expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
      @rollout.set_feature_data(:chat, nil)
      expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
    end

    it 'does not modify @data if param is empty string' do
      expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
      @rollout.set_feature_data(:chat, "   ")
      expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
    end

    it 'properly parses data when it contains a |' do
      user = double("User", id: 8)
      @rollout.activate_user(:chat, user)
      @rollout.set_feature_data(:chat, "|call||text|" => "a|bunch|of|stuff")
      expect(@rollout.get(:chat).data).to include("|call||text|" => "a|bunch|of|stuff")
      expect(@rollout.active?(:chat, user)).to be true
    end
  end

  describe "#clear_feature_data" do
    it 'resets data to empty string' do
      @rollout.set_feature_data(:chat, description: 'foo')
      expect(@rollout.get(:chat).data).to include('description' => 'foo')
      @rollout.clear_feature_data(:chat)
      expect(@rollout.get(:chat).data).to eq({})
    end
  end
end

describe "Rollout::Feature" do
  describe "#add_user" do
    it "ids a user using id_user_by" do
      user    = double("User", email: "test@test.com")
      feature = Rollout::Feature.new(:chat, nil, id_user_by: :email)
      feature.add_user(user)
      expect(user).to have_received :email
    end
  end

  describe "#initialize" do
    describe "when string does not exist" do
      it 'clears feature attributes when string is not given' do
        feature = Rollout::Feature.new(:chat)
        expect(feature.groups).to be_empty
        expect(feature.users).to be_empty
        expect(feature.percentage).to eq 0
        expect(feature.data).to eq({})
      end

      it 'clears feature attributes when string is nil' do
        feature = Rollout::Feature.new(:chat, nil)
        expect(feature.groups).to be_empty
        expect(feature.users).to be_empty
        expect(feature.percentage).to eq 0
        expect(feature.data).to eq({})
      end

      it 'clears feature attributes when string is empty string' do
        feature = Rollout::Feature.new(:chat, "")
        expect(feature.groups).to be_empty
        expect(feature.users).to be_empty
        expect(feature.percentage).to eq 0
        expect(feature.data).to eq({})
      end

      describe "when there is no data" do
        it 'sets @data to empty hash' do
          feature = Rollout::Feature.new(:chat, "0||")
          expect(feature.data).to eq({})
        end

        it 'sets @data to empty hash' do
          feature = Rollout::Feature.new(:chat, "|||   ")
          expect(feature.data).to eq({})
        end
      end
    end
  end
end
