# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with this
# work for additional information regarding copyright ownership. The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.


require File.expand_path('../spec_helpers', File.dirname(__FILE__))
require 'stringio'
Sandbox.require_optional_extension 'buildr/drb'


describe Buildr::DRbApplication do

  module DRbHelper
    attr_accessor :app, :drb, :cfg

    def use_stdio(stdin = nil, stdout = nil, stderr = nil)
      stdin ||= StringIO.new
      stdout ||= StringIO.new
      stderr ||= StringIO.new
      cfg.update :in => stdin, :out => stdout, :err => stderr
    end

    def remote_run(*argv)
      cfg.update :argv => argv
      drb.remote_run(cfg)
    end

    def output
      cfg[:out].string
    end

    def write_buildfile(content = nil)
      write 'buildfile', content || %q{
        define('foo') do

          rule '.rbc' => '.rb' do |t|
            $stdout.puts "#{t.name} from #{t.source}"
          end

          task('hello') do
            $stdout.puts 'hi'
          end

          task('empty')

          task('no') do
            task('empty').enhance ['delete_me']
            task('empty') { $stdout.puts 'no' }
          end

          task('delete_me')

          task('create') do
            Rake::Task.define_task('created')
            rule '.rbc' => '.rb' do |t|
               $stdout.puts "#{t.name} from #{t.source}"
            end
          end

          task('exists') do
            $stdout.puts !!Buildr.application.lookup('created')
          end

          task('setopt', :name, :value) do |task, args|
            Buildr.application.options.send("#{args[:name]}=", args[:value])
          end
        end
      }
    end
  end

  include DRbHelper

  before(:each) do
    @in, @out, @err = $stdin, $stdout, $stderr
    @cfg = {
      :dir => Dir.pwd, :argv => [],
      :in => @in, :out => @out, :err => @err
    }
    @drb = Buildr::DRbApplication.clone
    @drb.send :setup
    @app = Buildr.application
  end

  after(:each) do
    $stdin, $stdout, $stderr = @in, @out, @err
  end

  describe '.run' do
    it 'starts server if no server is running' do
      drb.should_receive(:connect).and_raise DRb::DRbConnError
      drb.should_receive(:run_server!)
      drb.should_not_receive(:run_client)
      drb.run
    end

    it 'connects to an already started server' do
      drb.should_receive(:connect).and_return "client"
      drb.should_receive(:run_client).with "client"
      drb.should_not_receive(:run_server!)
      drb.run
    end
  end

  describe '.remote_run' do

    describe 'stdout' do
      it 'is redirected to client' do
        use_stdio
        Buildr.application.should_receive(:remote_run) do
          $stdout.puts "HELLO"
        end
        remote_run
        output.should eql("HELLO\n")
      end
    end

    describe 'stderr' do
      it 'is redirected to client' do
        use_stdio
        Buildr.application.should_receive(:remote_run) do
          $stderr.puts "HELLO"
        end
        remote_run
        cfg[:err].string.should eql("HELLO\n")
      end
    end

    describe 'stdin' do
      it 'is redirected to client' do
        use_stdio
        cfg[:in].should_receive(:gets).and_return("HELLO\n")
        result = nil
        Buildr.application.should_receive(:remote_run) do
          result = $stdin.gets
        end
        remote_run
        result.should eql("HELLO\n")
      end
    end

    describe 'server ARGV' do
      it 'is replaced with client argv' do
        Buildr.application.should_receive(:remote_run) do
          ARGV.should eql(['hello'])
        end
        remote_run 'hello'
      end
    end

    describe 'without buildfile loaded' do
      before(:each) do
        app.instance_eval { @rakefile = nil }
        write_buildfile
      end

      it 'should load the buildfile' do
        app.should_receive(:top_level)
        lambda { remote_run }.should run_task('foo')
      end
    end

    describe 'with unmodified buildfile' do

      before(:each) do
        write_buildfile
        app.options.rakelib = []
        app.send :load_buildfile
        drb.save_snapshot(app)
      end

      it 'should not reload the buildfile' do
        app.should_not_receive(:reload_buildfile)
        app.should_receive(:top_level)
        remote_run
      end

      it 'should not define projects again' do
        use_stdio
        lambda { 2.times { remote_run 'foo:hello' } }.should_not run_task('foo')
        output.should eql("hi\nhi\n")
      end

      it 'should restore task actions' do
        use_stdio
        remote_run 'foo:empty'
        output.should be_empty
        2.times { remote_run 'foo:no' }
        remote_run 'foo:empty'
        actions = app.lookup('foo:empty').instance_eval { @actions }
        actions.should be_empty # as originally defined
        output.should be_empty
      end

      it 'should restore task prerequisites' do
        use_stdio
        remote_run 'foo:empty'
        output.should be_empty
        2.times { remote_run 'foo:no' }
        remote_run 'foo:empty'
        pres = app.lookup('foo:empty').send(:prerequisites).map(&:to_s)
        pres.should be_empty # as originally defined
        output.should be_empty
      end

      it 'should drop runtime created tasks' do
        remote_run 'foo:create'
        app.lookup('created').should_not be_nil
        remote_run 'foo:empty'
        app.lookup('created').should be_nil
      end

      it 'should restore options' do
        remote_run 'foo:setopt[bar,baz]'
        app.options.bar.should eql("baz")
        remote_run 'foo:empty'
        app.options.bar.should be_nil
      end

      it 'should restore rules' do
        orig = app.instance_eval { @rules.size }
        remote_run 'foo:create'
        app.instance_eval { @rules.size }.should eql(orig + 1)
        remote_run 'foo:empty'
        app.instance_eval { @rules.size }.should eql(orig)
      end

    end

    describe 'with modified buildfile' do

      before(:each) do
        write_buildfile
        app.options.rakelib = []
        app.send :load_buildfile
        drb.save_snapshot(app)
        app.instance_eval { @last_loaded = Time.now - 10 }
        write_buildfile %q{
          rule '.rbc' => '.rb' do |t|
            $stdout.puts "#{t.name} from #{t.source}"
          end
          define('foo') do
            task('hello') do
              $stdout.puts 'bye'
            end
            task('empty')
            define('bar') do

            end
          end
        }
      end

      it 'should reload the buildfile' do
        app.should_receive(:reload_buildfile)
        app.should_receive(:top_level)
        remote_run
      end

      it 'should redefine projects' do
        lambda { remote_run }.should run_tasks('foo', 'foo:bar')
      end

      it 'should remove tasks deleted from buildfile' do
        app.lookup('foo:delete_me').should_not be_nil
        remote_run
        app.lookup('foo:delete_me').should be_nil
      end

      it 'should redefine tasks actions' do
        actions = app.lookup('foo:empty').instance_eval { @actions }
        actions.should be_empty # no action
        app.lookup('foo:no').invoke # enhance the empty task
        actions = app.lookup('foo:empty').instance_eval { @actions }
        actions.should_not be_empty
        remote_run # cause to reload the buildfile
        actions = app.lookup('foo:empty').instance_eval { @actions }
        actions.should be_empty # as defined on the new buildfile
      end

      it 'should redefine task prerequisites' do
        pres = app.lookup('foo:empty').send(:prerequisites).map(&:to_s)
        pres.should be_empty # no action
        app.lookup('foo:no').invoke # enhance the empty task
        pres = app.lookup('foo:empty').send(:prerequisites).map(&:to_s)
        pres.should_not be_empty
        remote_run # cause to reload the buildfile
        pres = app.lookup('foo:empty').send(:prerequisites).map(&:to_s)
        pres.should be_empty # as defined on the new buildfile
      end

      it 'should drop runtime created tasks' do
        app.lookup('foo:create').invoke
        app.lookup('created').should_not be_nil
        remote_run 'foo:empty'
        app.lookup('created').should be_nil
      end

      it 'should restore options' do
        app.options.bar = 'baz'
        remote_run 'foo:empty'
        app.options.bar.should be_nil
      end

      it 'should redefine rules' do
        orig = app.instance_eval { @rules.size }
        app.lookup('foo:create').invoke
        app.instance_eval { @rules.size }.should eql(orig + 1)
        remote_run 'foo:empty'
        app.instance_eval { @rules.size }.should eql(orig)
      end

    end

  end
end
