# frozen_string_literal: true

require_relative "helper"
require "sidekiq/cli"

describe Sidekiq::CLI do
  describe "#parse" do
    before do
      Sidekiq.reset!
      @logger = Sidekiq.logger
      @logdev = StringIO.new
      Sidekiq.logger = Logger.new(@logdev)
      @config = Sidekiq
    end

    attr_reader :config

    after do
      Sidekiq.logger = @logger
    end

    subject do
      Sidekiq::CLI.new.tap { |c| c.config = config }
    end

    def logdev
      @logdev ||= StringIO.new
    end

    describe "#parse" do
      describe "options" do
        describe "require" do
          it "accepts with -r" do
            subject.parse(%w[sidekiq -r ./test/fake_env.rb])

            assert_equal "./test/fake_env.rb", config[:require]
          end
        end

        describe "concurrency" do
          it "accepts with -c" do
            subject.parse(%w[sidekiq -c 60 -r ./test/fake_env.rb])

            assert_equal 60, config[:concurrency]
          end

          describe "when concurrency is empty and RAILS_MAX_THREADS env var is set" do
            before do
              ENV["RAILS_MAX_THREADS"] = "9"
            end

            after do
              ENV.delete("RAILS_MAX_THREADS")
            end

            it "sets concurrency from RAILS_MAX_THREADS env var" do
              subject.parse(%w[sidekiq -r ./test/fake_env.rb])

              assert_equal 9, config[:concurrency]
            end

            it "option overrides RAILS_MAX_THREADS env var" do
              subject.parse(%w[sidekiq -c 60 -r ./test/fake_env.rb])

              assert_equal 60, config[:concurrency]
            end
          end
        end

        describe "setting internal options via the config file" do
          describe "setting the `strict` option via the config file" do
            it "discards the `strict` option specified via the config file" do
              subject.parse(%w[sidekiq -C ./test/config_with_internal_options.yml])

              assert_equal true, !!config[:strict]
            end
          end
        end

        describe "queues" do
          it "accepts with -q" do
            subject.parse(%w[sidekiq -q foo -r ./test/fake_env.rb])

            assert_equal ["foo"], config[:queues]
          end

          describe "when weights are not present" do
            it "accepts queues without weights" do
              subject.parse(%w[sidekiq -q foo -q bar -r ./test/fake_env.rb])

              assert_equal ["foo", "bar"], config[:queues]
            end

            it "sets strictly ordered queues" do
              subject.parse(%w[sidekiq -q foo -q bar -r ./test/fake_env.rb])

              assert_equal true, !!config[:strict]
            end
          end

          describe "when weights are present" do
            it "accepts queues with weights" do
              subject.parse(%w[sidekiq -q foo,3 -q bar -r ./test/fake_env.rb])

              assert_equal ["foo", "foo", "foo", "bar"], config[:queues]
            end

            it "does not set strictly ordered queues" do
              subject.parse(%w[sidekiq -q foo,3 -q bar -r ./test/fake_env.rb])

              assert_equal false, !!config[:strict]
            end
          end

          it "accepts queues with multi-word names" do
            subject.parse(%w[sidekiq -q queue_one -q queue-two -r ./test/fake_env.rb])

            assert_equal ["queue_one", "queue-two"], config[:queues]
          end

          it "accepts queues with dots in the name" do
            subject.parse(%w[sidekiq -q foo.bar -r ./test/fake_env.rb])

            assert_equal ["foo.bar"], config[:queues]
          end

          describe "when duplicate queue names" do
            it "raises an argument error" do
              assert_raises(ArgumentError) { subject.parse(%w[sidekiq -q foo -q foo -r ./test/fake_env.rb]) }
              assert_raises(ArgumentError) { subject.parse(%w[sidekiq -q foo,3 -q foo,1 -r ./test/fake_env.rb]) }
            end
          end

          describe "when queues are empty" do
            describe "when no queues are specified via -q" do
              it "sets 'default' queue" do
                subject.parse(%w[sidekiq -r ./test/fake_env.rb])

                assert_equal ["default"], config[:queues]
              end
            end

            describe "when no queues are specified via the config file" do
              it "sets 'default' queue" do
                subject.parse(%w[sidekiq -C ./test/config_empty.yml -r ./test/fake_env.rb])

                assert_equal ["default"], config[:queues]
              end
            end
          end
        end

        describe "timeout" do
          it "accepts with -t" do
            subject.parse(%w[sidekiq -t 30 -r ./test/fake_env.rb])

            assert_equal 30, config[:timeout]
          end
        end

        describe "verbose" do
          it "accepts with -v" do
            subject.parse(%w[sidekiq -v -r ./test/fake_env.rb])

            assert_equal Logger::DEBUG, Sidekiq.logger.level
          end
        end

        describe "config file" do
          it "accepts with -C" do
            subject.parse(%w[sidekiq -C ./test/config.yml])

            assert_equal "./test/config.yml", config[:config_file]
            refute config[:verbose]
            assert_equal "./test/fake_env.rb", config[:require]
            assert_nil config[:environment]
            assert_equal 50, config[:concurrency]
            assert_equal 2, config[:queues].count { |q| q == "very_often" }
            assert_equal 1, config[:queues].count { |q| q == "seldom" }
          end

          it "accepts stringy keys" do
            subject.parse(%w[sidekiq -C ./test/config_string.yml])

            assert_equal "./test/config_string.yml", config[:config_file]
            refute config[:verbose]
            assert_equal "./test/fake_env.rb", config[:require]
            assert_nil config[:environment]
            assert_equal 50, config[:concurrency]
            assert_equal 2, config[:queues].count { |q| q == "very_often" }
            assert_equal 1, config[:queues].count { |q| q == "seldom" }
          end

          it "accepts environment specific config" do
            subject.parse(%w[sidekiq -e staging -C ./test/config_environment.yml])

            assert_equal "./test/config_environment.yml", config[:config_file]
            refute config[:verbose]
            assert_equal "./test/fake_env.rb", config[:require]
            assert_equal "staging", config[:environment]
            assert_equal 50, config[:concurrency]
            assert_equal 2, config[:queues].count { |q| q == "very_often" }
            assert_equal 1, config[:queues].count { |q| q == "seldom" }
          end

          it "accepts environment specific config with alias" do
            subject.parse(%w[sidekiq -e staging -C ./test/config_with_alias.yml])
            assert_equal "./test/config_with_alias.yml", config[:config_file]
            refute config[:verbose]
            assert_equal "./test/fake_env.rb", config[:require]
            assert_equal "staging", config[:environment]
            assert_equal 50, config[:concurrency]
            assert_equal 2, config[:queues].count { |q| q == "very_often" }
            assert_equal 1, config[:queues].count { |q| q == "seldom" }

            subject.parse(%w[sidekiq -e production -C ./test/config_with_alias.yml])
            assert_equal "./test/config_with_alias.yml", config[:config_file]
            assert config[:verbose]
            assert_equal "./test/fake_env.rb", config[:require]
            assert_equal "production", config[:environment]
            assert_equal 50, config[:concurrency]
            assert_equal 2, config[:queues].count { |q| q == "very_often" }
            assert_equal 1, config[:queues].count { |q| q == "seldom" }
          end

          it "exposes ERB expected __FILE__ and __dir__" do
            given_path = "./test/config__FILE__and__dir__.yml"
            expected_file = File.expand_path(given_path)
            # As per Ruby's Kernel module docs, __dir__ is equivalent to File.dirname(File.realpath(__FILE__))
            expected_dir = File.dirname(File.realpath(expected_file))

            subject.parse(%W[sidekiq -C #{given_path}])

            assert_equal(expected_file, config.fetch(:__FILE__))
            assert_equal(expected_dir, config.fetch(:__dir__))
          end
        end

        describe "default config file" do
          describe "when required path is a directory" do
            it "tries config/sidekiq.yml from required diretory" do
              subject.parse(%w[sidekiq -r ./test/dummy])

              assert_equal "./test/dummy/config/sidekiq.yml", config[:config_file]
              assert_equal 25, config[:concurrency]
            end
          end

          describe "when required path is a file" do
            it "tries config/sidekiq.yml from current diretory" do
              config[:require] = "./test/dummy" # stub current dir – ./

              subject.parse(%w[sidekiq -r ./test/fake_env.rb])

              assert_equal "./test/dummy/config/sidekiq.yml", config[:config_file]
              assert_equal 25, config[:concurrency]
            end
          end

          describe "without any required path" do
            it "tries config/sidekiq.yml from current diretory" do
              config[:require] = "./test/dummy" # stub current dir – ./

              subject.parse(%w[sidekiq])

              assert_equal "./test/dummy/config/sidekiq.yml", config[:config_file]
              assert_equal 25, config[:concurrency]
            end
          end

          describe "when config file and flags" do
            it "merges options" do
              subject.parse(%w[sidekiq -C ./test/config.yml
                -e snoop
                -c 100
                -r ./test/fake_env.rb
                -q often,7
                -q seldom,3])

              assert_equal "./test/config.yml", config[:config_file]
              refute config[:verbose]
              assert_equal "./test/fake_env.rb", config[:require]
              assert_equal "snoop", config[:environment]
              assert_equal 100, config[:concurrency]
              assert_equal 7, config[:queues].count { |q| q == "often" }
              assert_equal 3, config[:queues].count { |q| q == "seldom" }
            end

            describe "when the config file specifies queues with weights" do
              describe "when -q specifies queues without weights" do
                it "sets strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config.yml
                    -r ./test/fake_env.rb
                    -q foo -q bar])

                  assert_equal true, !!config[:strict]
                end
              end

              describe "when -q specifies no queues" do
                it "does not set strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config.yml
                    -r ./test/fake_env.rb])

                  assert_equal false, !!config[:strict]
                end
              end

              describe "when -q specifies queues with weights" do
                it "does not set strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config.yml
                    -r ./test/fake_env.rb
                    -q foo,2 -q bar,3])

                  assert_equal false, !!config[:strict]
                end
              end
            end

            describe "when the config file specifies queues without weights" do
              describe "when -q specifies queues without weights" do
                it "sets strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config_queues_without_weights.yml
                    -r ./test/fake_env.rb
                    -q foo -q bar])

                  assert_equal true, !!config[:strict]
                end
              end

              describe "when -q specifies no queues" do
                it "sets strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config_queues_without_weights.yml
                    -r ./test/fake_env.rb])

                  assert_equal true, !!config[:strict]
                end
              end

              describe "when -q specifies queues with weights" do
                it "does not set strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config_queues_without_weights.yml
                    -r ./test/fake_env.rb
                    -q foo,2 -q bar,3])

                  assert_equal false, !!config[:strict]
                end
              end
            end

            describe "when the config file specifies no queues" do
              describe "when -q specifies queues without weights" do
                it "sets strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config_empty.yml
                    -r ./test/fake_env.rb
                    -q foo -q bar])

                  assert_equal true, !!config[:strict]
                end
              end

              describe "when -q specifies no queues" do
                it "sets strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config_empty.yml
                    -r ./test/fake_env.rb])

                  assert_equal true, !!config[:strict]
                end
              end

              describe "when -q specifies queues with weights" do
                it "does not set strictly ordered queues" do
                  subject.parse(%w[sidekiq -C ./test/config_empty.yml
                    -r ./test/fake_env.rb
                    -q foo,2 -q bar,3])

                  assert_equal false, !!config[:strict]
                end
              end
            end
          end

          describe "default config file" do
            describe "when required path is a directory" do
              it "tries config/sidekiq.yml" do
                subject.parse(%w[sidekiq -r ./test/dummy])

                assert_equal "sidekiq.yml", File.basename(config[:config_file])
                assert_equal 25, config[:concurrency]
              end
            end
          end
        end
      end

      describe "validation" do
        describe "when required application path does not exist" do
          it "exits with status 1" do
            exit = assert_raises(SystemExit) { subject.parse(%w[sidekiq -r /non/existent/path]) }
            assert_equal 1, exit.status
          end
        end

        describe "when required path is a directory without config/application.rb" do
          it "exits with status 1" do
            exit = assert_raises(SystemExit) { subject.parse(%w[sidekiq -r ./test/fixtures]) }
            assert_equal 1, exit.status
          end

          describe "when config file path does not exist" do
            it "raises argument error" do
              assert_raises(ArgumentError) do
                subject.parse(%w[sidekiq -r ./test/fake_env.rb -C /non/existent/path])
              end
            end
          end
        end

        describe "when concurrency is not valid" do
          describe "when set to 0" do
            it "raises argument error" do
              assert_raises(ArgumentError) do
                subject.parse(%w[sidekiq -r ./test/fake_env.rb -c 0])
              end
            end
          end

          describe "when set to a negative number" do
            it "raises argument error" do
              assert_raises(ArgumentError) do
                subject.parse(%w[sidekiq -r ./test/fake_env.rb -c -2])
              end
            end
          end
        end

        describe "when timeout is not valid" do
          describe "when set to 0" do
            it "raises argument error" do
              assert_raises(ArgumentError) do
                subject.parse(%w[sidekiq -r ./test/fake_env.rb -t 0])
              end
            end
          end

          describe "when set to a negative number" do
            it "raises argument error" do
              assert_raises(ArgumentError) do
                subject.parse(%w[sidekiq -r ./test/fake_env.rb -t -2])
              end
            end
          end
        end
      end
    end

    describe "#run" do
      before do
        subject.config = Sidekiq
        subject.config[:concurrency] = 2
        subject.config[:require] = "./test/fake_env.rb"
      end

      describe "require workers" do
        describe "when path is a rails directory" do
          before do
            subject.config[:require] = "./test/dummy"
            subject.environment = "test"
          end

          it "requires sidekiq railtie and rails application with environment" do
            subject.stub(:launch, nil) do
              subject.run
            end

            assert defined?(Sidekiq::Rails)
            assert defined?(Dummy::Application)
          end

          it "tags with the app directory name" do
            subject.stub(:launch, nil) do
              subject.run
            end

            assert_equal "dummy", subject.config[:tag]
          end
        end

        describe "when path is file" do
          it "requires application" do
            subject.stub(:launch, nil) do
              subject.run
            end

            assert $LOADED_FEATURES.any? { |x| x =~ /test\/fake_env/ }
          end
        end
      end

      describe "when development environment and stdout tty" do
        it "prints banner" do
          subject.stub(:environment, "development") do
            assert_output(/#{Regexp.escape(Sidekiq::CLI.banner)}/) do
              $stdout.stub(:tty?, true) do
                subject.stub(:launch, nil) do
                  subject.run
                end
              end
            end
          end
        end
      end

      it "prints rails info" do
        subject.stub(:environment, "production") do
          subject.stub(:launch, nil) do
            subject.run
          end
          assert_includes @logdev.string, "Booted Rails #{::Rails.version} application in production environment"
        end
      end

      describe "checking maxmemory policy" do
        it "warns if the policy is not noeviction" do
          redis_info = {"maxmemory_policy" => "allkeys-lru", "redis_version" => "6"}

          Sidekiq.stub(:redis_info, redis_info) do
            subject.stub(:launch, nil) do
              subject.run
            end
          end

          assert_includes @logdev.string, "allkeys-lru"
        end

        it "silent if the policy is noeviction" do
          redis_info = {"maxmemory_policy" => "noeviction", "redis_version" => "6"}

          Sidekiq.stub(:redis_info, redis_info) do
            subject.stub(:launch, nil) do
              subject.run
            end
          end

          refute_includes @logdev.string, "noeviction"
        end
      end
    end

    describe "signal handling" do
      %w[INT TERM].each do |sig|
        describe sig do
          it "raises interrupt error" do
            assert_raises Interrupt do
              subject.handle_signal(sig)
            end
          end
        end
      end

      describe "TSTP" do
        it "quiets with a corresponding event" do
          quiet = false

          subject.config = Sidekiq
          subject.config.on(:quiet) do
            quiet = true
          end

          subject.launcher = Sidekiq::Launcher.new(subject.config)
          subject.handle_signal("TSTP")

          assert_match(/Got TSTP signal/, logdev.string)
          assert_equal true, quiet
        end
      end

      describe "TTIN" do
        it "prints backtraces for all threads in the process to the logfile" do
          subject.handle_signal("TTIN")

          assert_match(/Got TTIN signal/, logdev.string)
          assert_match(/\bbacktrace\b/, logdev.string)
        end
      end

      describe "UNKNOWN" do
        it "logs about" do
          # subject.parse(%w[sidekiq -r ./test/fake_env.rb])
          subject.handle_signal("UNKNOWN")

          assert_match(/Got UNKNOWN signal/, logdev.string)
          assert_match(/No signal handler registered/, logdev.string)
        end
      end
    end
  end
end
