module Capistrano
  module Spec
    module ConfigurationExtension
      def get(remote_path, path, options={}, &block)
        gets[remote_path] = {:path => path, :options => options, :block => block}
      end

      def gets
        @gets ||= {}
      end

      def run(cmd, options={}, &block)
        runs[cmd] = {:options => options, :block => block}
        if (stub = stubbed_commands[cmd])
          raise ::Capistrano::CommandError if stub[:fail]
          raise stub[:raise] if stub[:raise]

          data = stub[:data]
          data = stub[:with].call(cmd) if stub[:with].respond_to? :call

          block.call stub[:channel], stub[:stream], data if block_given?
        end
      end

      def runs
        @runs ||= {}
      end

      def upload(from, to, options={}, &block)
        uploads[from] = {:to => to, :options => options, :block => block}
      end

      def uploads
        @uploads ||= {}
      end

      def stubbed_commands
        @stubbed_commands ||= {}
      end

      def stub_command(command, options = {}, &block)
        options[:with] = block if block_given?
        stubbed_commands[command] = { :stream => :out, :data => '' }.merge options
      end
    end

    module Helpers
      def find_callback(configuration, on, task)
        if task.kind_of?(String)
          task = configuration.find_task(task)
        end

        callbacks = configuration.callbacks[on]

        callbacks && callbacks.select do |task_callback|
          task_callback.applies_to?(task) || task_callback.source == task.fully_qualified_name
        end
      end

    end

    module Matchers
      extend ::RSpec::Matchers::DSL

      define :callback do |task_name|
        extend Helpers

        match do |configuration|
          @task = configuration.find_task(task_name)
          callbacks = find_callback(configuration, @on, @task)

          if callbacks
            if @after_task_name
              @after_task = configuration.find_task(@after_task_name)
              callbacks.any? { |callback| callback.applies_to?(@after_task) }
            elsif @before_task_name
              @before_task = configuration.find_task(@before_task_name)
              callbacks.any? { |callback| callback.applies_to?(@before_task) }
            else
              ! @callback.nil?
            end
          else
            false
          end
        end

        def on(on)
          @on = on
          self
        end

        def before(before_task_name)
          @on = :before
          @before_task_name = before_task_name
          self
        end

        def after(after_task_name)
          @on = :after
          @after_task_name = after_task_name
          self
        end

        failure_message_for_should do |actual|
          if @after_task_name
            "expected configuration to callback #{task_name.inspect} #{@on} #{@after_task_name.inspect}, but did not"
          elsif @before_task_name
            "expected configuration to callback #{task_name.inspect} #{@on} #{@before_task_name.inspect}, but did not"
          else
            "expected configuration to callback #{task_name.inspect} on #{@on}, but did not"
          end
        end

      end

      define :have_gotten do |path|
        match do |configuration|

          get = configuration.gets[path]
          if @to
            get && get[:path] == @to
          else
            get
          end
        end

        def to(to)
          @to = to
          self
        end

        failure_message_for_should do |actual|
          if @to
            "expected configuration to get #{path} to #{@to}, but did not"
          else
            "expected configuration to get #{path}, but did not"
          end
        end
      end

      define :have_uploaded do |path|
        @to = nil # Reset `to` because it will influence next match otherwise.

        match do |configuration|
          uploads = configuration.uploads
          uploads = uploads.select { |f, u| f == path } if path
          uploads = uploads.select { |f, u| u[:to] == @to } if @to
          uploads.any?
        end

        def to(to)
          @to = to
          self
        end

        failure_message_for_should do |actual|
          if @to
            "expected configuration to upload #{path} to #{@to}, but did not"
          else
            "expected configuration to upload #{path}, but did not"
          end
        end
      end

      define :have_run do |cmd|

        match do |configuration|
          run = configuration.runs[cmd]

          run
        end

        failure_message_for_should do |actual|
          "expected configuration to run #{cmd}, but did not"
        end

      end

    end
  end
end