require 'helper'
require 'ostruct'

module SSHKit
  module Backend
    class TestConnectionPool < UnitTest

      def setup
        super
        pool.flush_connections
      end

      def pool
        @pool ||= SSHKit::Backend::ConnectionPool.new
      end

      def connect
        ->(*_args) { Object.new }
      end

      def connect_and_close
        ->(*_args) { OpenStruct.new(:closed? => true) }
      end

      def echo_args
        ->(*args) { args }
      end

      def test_default_idle_timeout
        assert_equal 30, pool.idle_timeout
      end

      def test_connection_factory_receives_args
        args = %w(a b c)
        conn = pool.with(echo_args, *args) { |c| c }

        assert_equal args, conn
      end

      def test_connections_are_not_reused_if_not_checked_in
        conn1 = nil
        conn2 = nil

        pool.with(connect, "conn") do |yielded_conn_1|
          conn1 = yielded_conn_1
          conn2 = pool.with(connect, "conn") { |c| c }
        end

        refute_equal conn1, conn2
      end

      def test_connections_are_reused_if_checked_in
        conn1 = pool.with(connect, "conn") { |c| c }
        conn2 = pool.with(connect, "conn") { |c| c }

        assert_equal conn1, conn2
      end

      def test_connections_are_reused_across_threads_multiple_times
        t1 = Thread.new do
          pool.with(connect, "conn") { |c| c }
        end

        t2 = Thread.new do
          pool.with(connect, "conn") { |c| c }
        end

        t3 = Thread.new do
          pool.with(connect, "conn") { |c| c }
        end

        refute_nil t1.value
        assert_equal t1.value, t2.value
        assert_equal t2.value, t3.value
      end

      def test_zero_idle_timeout_disables_pooling
        pool.idle_timeout = 0

        conn1 = pool.with(connect, "conn") { |c| c }
        conn2 = pool.with(connect, "conn") { |c| c }
        refute_equal conn1, conn2
      end

      def test_expired_connection_is_not_reused
        pool.idle_timeout = 0.1

        conn1 = pool.with(connect, "conn") { |c| c }
        sleep(pool.idle_timeout)
        conn2 = pool.with(connect, "conn") { |c| c }

        refute_equal conn1, conn2
      end

      def test_expired_connection_is_closed
        pool.idle_timeout = 0.1
        conn1 = mock
        conn1.expects(:closed?).twice.returns(false)
        conn1.expects(:close)

        pool.with(->(*) { conn1 }, "conn1") {}
        # Pause to allow the background thread to wake and close the conn
        sleep(5 + pool.idle_timeout)
      end

      def test_closed_connection_is_not_reused
        conn1 = pool.with(connect_and_close, "conn") { |c| c }
        conn2 = pool.with(connect, "conn") { |c| c }

        refute_equal conn1, conn2
      end

      def test_connections_with_different_args_are_not_reused
        conn1 = pool.with(connect, "conn1") { |c| c }
        conn2 = pool.with(connect, "conn2") { |c| c }

        refute_equal conn1, conn2
      end

      def test_close_connections
        conn1 = mock
        conn1.expects(:closed?).twice.returns(false)
        conn1.expects(:close)

        conn2 = mock
        conn2.expects(:closed?).returns(false)
        conn2.expects(:close).never

        pool.with(->(*) { conn1 }, "conn1") {}

        # We are using conn2 when close_connections is called, so it should
        # not be closed.
        pool.with(->(*) { conn2 }, "conn2") do
          pool.close_connections
        end
      end

      def test_connections_with_changed_args_is_reused
        options = { known_hosts: "foo" }
        connect_change_options = ->(*args) { args.last[:known_hosts] = "bar"; Object.new }
        conn1 = pool.with(connect_change_options, "arg", options) { |c| c }
        conn2 = pool.with(connect_change_options, "arg", options) { |c| c }

        assert_equal conn1, conn2
      end
    end
  end
end
