require "spec_helper"

describe Moped::Node, replica_set: true do

  let(:replica_set_node) do
    @replica_set.nodes.first
  end

  let(:node) do
    Moped::Node.new(replica_set_node.address, pool_size: 1)
  end

  describe "#auto_discovering?" do

    context "when no option is provided" do

      let(:node) do
        described_class.new(replica_set_node.address, {})
      end

      it "returns true" do
        expect(node).to be_auto_discovering
      end
    end

    context "when an option is provided" do

      context "when the option is true" do

        let(:node) do
          described_class.new(replica_set_node.address, auto_discover: true)
        end

        it "returns true" do
          expect(node).to be_auto_discovering
        end
      end

      context "when the option is false" do

        let(:node) do
          described_class.new(replica_set_node.address, auto_discover: false)
        end

        it "returns true" do
          expect(node).to_not be_auto_discovering
        end
      end
    end
  end

  describe "#disconnect" do

    context "when the node is running" do

      before do
        node.disconnect
      end

      it "disconnects from the server" do
        node.should_not be_connected
      end
    end
  end

  describe '#down!' do
    describe 'when connected' do
      before do
        node.connected?
      end

      describe 'shutdown' do
        before do
          node.down!
        end

        it 'clears out the connection pool' do
          expect(node.instance_variable_get(:@pool)).to be_nil
        end
      end

      describe 'shutdown from multiple threads' do
        before do
          # fake the behavior of @pool that holds its value until shutdown returns
          allow(Moped::Connection::Manager).to receive(:shutdown).and_return(node.instance_variable_get(:@pool)).once
          node.down!
        end

        it 'clears out the connection pool before returning' do
          expect(node.instance_variable_get(:@pool)).to be_nil
        end
      end
    end
  end

  describe "#latency" do

    let(:node) do
      Moped::Node.new("127.0.0.1:27017")
    end

    context "when the node is not connected" do

      it "returns nil" do
        expect(node.latency).to be_nil
      end
    end

    context "when the node is connected" do

      before do
        node.connection do |conn|
          node.send(:connect, conn)
        end
      end

      it "returns the latency in seconds" do
        expect(node.latency < 1).to be_true
      end
    end
  end

  describe "#peers" do

    let(:info) do
      {
         "setName"   => "moped_dev",
         "ismaster"  => true,
         "secondary" => false,
         "hosts"     => [ "127.0.0.1:27017", "localhost:27018" ],
         "primary"   => "127.0.0.1:27017",
         "me"        => "127.0.0.1:27017",
         "ok"        => 1.0
      }
    end

    context "when the node is auto discovering" do

      let(:node) do
        described_class.new("127.0.0.1:27017", pool_size: 1)
      end

      before do
        node.should_receive(:command).at_least(:once).with("admin", ismaster: 1).and_return(info)
        node.refresh
      end

      it "auto discovers additional host nodes" do
        expect(node.peers.size).to eq(2)
      end

      context "when calling refresh for second time" do

        before do
          node.refresh
        end

        it "does not add new nodes" do
          expect(node.peers.size).to eq(2)
        end
      end
    end

    context "when the node is not auto discovering" do

      let(:node) do
        described_class.new("127.0.0.1:27017", auto_discover: false)
      end

      before do
        node.should_receive(:command).with("admin", ismaster: 1).and_return(info)
        node.refresh
      end

      it "does not auto discover additional host nodes" do
        expect(node.peers.size).to eq(0)
      end
    end
  end

  describe "#ensure_connected" do

    context "when the node is running" do

      it "processes the block" do
        node.ensure_connected do
          node.command("admin", ping: 1)
        end.should eq("ok" => 1)
      end
    end

    context "when the node is not running" do

      before do
        replica_set_node.stop
      end

      it "raises a connection error" do
        lambda do
          node.ensure_connected do
            node.command("admin", ping: 1)
          end
        end.should raise_exception(Moped::Errors::ConnectionFailure)
      end

      it "marks the node as down" do
        begin
          node.ensure_connected do
            node.command("admin", ping: 1)
          end
        rescue Moped::Errors::ConnectionFailure
          node.should be_down
        end
      end
    end

    context "when node is connected but connection is dropped" do

      before do
        node.ensure_connected do
          node.command("admin", ping: 1)
        end

        replica_set_node.hiccup
      end

      it "reconnects without raising an exception" do
        node.ensure_connected do
          node.command("admin", ping: 1)
        end.should eq("ok" => 1)
      end
    end

    context "when the server crashes or responds with nil" do

      it "fails over to the next node" do
        replica_set_node.crash_on_next_message!
        node.ensure_connected do
          node.command("admin", ping: 1)
        end.should eq("ok" => 1)
      end
    end

    context "when node closes the connection before sending a reply" do

      it "fails over to the next node" do
        replica_set_node.hiccup_on_next_message!
        node.ensure_connected do
          node.command("admin", ping: 1)
        end.should eq("ok" => 1)
      end
    end

    context "when the socket gets disconnected in the middle of a send" do

      before do
        node.connection do |conn|
          conn.stub(:connected?).and_return(true)
          conn.stub(:alive?).and_return(false)
          conn.instance_variable_set(:@sock, nil)
        end
      end

      it "reconnects the socket" do
        lambda do
          node.ensure_connected do
            node.command("admin", ping: 1)
          end
        end.should_not raise_exception
      end
    end

    context "when there is a reconfiguration" do

      let(:potential_reconfiguration_error) do
        Moped::Errors::QueryFailure.new("", {})
      end

      before do
        node.connection do |conn|
          conn.stub(:connected?).and_return(false)
          conn.stub(:connect).and_raise(potential_reconfiguration_error)
        end
      end

      context "and the reconfigation is of a replica set" do

        before do
          potential_reconfiguration_error.stub(:reconfiguring_replica_set?).and_return(true)
        end

        it "raises a ReplicaSetReconfigured error" do
          expect {
            node.ensure_connected {}
          }.to raise_error(Moped::Errors::ReplicaSetReconfigured)
        end
      end

      context "and the reconfigation is not of a replica set" do

        before do
          potential_reconfiguration_error.stub(:reconfiguring_replica_set?).and_return(false)
        end

        it "raises a PotentialReconfiguration error" do
          expect {
            node.ensure_connected {}
          }.to raise_error(Moped::Errors::PotentialReconfiguration)
        end
      end
    end
  end

  describe "#initialize" do

    let(:node) do
      described_class.new("iamnota.mongoid.org")
    end

    let(:non_default_node) do
      described_class.new("iama.mongoid.org:5309")
    end

    context "when dns cannot resolve the address" do

      it "flags the node as being down" do
        node.should be_down
      end

      it "sets the down_at time" do
        node.down_at.should be_within(1).of(Time.now)
      end

      context "when attempting to refresh the node" do

        before do
          node.refresh
        end

        it "keeps the node flagged as down" do
          node.should be_down
        end

        it "updates the down_at time" do
          node.down_at.should be_within(1).of(Time.now)
        end
      end
    end
  end

  describe "#messagable?" do

    let(:node) do
      Moped::Node.new("127.0.0.1:27017")
    end

    context "when the node is primary" do

      before do
        node.instance_variable_set(:@primary, true)
      end

      it "returns true" do
        expect(node).to be_messagable
      end
    end

    context "when the node is secondary" do

      before do
        node.instance_variable_set(:@secondary, true)
      end

      it "returns true" do
        expect(node).to be_messagable
      end
    end

    context "when the node is an arbiter" do

      before do
        node.instance_variable_set(:@arbiter, true)
      end

      it "returns false" do
        expect(node).to_not be_messagable
      end
    end

    context "when the node is passive" do

      before do
        node.instance_variable_set(:@passive, true)
      end

      it "returns false" do
        expect(node).to_not be_messagable
      end
    end
  end

  describe "#refresh" do

    context "when the ismaster command fails" do

      let(:node) do
        described_class.new("127.0.0.1:27017")
      end

      before do
        node.should_receive(:command).with("admin", ismaster: 1).and_raise(Timeout::Error)
        node.refresh
      end

      it "still sets the refresh time" do
        expect(node.refreshed_at).to_not be_nil
      end
    end

    context "when the node has authentication details" do

      let(:node) do
        described_class.new("127.0.0.1:27017")
      end

      before do
        node.credentials["moped_test"] = [ "user", "pass" ]
      end

      context "when discovering a peer" do

        let(:info) do
          {
            "ismaster" => true,
            "secondary" => false,
            "hosts" => [ "127.0.0.1:27017", "127.0.0.1:27018" ],
            "me" => "127.0.0.1:27017",
            "maxBsonObjectSize" => 16777216,
            "ok" => 1.0
          }
        end

        before do
          node.should_receive(:command).with("admin", ismaster: 1).and_return(info)
          node.refresh
        end

        let(:peer) do
          node.peers.last
        end

        it "add the authentication details to the peer" do
          peer.credentials.should eq(node.credentials)
        end
      end
    end

    context "when refreshing a node with a bad address" do

      let(:node) do
        described_class.new("iamnota.mongoid.org")
      end

      before do
        node.address.stub(:resolve).and_return(true)
      end

      context "when ensuring primary" do

        before do
          node.stub(:executing?).with(:ensure_primary).and_return(true)
        end

        context "and not on the primary" do

          before do
            node.stub(:command).and_return("secondary" => true)
          end

          it "raises a ReplicaSetReconfigured error" do
            expect {
              node.refresh
            }.to raise_error(Moped::Errors::ReplicaSetReconfigured)
          end
        end
      end
    end
  end

  describe "#connection" do
    let(:node) do
      described_class.new("127.0.0.1:27017", pool_size: 1, pool_timeout: 0.1)
    end

    context "when take a long time to get a connection from pool" do
      it "raise a Errors::PoolTimeout error" do
        expect {

          exception = nil
          100.times.map do |i|
            Thread.new do
              begin
                node.connection do |conn|
                  conn.apply_credentials({})
                  node.update("test", "test_collection", { name: "test_counter" }, {'$inc' => {'cnt' => 1}}, Moped::WriteConcern.get({ w: 1 }), flags: {safe: true, upsert: true})
                end
              rescue => e
                exception = e if exception.nil?
              end
            end
          end.each {|t| t.join }
          raise exception unless exception.nil?

        }.to raise_error(Moped::Errors::PoolTimeout)
      end
    end

    context "when the timeout happens after get a connection from pool" do
      it "raise a Timeout::Error" do
        expect {
          node.connection do |conn|
            Timeout::timeout(0.01) do
              conn.apply_credentials({})
              node.update("test", "test_collection", { name: "test_counter" }, {'$inc' => {'cnt' => 1}}, Moped::WriteConcern.get({ w: 1 }), flags: {safe: true, upsert: true})
              sleep(0.1) # just to simulate a long block which raise a timeout
            end
          end
        }.to raise_error(Timeout::Error)
      end
    end
  end
end
