require 'open3'

module Wukong
  module SpecHelpers

    # Provides a `command` method for writing integration tests for
    # commands.
    module IntegrationRunner

      # Spawn a command and capture its STDOUT, STDERR, and exit code.
      #
      # The `args` will be joined together into a command line.
      #
      # It is expected that you will use the matchers defined in
      # IntegrationMatchers in your integration tests:
      #
      # @example Check output of 'ls' includes a string 'foo.txt'
      #   it "lists files" do
      #     command('ls').should have_output('foo.txt')
      #   end
      #
      # @example More complicated
      #   context "long format" do
      #     it "lists files with timestamps" do
      #       command('ls', '-l').should have_output('foo.txt', /\w+ \d+ \d+:\d+/)
      #     end
      #   end
      #
      # @param [Array<String>] args
      #
      # @overload command(*args, options={})
      #   If the last element of `args` is a Hash it will be used for
      #   options.
      #
      #   The :env option specifies the command line environment to
      #   use for the command.  By default this will be the value of
      #   the Ruby process's own `ENV` variable.  If running in a
      #   context in which the `integration_env` method is defined,
      #   its return value will be merged on top of `ENV`.  An
      #   explicitly provided :env option will again be merged on top.
      #
      #   The :cwd option specifies the working directory to start in.
      #   It defaults to the value of <tt>Dir.pwd</tt>
      #
      #   @param [Array<String>] args
      #   @param [Hash] options
      #   @option options [Hash] env the shell environment to spawn the command with
      #   @option options [Hash] cwd the directory to execute the command in
      def command *args
        a = args.flatten.compact
        options = (a.last.is_a?(Hash) ? a.pop : {})

        env = ENV.to_hash.dup
        env.merge!(integration_env) if respond_to?(:integration_env)
        env.merge!(options[:env] || {})

        cwd   = options[:cwd]
        cwd ||= (respond_to?(:integration_cwd) ? integration_cwd : Dir.pwd)

        IntegrationDriver.new(a, cwd: cwd, env: env)
      end
    end

    # A driver for running commands in a subprocess.
    class IntegrationDriver

      # The command to execute
      attr_accessor :cmd
      
      # The directory in which to execute the command.
      attr_accessor :cwd

      # The ID of the spawned subprocess (while it was running).
      attr_accessor :pid

      # The STDOUT of the spawned process.
      attr_accessor :stdout

      # The STDERR of the spawned process.
      attr_accessor :stderr

      # The exit code of the spawned process.
      attr_accessor :exit_code

      # Run the command and capture its outputs and exit code.
      #
      # @return [true, false]
      def run!
        return false if ran?
        Open3.popen3(env, cmd) do |i, o, e, wait_thr|
          self.pid = wait_thr.pid

          @inputs.each { |input| i.puts(input) }
          i.close

          self.stdout    = o.read
          self.stderr    = e.read
          self.exit_code = wait_thr.value.to_i
        end
        @ran = true
      end

      # Initialize a new IntegrationDriver to run a given command.
      def initialize args, options
        @args   = args
        @env    = options[:env]
        @cwd    = options[:cwd]
        @inputs = []
      end

      def cmd
        @args.compact.map(&:to_s).join(' ')
      end

      def on *events
        @inputs.concat(events)
        self
      end

      def env
        ENV.to_hash.merge(@env || {})
      end

      def ran?
        @ran
      end
      
      def cmd_summary
        [
         cmd,
         "with env #{env_summary}",
         "in dir #{cwd}"
        ].join("\n")
      end

      def env_summary
        { "PATH" => env["PATH"], "RUBYLIB" => env["RUBYLIB"] }.inspect
      end
      
    end
  end
end