require File.expand_path(File.dirname(__FILE__)) + '/../unit_helper'

require 'lhm/throttler/slave_lag'

describe Lhm::Throttler do
  include UnitHelper

  describe '#format_hosts' do
    describe 'with only localhost hosts' do
      it 'returns no hosts' do
        assert_equal([], Lhm::Throttler.format_hosts(['localhost:1234', '127.0.0.1:5678']))
      end
    end

    describe 'with only remote hosts' do
      it 'returns remote hosts' do
        assert_equal(['server.example.com', 'anotherserver.example.com'], Lhm::Throttler.format_hosts(['server.example.com:1234', 'anotherserver.example.com']))
      end
    end

    describe 'with only nil hosts' do
      it 'returns no hosts' do
        assert_equal([], Lhm::Throttler.format_hosts([nil]))
      end
    end

    describe 'with some nil hosts' do
      it 'returns the remaining hosts' do
        assert_equal(['server.example.com'], Lhm::Throttler.format_hosts([nil, 'server.example.com:1234']))
      end
    end
  end
end

describe Lhm::Throttler::Slave do
  include UnitHelper

  before :each do
    @logs = StringIO.new
    Lhm.logger = Logger.new(@logs)

    @dummy_mysql_client_config = lambda { { 'username' => 'user', 'password' => 'pw', 'database' => 'db' } }
  end

  describe "#client" do
    before do
      class TestMysql2Client
        def initialize(config)
          raise Mysql2::Error.new("connection error")
        end
      end
    end

    describe 'on connection error' do
      it 'logs and returns nil' do
        assert_nil(Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config).connection)

        log_messages = @logs.string.lines
        assert_equal(2, log_messages.length)
        assert log_messages[0].include? "Connecting to slave on database: db"
        assert log_messages[1].include? "Error connecting to slave: Unknown MySQL server host 'slave'"
      end
    end

    describe 'with proper config' do
      it "creates a new Mysql2::Client" do
        expected_config = { username: 'user', password: 'pw', database: 'db', host: 'slave' }
        Mysql2::Client.stubs(:new).with(expected_config).returns(mock())

        assert Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config).connection
      end
    end

    describe 'with active record config' do
      it 'logs and creates client' do
        active_record_config = { username: 'user', password: 'pw', database: 'db' }
        if ActiveRecord::VERSION::MAJOR > 6 || ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1
          ActiveRecord::Base.stubs(:connection_pool).returns(stub(db_config: stub(configuration_hash: active_record_config)))
        else
          ActiveRecord::Base.stubs(:connection_pool).returns(stub(spec: stub(config: active_record_config)))
        end

        Mysql2::Client.stubs(:new).returns(mock())

        assert Lhm::Throttler::Slave.new('slave').connection

        log_messages = @logs.string.lines
        assert_equal(1, log_messages.length)
        assert log_messages[0].include? "Connecting to slave on database: db"
      end
    end
  end

  describe "#connection" do
    before do
      class Connection
        def self.query(query)
          if query == Lhm::Throttler::Slave::SQL_SELECT_MAX_SLAVE_LAG
            [{ 'Seconds_Behind_Master' => 20 }]
          elsif query == Lhm::Throttler::Slave::SQL_SELECT_SLAVE_HOSTS
            [{ 'host' => '1.1.1.1:80' }]
          end
        end
      end

      @slave = Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config)
      @slave.instance_variable_set(:@connection, Connection)

      class StoppedConnection
        def self.query(query)
          [{ 'Seconds_Behind_Master' => nil }]
        end
      end

      @stopped_slave = Lhm::Throttler::Slave.new('stopped_slave', @dummy_mysql_client_config)
      @stopped_slave.instance_variable_set(:@connection, StoppedConnection)
    end

    describe "#lag" do
      it "returns the slave lag" do
        assert_equal(20, @slave.lag)
      end
    end

    describe "#lag with a stopped slave" do
      it "returns 0 slave lag" do
        assert_equal(0, @stopped_slave.lag)
      end
    end

    describe "#slave_hosts" do
      it "returns the hosts" do
        assert_equal(['1.1.1.1'], @slave.slave_hosts)
      end
    end

    describe "#lag on connection error" do
      it "logs and returns 0 slave lag" do
        client = mock()
        client.stubs(:query).raises(Mysql2::Error, "Can't connect to MySQL server")
        Lhm::Throttler::Slave.any_instance.stubs(:client).returns(client)
        Lhm::Throttler::Slave.any_instance.stubs(:config).returns([])

        slave = Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config)
        Logger.any_instance.expects(:info).with("Unable to connect and/or query slave: Can't connect to MySQL server")
        assert_equal(0, slave.lag)
      end
    end
  end
end

describe Lhm::Throttler::SlaveLag do
  include UnitHelper

  before :each do
    @throttler = Lhm::Throttler::SlaveLag.new
  end

  describe '#throttle_seconds' do
    describe 'with no slave lag' do
      before do
        def @throttler.max_current_slave_lag
          0
        end
      end

      it 'does not alter the currently set timeout' do
        timeout = @throttler.timeout_seconds
        assert_equal(timeout, @throttler.send(:throttle_seconds))
      end
    end

    describe 'with a large slave lag' do
      before do
        def @throttler.max_current_slave_lag
          100
        end
      end

      it 'doubles the currently set timeout' do
        timeout = @throttler.timeout_seconds
        assert_equal(timeout * 2, @throttler.send(:throttle_seconds))
      end

      it 'does not increase the timeout past the maximum' do
        @throttler.timeout_seconds = Lhm::Throttler::SlaveLag::MAX_TIMEOUT
        assert_equal(Lhm::Throttler::SlaveLag::MAX_TIMEOUT, @throttler.send(:throttle_seconds))
      end
    end

    describe 'with no slave lag after it has previously been increased' do
      before do
        def @throttler.max_current_slave_lag
          0
        end
      end

      it 'halves the currently set timeout' do
        @throttler.timeout_seconds *= 2 * 2
        timeout = @throttler.timeout_seconds
        assert_equal(timeout / 2, @throttler.send(:throttle_seconds))
      end

      it 'does not decrease the timeout past the minimum on repeated runs' do
        @throttler.timeout_seconds = Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT * 2
        assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT, @throttler.send(:throttle_seconds))
        assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT, @throttler.send(:throttle_seconds))
      end
    end
  end

  describe '#max_current_slave_lag' do
    describe 'with multiple slaves' do
      it 'returns the largest amount of lag' do
        slave1 = mock()
        slave2 = mock()
        slave1.stubs(:lag).returns(5)
        slave2.stubs(:lag).returns(0)
        Lhm::Throttler::SlaveLag.any_instance.stubs(:slaves).returns([slave1, slave2])
        assert_equal 5, @throttler.send(:max_current_slave_lag)
      end
    end

    describe 'with MySQL stopped on the slave' do
      it 'assumes 0 slave lag' do
        client = mock()
        client.stubs(:query).raises(Mysql2::Error, "Can't connect to MySQL server")
        Lhm::Throttler::Slave.any_instance.stubs(:client).returns(client)

        Lhm::Throttler::Slave.any_instance.stubs(:prepare_connection_config).returns([])
        Lhm::Throttler::Slave.any_instance.stubs(:slave_hosts).returns(['1.1.1.2'])
        @throttler.stubs(:master_slave_hosts).returns(['1.1.1.1'])

        assert_equal 0, @throttler.send(:max_current_slave_lag)
      end
    end
  end

  describe '#get_slaves' do
    describe 'with no slaves' do
      before do
        def @throttler.master_slave_hosts
          []
        end
      end

      it 'returns no slaves' do
        assert_equal([], @throttler.send(:get_slaves))
      end
    end

    describe 'with multiple slaves' do
      before do
        class TestSlave
          attr_reader :host, :connection

          def initialize(host, _)
            @host = host
            @connection = 'conn' if @host
          end

          def slave_hosts
            if @host == '1.1.1.1'
              ['1.1.1.2', '1.1.1.3']
            else
              [nil]
            end
          end
        end

        @create_slave = lambda { |host, config|
          TestSlave.new(host, config)
        }
      end

      describe 'without the :check_only option' do
        before do
          def @throttler.master_slave_hosts
            ['1.1.1.1', '1.1.1.4']
          end
        end

        it 'returns the slave instances' do
          Lhm::Throttler::Slave.stubs(:new).returns(@create_slave) do
            assert_equal(["1.1.1.4", "1.1.1.1", "1.1.1.3", "1.1.1.2"], @throttler.send(:get_slaves).map(&:host))
          end
        end
      end

      describe 'with the :check_only option' do
        describe 'with a callable argument' do
          before do
            check_only = lambda { { 'host' => '1.1.1.3' } }
            @throttler = Lhm::Throttler::SlaveLag.new :check_only => check_only
          end

          it 'returns only that single slave' do
            Lhm::Throttler::Slave.stubs(:new).returns(@create_slave) do
              assert_equal ['1.1.1.3'], @throttler.send(:get_slaves).map(&:host)
            end
          end
        end

        describe 'with a non-callable argument' do
          before do
            @throttler = Lhm::Throttler::SlaveLag.new :check_only => 'I cannot be called'

            def @throttler.master_slave_hosts
              ['1.1.1.1', '1.1.1.4']
            end
          end

          it 'returns all the slave instances' do
            Lhm::Throttler::Slave.stubs(:new).returns(@create_slave) do
              assert_equal(["1.1.1.4", "1.1.1.1", "1.1.1.3", "1.1.1.2"], @throttler.send(:get_slaves).map(&:host))
            end
          end
        end
      end
    end
  end
end