# frozen_string_literal: true

require "rate_limiter"

RSpec.describe RateLimiter do
  fab!(:user)
  fab!(:admin)
  let(:rate_limiter) { RateLimiter.new(user, "peppermint-butler", 2, 60) }
  let(:apply_staff_rate_limiter) do
    RateLimiter.new(admin, "peppermint-servant", 5, 40, apply_limit_to_staff: true)
  end
  let(:staff_rate_limiter) do
    RateLimiter.new(user, "peppermind-servant", 5, 40, staff_limit: { max: 10, secs: 80 })
  end
  let(:admin_staff_rate_limiter) do
    RateLimiter.new(admin, "peppermind-servant", 5, 40, staff_limit: { max: 10, secs: 80 })
  end

  describe "disabled" do
    before do
      rate_limiter.performed!
      rate_limiter.performed!
    end

    it "should be disabled" do
      expect(RateLimiter.disabled?).to eq(true)
    end

    it "returns true for can_perform?" do
      expect(rate_limiter.can_perform?).to eq(true)
    end

    it "doesn't raise an error on performed!" do
      expect { rate_limiter.performed! }.not_to raise_error
    end
  end

  describe "enabled" do
    before do
      RateLimiter.enable
      rate_limiter.clear!
      staff_rate_limiter.clear!
      admin_staff_rate_limiter.clear!
    end

    context "with aggressive rate limiter" do
      it "can operate correctly and totally stop limiting" do
        freeze_time

        # 2 requests every 30 seconds
        limiter = RateLimiter.new(nil, "test", 2, 30, global: true, aggressive: true)
        limiter.clear!

        limiter.performed!
        limiter.performed!
        freeze_time 29.seconds.from_now

        expect do limiter.performed! end.to raise_error(RateLimiter::LimitExceeded)

        expect do limiter.performed! end.to raise_error(RateLimiter::LimitExceeded)

        # in aggressive mode both these ^^^ count as an attempt
        freeze_time 29.seconds.from_now

        expect do limiter.performed! end.to raise_error(RateLimiter::LimitExceeded)

        expect do limiter.performed! end.to raise_error(RateLimiter::LimitExceeded)

        freeze_time 30.seconds.from_now

        expect { limiter.performed! }.not_to raise_error
        expect { limiter.performed! }.not_to raise_error
      end
    end

    context "with global rate limiter" do
      it "can operate in global mode" do
        limiter = RateLimiter.new(nil, "test", 2, 30, global: true)
        limiter.clear!

        thrown = false

        limiter.performed!
        limiter.performed!
        begin
          limiter.performed!
        rescue RateLimiter::LimitExceeded => e
          expect(Integer === e.available_in).to eq(true)
          expect(e.available_in).to be > 28
          expect(e.available_in).to be < 32
          thrown = true
        end
        expect(thrown).to be(true)
      end
    end

    context "when handling readonly" do
      before do
        # random IP address in the ULA range that does not exist
        Discourse.redis.without_namespace.slaveof "fdec:3f5d:d0b7:4c4b:472b:636a:4370:7ac5", "49999"
      end

      after { Discourse.redis.without_namespace.slaveof "no", "one" }

      it "does not explode" do
        expect { rate_limiter.performed! }.not_to raise_error
      end
    end

    context "when never done" do
      it "should perform right away" do
        expect(rate_limiter.can_perform?).to eq(true)
      end

      it "performs without an error" do
        expect { rate_limiter.performed! }.not_to raise_error
      end
    end

    context "when remaining" do
      it "updates correctly" do
        expect(rate_limiter.remaining).to eq(2)
        rate_limiter.performed!
        expect(rate_limiter.remaining).to eq(1)
        rate_limiter.performed!
        expect(rate_limiter.remaining).to eq(0)
      end
    end

    context "when max is less than or equal to zero" do
      it "should raise the right error" do
        [-1, 0, nil].each do |max|
          expect do RateLimiter.new(user, "a", max, 60).performed! end.to raise_error(
            RateLimiter::LimitExceeded,
          )
        end
      end
    end

    context "with multiple calls" do
      before do
        freeze_time
        rate_limiter.performed!
        rate_limiter.performed!
      end

      it "returns false for can_perform when the limit has been hit" do
        expect(rate_limiter.can_perform?).to eq(false)
        expect(rate_limiter.remaining).to eq(0)
      end

      it "raises an error the third time called" do
        expect { rate_limiter.performed! }.to raise_error do |error|
          expect(error).to be_a(RateLimiter::LimitExceeded)
          expect(error).to having_attributes(available_in: 60)
        end
      end

      it "raises no error when the sliding window ended" do
        freeze_time 60.seconds.from_now
        expect { rate_limiter.performed! }.not_to raise_error
      end

      context "as an admin/moderator" do
        it "returns true for can_perform if the user is an admin" do
          user.admin = true
          expect(rate_limiter.can_perform?).to eq(true)
          expect(rate_limiter.remaining).to eq(2)
        end

        it "doesn't raise an error when an admin performs the task" do
          user.admin = true
          expect { rate_limiter.performed! }.not_to raise_error
        end

        it "returns true for can_perform if the user is a mod" do
          user.moderator = true
          expect(rate_limiter.can_perform?).to eq(true)
        end

        it "doesn't raise an error when a moderator performs the task" do
          user.moderator = true
          expect { rate_limiter.performed! }.not_to raise_error
        end

        it "applies max / secs to staff when apply_limit_to_staff flag is true" do
          5.times { apply_staff_rate_limiter.performed! }
          freeze_time 10.seconds.from_now
          expect { apply_staff_rate_limiter.performed! }.to raise_error do |error|
            expect(error).to be_a(RateLimiter::LimitExceeded)
            expect(error).to having_attributes(available_in: 30)
          end
        end

        it "applies staff_limit max when present for staff" do
          expect(admin_staff_rate_limiter.can_perform?).to eq(true)
          expect(admin_staff_rate_limiter.remaining).to eq(10)
        end

        it "applies staff_limit secs when present for staff" do
          10.times { admin_staff_rate_limiter.performed! }
          freeze_time 10.seconds.from_now
          expect { admin_staff_rate_limiter.performed! }.to raise_error do |error|
            expect(error).to be_a(RateLimiter::LimitExceeded)
            expect(error).to having_attributes(available_in: 70)
          end
        end

        it "applies standard max to non-staff users when staff_limit values are present" do
          expect(staff_rate_limiter.can_perform?).to eq(true)
          expect(staff_rate_limiter.remaining).to eq(5)
        end
      end

      describe "#rollback!" do
        before { rate_limiter.rollback! }

        it "returns true for can_perform since there is now room" do
          expect(rate_limiter.can_perform?).to eq(true)
        end

        it "raises no error now that there is room" do
          expect { rate_limiter.performed! }.not_to raise_error
        end
      end
    end
  end
end