require 'assert'
require 'dk/remote'

require 'dk/local'

module Dk::Remote

  class UnitTests < Assert::Context
    desc "Dk::Remote"
    setup do
      @hosts         = Factory.hosts
      @ssh_args      = Factory.string
      @host_ssh_args = { @hosts.sample => Factory.string }
      @cmd_str       = Factory.string

      @opts = {
        :env           => { Factory.string => Factory.string },
        :dry_tree_run  => Factory.boolean,
        :hosts         => @hosts,
        :ssh_args      => @ssh_args,
        :host_ssh_args => @host_ssh_args,
        Factory.string => Factory.string
      }

      @local_cmd_new_called_with = nil
      Assert.stub(Dk::Local::Cmd, :new) do |*args|
        @local_cmd_new_called_with = args
        Assert.stub_send(Dk::Local::Cmd, :new, *args)
      end

      @local_cmd_spy_new_called_with = nil
      Assert.stub(Dk::Local::CmdSpy, :new) do |*args|
        @local_cmd_spy_new_called_with = args
        Assert.stub_send(Dk::Local::CmdSpy, :new, *args).tap do |spy|
          spy.exitstatus = Factory.exitstatus
          spy.stdout     = [Factory.stdout, nil].sample
          spy.stderr     = [Factory.stderr, nil].sample
        end
      end

      @remote_class = Dk::Remote
    end
    subject{ @remote_class }

    should have_imeths :ssh_cmd_str

    should "build ssh cmd strs" do
      h = @host_ssh_args.keys.first
      exp = ssh_cmd_str(@cmd_str, h, @ssh_args, @host_ssh_args)
      assert_equal exp, subject.ssh_cmd_str(@cmd_str, h, @ssh_args, @host_ssh_args)
    end

    private

    def ssh_cmd_str(cmd_str, host, args, host_args)
      val = "\"#{cmd_str}\"".gsub("\\", "\\\\\\").gsub('"', '\"')
      "ssh #{args} #{host_args[host]} #{host} -- \"sh -c #{val}\""
    end

  end

  class BaseCmdTests < UnitTests
    desc "BaseCmd"
    setup do
      @cmd_class = @remote_class::BaseCmd
      @cmd = @cmd_class.new(Dk::Local::CmdSpy, @cmd_str, @opts)
    end
    subject{ @cmd }

    should have_readers :hosts, :ssh_args, :host_ssh_args, :cmd_str, :local_cmds
    should have_imeths :to_s, :ssh_cmd_str, :run
    should have_imeths :stdout, :stderr, :success?, :output_lines

    should "know its hosts" do
      assert_equal @hosts.sort, subject.hosts
    end

    should "complain if not given any hosts" do
      assert_raises(NoHostsError) do
        @cmd_class.new(Dk::Local::CmdSpy, @cmd_str, :hosts => nil)
      end
      assert_raises(NoHostsError) do
        @cmd_class.new(Dk::Local::CmdSpy, @cmd_str, :hosts => Factory.integer)
      end
      assert_raises(NoHostsError) do
        @cmd_class.new(Dk::Local::CmdSpy, @cmd_str, :hosts => [])
      end
      assert_raises(NoHostsError) do
        @cmd_class.new(Dk::Local::CmdSpy, @cmd_str, :hosts => [nil])
      end
      assert_raises(NoHostsError) do
        @cmd_class.new(Dk::Local::CmdSpy, @cmd_str, :hosts => [Factory.string, nil])
      end
      assert_raises(NoHostsError) do
        @cmd_class.new(Dk::Local::CmdSpy, @cmd_str, :hosts => [nil, Factory.string])
      end
    end

    should "know its ssh args" do
      assert_equal @ssh_args,      subject.ssh_args
      assert_equal @host_ssh_args, subject.host_ssh_args
    end

    should "know its cmd str" do
      assert_equal @cmd_str,        subject.cmd_str
      assert_equal subject.cmd_str, subject.to_s
    end

    should "build ssh cmd strs given a host" do
      host = Factory.string
      exp  = @remote_class.ssh_cmd_str(@cmd_str, host, @ssh_args, @host_ssh_args)
      assert_equal exp, subject.ssh_cmd_str(host)
    end

    should "build a local cmd for each of its hosts" do
      subject.hosts.each do |host|
        assert_instance_of Dk::Local::CmdSpy, subject.local_cmds[host]
        exp = subject.ssh_cmd_str(host)
        assert_equal exp, subject.local_cmds[host].cmd_str
      end
      exp_cmd_str = subject.ssh_cmd_str(subject.hosts.last)
      exp_opts = {
        :env          => @opts[:env],
        :dry_tree_run => @opts[:dry_tree_run]
      }
      assert_equal [exp_cmd_str, exp_opts], @local_cmd_spy_new_called_with

      cmd = @cmd_class.new(Dk::Local::Cmd, @cmd_str, @opts)
      cmd.hosts.each do |host|
        assert_instance_of Dk::Local::Cmd, cmd.local_cmds[host]
        exp = subject.ssh_cmd_str(host)
        assert_equal exp, cmd.local_cmds[host].cmd_str
      end
      exp_cmd_str = subject.ssh_cmd_str(cmd.hosts.last)
      exp_opts = {
        :env          => @opts[:env],
        :dry_tree_run => @opts[:dry_tree_run]
      }
      assert_equal [exp_cmd_str, exp_opts], @local_cmd_new_called_with
    end

    should "start and wait on each of its local cmds' scmd when run" do
      subject.hosts.each do |host|
        assert_false subject.local_cmds[host].scmd.start_called?
        assert_false subject.local_cmds[host].scmd.wait_called?
      end

      input = Factory.string
      subject.run(input)

      subject.hosts.each do |host|
        assert_true subject.local_cmds[host].scmd.start_called?
        assert_true subject.local_cmds[host].scmd.wait_called?
        assert_equal input, subject.local_cmds[host].scmd.start_calls.last.input
      end
    end

    should "use the stdout/stderr of the first non-empty stdout/stderr" do
      outs = subject.hosts.map{ |h| subject.local_cmds[h].stdout.to_s }
      exp = outs.find{ |o| !o.empty? } || ''
      assert_equal exp, subject.stdout

      errs = subject.hosts.map{ |h| subject.local_cmds[h].stderr.to_s }
      exp = errs.find{ |e| !e.empty? } || ''
      assert_equal exp, subject.stderr
    end

    should "know whether it was successful or not" do
      subject.hosts.each do |host|
        Assert.stub(subject.local_cmds[host], :success?){ true }
      end
      assert_true subject.success?

      Assert.stub(subject.local_cmds[subject.hosts.sample], :success?){ false }
      assert_false subject.success?
    end

    should "know its output lines" do
      exp = []
      subject.hosts.each do |host|
        subject.local_cmds[host].output_lines.each do |ol|
          exp << [host, ol]
        end
      end
      output_lines = subject.output_lines

      assert_equal exp.size, output_lines.size

      if output_lines.size > 0
        assert_instance_of BaseCmd::OutputLine, output_lines.sample
      end

      assert_equal exp.map{ |(h, ol)| h },       output_lines.map(&:host)
      assert_equal exp.map{ |(h, ol)| ol.name }, output_lines.map(&:name)
      assert_equal exp.map{ |(h, ol)| ol.line }, output_lines.map(&:line)
    end

  end

  class CmdTests < UnitTests
    desc "Cmd"
    setup do
      @cmd_class = @remote_class::Cmd
      @cmd = @cmd_class.new(@cmd_str, :hosts => @hosts)
    end
    subject{ @cmd }

    should "build a local cmd for each host with the cmd str, given opts" do
      subject.hosts.each do |host|
        assert_instance_of Dk::Local::Cmd, subject.local_cmds[host]
        exp = ssh_cmd_str(@cmd_str, host, subject.ssh_args, subject.host_ssh_args)
        assert_equal exp, subject.local_cmds[host].cmd_str
      end
      exp_cmd_str = ssh_cmd_str(
        @cmd_str,
        subject.hosts.last,
        subject.ssh_args,
        subject.host_ssh_args
      )
      exp_opts = {
        :env          => nil,
        :dry_tree_run => nil
      }
      assert_equal [exp_cmd_str, exp_opts], @local_cmd_new_called_with

      cmd  = @cmd_class.new(@cmd_str, @opts)
      cmd.hosts.each do |host|
        assert_instance_of Dk::Local::Cmd, cmd.local_cmds[host]
        exp = ssh_cmd_str(@cmd_str, host, cmd.ssh_args, cmd.host_ssh_args)
        assert_equal exp, cmd.local_cmds[host].cmd_str
      end
      exp_cmd_str = ssh_cmd_str(
        @cmd_str,
        cmd.hosts.last,
        cmd.ssh_args,
        cmd.host_ssh_args
      )
      exp_opts = {
        :env          => @opts[:env],
        :dry_tree_run => @opts[:dry_tree_run]
      }
      assert_equal [exp_cmd_str, exp_opts], @local_cmd_new_called_with
    end

  end

  class CmdSpyTests < UnitTests
    desc "CmdSpy"
    setup do
      @cmd_class = @remote_class::CmdSpy
      @cmd = @cmd_class.new(@cmd_str, :hosts => @hosts)
    end
    subject{ @cmd }

    should have_readers :cmd_opts
    should have_imeths :run_input, :stdout=, :stderr=, :exitstatus=
    should have_imeths :run_calls, :run_called?, :ssh?

    should "build a local cmd spy for each host with the cmd str, given opts" do
      subject.hosts.each do |host|
        assert_instance_of Dk::Local::CmdSpy, subject.local_cmds[host]
        exp = ssh_cmd_str(@cmd_str, host, subject.ssh_args, subject.host_ssh_args)
        assert_equal exp, subject.local_cmds[host].cmd_str
      end
      exp_cmd_str = ssh_cmd_str(
        @cmd_str,
        subject.hosts.last,
        subject.ssh_args,
        subject.host_ssh_args
      )
      exp_opts = {
        :env          => nil,
        :dry_tree_run => nil
      }
      assert_equal [exp_cmd_str, exp_opts], @local_cmd_spy_new_called_with

      cmd = @cmd_class.new(@cmd_str, @opts)
      cmd.hosts.each do |host|
        assert_instance_of Dk::Local::CmdSpy, cmd.local_cmds[host]
        exp = ssh_cmd_str(@cmd_str, host, cmd.ssh_args, cmd.host_ssh_args)
        assert_equal exp, cmd.local_cmds[host].cmd_str
      end
      exp_cmd_str = ssh_cmd_str(
        @cmd_str,
        cmd.hosts.last,
        cmd.ssh_args,
        cmd.host_ssh_args
      )
      exp_opts = {
        :env          => @opts[:env],
        :dry_tree_run => @opts[:dry_tree_run]
      }
      assert_equal [exp_cmd_str, exp_opts], @local_cmd_spy_new_called_with
    end

    should "know the input it was run with" do
      input = Factory.string

      assert_nil subject.run_input
      subject.run(input)
      assert_equal input, subject.run_input
    end

    should "demeter its first local cmd spy" do
      first_local_cmd_spy = subject.local_cmds[subject.hosts.first]

      stdout = Factory.stdout
      subject.stdout = stdout
      assert_equal stdout, first_local_cmd_spy.scmd.stdout

      stderr = Factory.stderr
      subject.stderr = stderr
      assert_equal stderr, first_local_cmd_spy.scmd.stderr

      exitstatus = Factory.exitstatus
      subject.exitstatus = exitstatus
      assert_equal exitstatus, first_local_cmd_spy.scmd.exitstatus

      subject.run
      assert_equal first_local_cmd_spy.scmd.start_calls,   subject.run_calls
      assert_equal first_local_cmd_spy.scmd.start_called?, subject.run_called?
    end

    should "be ssh" do
      assert_true subject.ssh?
    end

  end

end
