$LOAD_PATH.push File.expand_path("../lib", File.dirname(__FILE__))

unless defined?(Bundler)
  require 'rubygems'
end


unless RUBY_VERSION =~ /^1\.8\./
  require 'simplecov'
  SimpleCov.coverage_dir 'coverage/outside'
  SimpleCov.start do
    add_filter '/spec/'
    add_filter '/features/'
    add_filter '/mock/'
    add_filter '/lib/vendor/'
    add_group 'CLI Workflows', 'lib/engineyard-serverside/cli/workflows/'
    add_group 'Callbacks', 'lib/engineyard-serverside/callbacks'
  end
end

require 'pp'
require 'engineyard-serverside'
require 'engineyard-serverside-adapter'
require 'support/integration'
require 'support/source_doubles'
require 'support/timecop'

FIXTURES_DIR = Pathname.new(__FILE__).dirname.join("fixtures")
TMPDIR = Pathname.new(__FILE__).dirname.parent.join('tmp')
GROUP = `id -gn`.strip
INTERNAL_KEY = Pathname.new("~/.ssh/id_rsa").expand_path

module EY
  module Serverside
    def self.dna_json=(j)
      @dna_json = j
      @node = nil
    end
  end
end

module SpecDependencyHelpers
  $NPM_INSTALLED = system('which npm 2>&1')
  unless $NPM_INSTALLED
    $stderr.puts "npm not found; skipping Node.js specs."
  end

  def with_npm_mocked(&block)
    context("mocked") { yield true }
    context("unmocked") { yield false } if $NPM_INSTALLED
  end

  $COMPOSER_INSTALLED = system('command -v composer > /dev/null')
  if $COMPOSER_INSTALLED
    $stderr.puts "composer found; skipping tests that expect it to be missing."
  else
    $stderr.puts "composer not found; skipping tests that expect it to be available."
  end

  def with_composer_mocked(&block)
    context("mocked") { yield true }
    context("unmocked") { yield false } if $COMPOSER_INSTALLED
  end
end

RSpec.configure do |config|
  config.extend SpecDependencyHelpers

  config.before(:all) do
    make_tmpdir
    EY::Serverside.dna_json = MultiJson.dump({})
  end

  config.after(:all) do
    delete_tmpdir
  end

  class VerboseStringIO < StringIO
    def <<(str)
      if ENV['VERBOSE'] || ENV['DEBUG']
        STDERR << str
      end
      super
    end
  end

  def tmpdir
    TMPDIR
  end

  def make_tmpdir
    tmpdir.mkpath
  end

  def delete_tmpdir
    tmpdir.exist? && tmpdir.rmtree
  end

  def stdout
    @stdout ||= VerboseStringIO.new
  end

  def stderr
    @stderr ||= VerboseStringIO.new
  end

  def read_stdout
    stdout.rewind
    stdout.read
  end

  def read_stderr
    stderr.rewind
    stderr.read
  end

  def read_output
    read_stdout + "\n" + read_stderr
  end

  def capture
    begin
      $stdout = @stdout = VerboseStringIO.new
      $stderr = @stderr = VerboseStringIO.new
      yield
    ensure
      $stdout, $stderr = STDOUT, STDERR
    end
  end

  def test_shell(verbose=true)
    @test_shell ||= begin
                      @log_path = tmpdir.join("serverside-deploy-#{Time.now.to_f}-#{$$}.log")
                      EY::Serverside::Shell.new(:verbose => verbose, :log_path => @log_path, :stdout => stdout, :stderr => stderr)
                    end
  end

  def exist
    be_exist
  end

  def bindir
    @bindir ||= begin
                  dir = tmpdir.join("ey_test_cmds_#{Time.now.to_f}_#{$$}")
                  dir.mkpath
                  dir
                end
  end

  def mock_command(cmd, contents, &block)
    bindir.join(cmd).open('w') do |f|
      f.write contents
      f.chmod(0755)
    end
    with_mocked_commands(&block) if block_given?
  end

  def mock_bundler(failure = false, &block)
    mock_command('bundle', <<-SCRIPT, &block)
#!#{`which ruby`}
puts "Bundling gems"
$stdout.flush
#{failure && '$stderr.puts "bundle install failure"; exit 1'}
    SCRIPT
  end

  def mock_npm(&block)
    mock_command('npm', <<-SCRIPT, &block)
#!/bin/bash
echo "Running npm with $@"
    SCRIPT
  end

  def mock_composer(&block)
    mock_command('composer', <<-SCRIPT, &block)
#!/bin/bash
echo "Running composer with $@"
    SCRIPT
  end

  def mock_sudo(&block)
    mock_command('sudo', <<-SCRIPT, &block)
#!/bin/bash
echo "$@"
exec "$@"
    SCRIPT
  end

  def with_mocked_commands(&block)
    with_env('PATH' => "#{bindir}:#{ENV['PATH']}", &block)
  end

  def with_env(new_env_vars)
    raise ArgumentError, "with_env takes a block" unless block_given?

    old_env_vars = {}
    new_env_vars.each do |k, v|
      if ENV.has_key?(k)
        old_env_vars[k] = ENV[k]
      end
      ENV[k] = v if v
    end

    yield
  ensure
    new_env_vars.keys.each do |k|
      if old_env_vars.has_key?(k)
        ENV[k] = old_env_vars[k]
      else
        ENV.delete(k)
      end
    end
  end


  def deploy_dir
    @deploy_dir ||= tmpdir.join("serverside-deploy-#{Time.now.to_f}-#{$$}")
  end

  def whoami
    ENV['USER']
  end

  # set up EY::Serverside::Server like we're on a solo
  def test_servers
    @test_servers ||= EY::Serverside::Servers.from_hashes([{:hostname => 'localhost', :roles => %w[solo], :user => whoami}], test_shell)
  end

  def default_configuration
    {
      "source_class"     => "IntegrationSpec",
      "deploy_to"        => deploy_dir.to_s,
      "group"            => GROUP,
      "stack"            => 'nginx_passenger',
      "migrate"          => "ruby -e 'puts ENV[\"PATH\"]' > #{deploy_dir}/path-when-migrating",
      "app"              => 'rails31',
      "environment_name" => 'env',
      "account_name"     => 'acc',
      "framework_env"    => 'staging',
      "branch"           => 'somebranch',
      "verbose"          => true,
      "git"              => FIXTURES_DIR.join('repos', 'default'),
    }
  end

  def test_adapter(repo_fixture_name = 'default', extra_config = {})
    options = default_configuration.merge({ "git" => FIXTURES_DIR.join('repos', repo_fixture_name)}).merge(extra_config)

    # pretend there is a shared bundled_gems directory
    deploy_dir.join('shared', 'bundled_gems').mkpath
    %w(RUBY_VERSION SYSTEM_VERSION).each do |name|
      deploy_dir.join('shared', 'bundled_gems', name).open("w") { |f| f.write("old\n") }
    end

    EY::Serverside::Adapter.new do |args|
      args.app                = options['app']
      args.environment_name   = options['environment_name']
      args.account_name       = options['account_name']
      args.migrate            = options['migrate']
      args.ref                = options['branch']
      args.git                = options['git']
      args.serverside_version = Gem::Version.create(EY::Serverside::VERSION.dup).release
      args.config             = {
        "services_check_command" => "which echo",
        "services_setup_command" => "echo 'services setup command'",
        "source_class"           => options["source_class"],
        "deploy_to"              => options["deploy_to"],
        "group"                  => options["group"],
      }.merge(options['config'] || {})
      args.framework_env      = options['framework_env']
      args.stack              = options['stack']
      args.verbose            = options['verbose']
      args.clean              = options['clean']
      args.instances          = test_servers.map {|s| {:hostname => s.hostname, :roles => s.roles.to_a, :name => s.name} }
    end
  end

  # When a repo fixture name is specified, the files found in the specified
  # spec/fixtures/repos dir are copied into the test github repository.
  def deploy_test_application(repo_fixture_name = 'default', extra_config = {}, &block)
    Timecop.travel(1)
    @adapter = test_adapter(repo_fixture_name, extra_config)
    @argv = @adapter.deploy.commands.last.to_argv[2..-1]

    FullTestDeploy.on_create_callback = block

    mock_bundler(extra_config['bundle_install_fails'])
    with_mocked_commands do
      capture do
        EY::Serverside::CLI::App.start(@argv)
      end
    end
  ensure
    @deployer = EY::Serverside::Deploy.deployer
    @config = EY::Serverside::Deploy.config
  end

  def redeploy_test_application(extra_config = {}, &block)
    Timecop.travel(1)
    raise "Please deploy_test_application first" unless @argv
    bundle_install_fails = extra_config.delete('bundle_install_fails')

    @action = @adapter.deploy do |args|
      extra_config.each do |key,val|
        case key
        when 'branch' then args.ref    = val
        when 'config' then args.config = args.config.merge(val || {})
        else               args.send("#{key}=", val)
        end
      end
    end

    @argv = @action.commands.last.to_argv[2..-1]

    FullTestDeploy.on_create_callback = block

    mock_bundler(bundle_install_fails)

    with_mocked_commands do
      capture do
        EY::Serverside::CLI::App.start(@argv)
      end
    end
  ensure
    @deployer = EY::Serverside::Deploy.deployer
    @config = EY::Serverside::Deploy.config
  end

  def enable_maintenance(extra_adapter_config = {})
    @adapter = test_adapter("default", extra_adapter_config)
    @argv = @adapter.enable_maintenance.commands.last.to_argv[2..-1]

    with_mocked_commands do
      capture do
        EY::Serverside::CLI::App.start(@argv)
      end
    end
  end

  def disable_maintenance(extra_adapter_config = {})
    @adapter = test_adapter("default", extra_adapter_config)
    @argv = @adapter.disable_maintenance.commands.last.to_argv[2..-1]

    with_mocked_commands do
      capture do
        EY::Serverside::CLI::App.start(@argv)
      end
    end
  end

  def maintenance_status(extra_adapter_config = {})
    @adapter = test_adapter("default", extra_adapter_config)
    @argv = @adapter.maintenance_status.commands.last.to_argv[2..-1]

    with_mocked_commands do
      capture do
        EY::Serverside::CLI::App.start(@argv)
      end
    end
  end
end
