require 'spec_helper'

require 'engineyard-serverside/server'

module EY
  module Serverside

    describe Server do
      let(:hostname) {'nosuch.server.com'}
      let(:roles) {[:romeo, :mercutio, :tybalt]}
      let(:name) {'superserverx5000'}
      let(:user) {'joe'}
      let(:server_hash) {{
        :hostname => hostname,
        :roles => roles,
        :name => name,
        :user => user
      }}

      let(:server) {described_class.from_hash(server_hash)}

      describe '.from_hash' do
        let(:role_set) {Object.new}
        let(:expected) {Object.new}

        let(:result) {described_class.from_hash(server_hash)}

        before(:each) do
          # Stub out the parts of the Set API that we need to be able to express
          # its use.
          allow(Set).to receive(:new).with(roles).and_return(role_set)

          # This is a little dirty, but not incredibly. Server is a Struct with
          # specific attributes. As such, it doesn't have an initializer of its
          # own, and our seam is between Server and Struct, so we can stub out
          # instantiation and check to ensure that it's given the proper values.
          allow(described_class).to receive(:new).and_return(expected)
        end

        it 'instantiates a new Server from a server hash' do
          expect(described_class).
            to receive(:new).
            with(hostname, role_set, name, user)

          result
        end

        it 'is the newly instantiated Server instance' do
          expect(result).to eql(expected)
        end
      end

      describe '.known_hosts_file' do
        let(:tmp) {Object.new}
        let(:result) {described_class.known_hosts_file}

        before(:each) do
          # Of all the design decisions that could have been made here, this
          # class method is stateful, so we have to blow away that state to
          # gurantee the idempotence of the examples here.
          described_class.class_eval do
            @known_hosts_file = nil
          end

          # Stub out the pieces of the Tempfile interface that we use so as to
          # cut down on test artifacts.
          allow(Tempfile).
            to receive(:new).
            with('ey-ss-known-hosts').
            and_return(tmp)
        end

        context 'on initial use' do
          it 'generates a temp file' do
            expect(Tempfile).
              to receive(:new).
              with('ey-ss-known-hosts').
              and_return(tmp)

            result
          end

          it 'returns the generated temp file' do
            expect(result).to eql(tmp)
          end
        end

        context 'on subsequent uses' do
          let(:tmp) {'an established temp file'}

          before(:each) do
            described_class.class_eval do
              @known_hosts_file = tmp
            end

            it 'reuses its existing temp file' do
              expect(Tempfile).not_to receive(:new)

              expect(result).to eql(tmp)
            end
          end
        end
      end

      describe '#authority' do
        let(:result) {server.authority}

        it 'is the SSH signature for the server' do
          expect(result).to eql(user + '@' + hostname)
        end
      end

      describe '#inspect' do
        let(:result) {server.inspect}

        it 'begins with the hostname and role' do
          expect(result).to match(%r{^#{hostname}\(#{server.role}})
        end

        context 'when the server has a name' do
          let(:name) {'wiggly'}

          it 'includes the server name' do
            expect(result).to match(%r{#{name}\)$})
          end
        end

        context 'when the server has no name' do
          before(:each) do
            server_hash.delete(:name)
          end

          it 'does not include a server name' do
            expect(result).to match(%r{#{server.role}\)$})
          end
        end
      end

      describe '#role' do
        let(:result) {server.role}

        it 'is the first role in the role list' do
          expect(result).to eql(Set.new(roles).to_a.first)
        end
      end

      describe '#matches_roles?' do
        let(:candidates) {[]}
        let(:result) {server.matches_roles?(candidates)}

        context 'when the server has all of the candidate roles' do
          let(:candidates) {[:tybalt, :romeo, :mercutio]}

          it 'is true' do
            expect(result).to eql(true)
          end
        end

        context 'when the server has some of the candidate roles' do
          let(:candidates) {[:romeo, :tybalt]}

          it 'is true' do
            expect(result).to eql(true)
          end
        end

        context 'when the server has none of the candidate roles' do
          let(:candidates) {[:benvolio, :gildenstern]}

          it 'is false' do
            expect(result).to eql(false)
          end
        end

        context 'when the candidates list is empty' do
          let(:candidates) {[]}

          it 'is false' do
            expect(result).to eql(false)
          end
        end
      end

      describe '#roles=' do
        let(:candidates) {[:benvolio, :gildenstern]}
        let(:result) {server.roles = candidates}

        it 'removes the original roles' do
          result

          roles.each do |role|
            expect(server.matches_roles?([role])).to eql(false)
          end
        end

        it 'adds the candidate roles' do
          result

          candidates.each do |role|
            expect(server.matches_roles?([role])).to eql(true)
          end
        end
      end

      describe '#local?' do
        let(:result) {server.local?}

        context 'when the hostname is localhost' do
          let(:hostname) {'localhost'}

          it 'is true' do
            expect(result).to eql(true)
          end
        end

        context 'when the hostname is not localhost' do
          let(:hostname) {'a.b.c.d'}

          it 'is false' do
            expect(result).to eql(false)
          end
        end
      end

      describe '#sync_directory_command' do
        let(:directory) {'/path/to/something/interesting'}
        let(:result) {server.sync_directory_command(directory)}

        context 'when the server is local' do
          let(:hostname) {'localhost'}

          it 'is nil' do
            expect(result).to eql(nil)
          end
        end

        context 'when the server is not local' do
          let(:ssh_command) {'dummy_ssh_command'}
          let(:remote_mkdir) {'remote_mkdir'}
          let(:rsync_command) {'rsync_command'}
          let(:hostname) {'donkey.kong'}

          before(:each) do
            allow(server).to receive(:ssh_command).and_return(ssh_command)
            allow(server).to receive(:remote_command).and_return(remote_mkdir)

            allow(Escape).to receive(:shell_command).and_return(rsync_command)
          end

          it 'generates a command to create the full path to the directory remotely' do
            expect(server).to receive(:remote_command).with("mkdir -p #{directory}")

            result
          end

          it 'generates a command to sync the directory to the remote server' do
            expect(Escape).
              to receive(:shell_command).
              with([
                'rsync',
                '--delete',
                '-rlpgoDq',
                '-e',
                ssh_command,
                "#{directory}/",
                "#{user}@#{hostname}:#{directory}"
              ])

            result
          end

          context 'when existing files are to be ignored' do
            let(:result) {server.sync_directory_command(directory, true)}

            it 'adds the ignore existing flag to the rsync command' do
              expect(Escape).
                to receive(:shell_command).
                with([
                  'rsync',
                  '--delete',
                  '-rlpgoDq',
                  '--ignore-existing',
                  '-e',
                  ssh_command,
                  "#{directory}/",
                  "#{user}@#{hostname}:#{directory}"
                ])

              result
            end
          end

          it 'is the remote mkdir and the rsync separated by a shell AND operation' do
            expect(result).to eql("#{remote_mkdir} && #{rsync_command}")
          end
        end
      end

      describe '#scp_command' do
      end

      describe '#command_on_server' do
      end

      describe '#run' do
        let(:command) {'echo'}

        context 'when the server is local' do
          let(:hostname) {'localhost'}

          it 'yields the command unalertered to the given block' do
            server.run(command) do |cmd|
              expect(cmd).to eql(command)
            end
          end
        end

        context 'when the server is not local' do
          let(:hostname) {'pac.man'}

          before(:each) do
            allow(server).to receive(:remote_command) {'totally remote my dude'}
          end

          it 'yields the remote version of the command to the given block' do
            server.run(command) do |cmd|
              expect(cmd).to eql(server.remote_command(command))
            end
          end
        end
      end

      describe '#remote_command' do
        let(:command) {'touch toes'}
        let(:result) {server.remote_command(command)}

        before(:each) do
          allow(server).to receive(:ssh_command).and_return('default ssh command')
        end

        it 'is an SSH command that would run the escaped command on the remote server' do
          expect(result).to eql("#{server.ssh_command} #{user}@#{hostname} '#{command}'")
        end
      end

      describe '#ssh_command' do
        let(:result) {server.ssh_command}
        let(:known_hosts_file) {Object.new}

        before(:each) do
          allow(described_class).
            to receive(:known_hosts_file).
            and_return(known_hosts_file)

          allow(known_hosts_file).
            to receive(:path).
            and_return('less_followed')
        end

        it 'is an ssh invocation' do
          expect(result).to match(%r{^ssh })
        end

        it 'uses the internal ssh key' do
          expect(result).to match(%r{\s-i #{ENV['HOME']}\/\.ssh\/internal\s+})
        end

        it 'disables strict host key checking' do
          expect(result).to match(%r{\s+-o StrictHostKeyChecking=no\s+})
        end

        it 'disables password authentication' do
          expect(result).to match(%r{\s+-o PasswordAuthentication=no\s+})
        end

        it 'sets the keepalive to one minute' do
          expect(result).to match(%r{\s+-o ServerAliveInterval=60})
        end

        it 'specifies a known hosts file' do
          expect(result).
            to match(
              %r{\s+-o UserKnownHostsFile=#{described_class.known_hosts_file.path}}
            )
        end
      end
    end

  end
end
