#!/usr/bin/env ruby

require 'rhc/coverage_helper'

require 'rhc-common'
require 'net/http'
require 'net/https'
require 'rbconfig'
require 'yaml'
require 'tempfile'
require 'rhc/vendor/sshkey'

require 'test/unit'
require 'test/unit/ui/console/testrunner'

#
# print help
#
def p_usage(exit_code = 255)
  rhlogin = get_var('default_rhlogin') ? "Default: #{get_var('default_rhlogin')}" : "required"
  puts <<USAGE

Usage: rhc domain status
Run a simple check on local configs and credentials to confirm tools are
properly setup.  Often run to troubleshoot connection issues.

  -l|--rhlogin   rhlogin    Red Hat login (RHN or OpenShift login) (#{rhlogin})
  -p|--password  password   Red Hat password (for RHN or OpenShift)
  -d|--debug                Print Debug info
  -h|--help                 Show Usage info
  --config  path            Path of alternate config file
  --timeout #               Timeout, in seconds, for the session

USAGE
exit exit_code
end

begin
  opts = GetoptLong.new(
    ["--debug", "-d", GetoptLong::NO_ARGUMENT],
    ["--help",  "-h", GetoptLong::NO_ARGUMENT],
    ["--rhlogin",  "-l", GetoptLong::REQUIRED_ARGUMENT],
    ["--password",  "-p", GetoptLong::REQUIRED_ARGUMENT],
    ["--config", GetoptLong::REQUIRED_ARGUMENT],
    ["--timeout", GetoptLong::REQUIRED_ARGUMENT]
  )
  $opt = {}
  opts.each do |o, a|
    $opt[o[2..-1]] = a.to_s
  end
rescue Exception => e
  #puts e.message
  p_usage
end

if $opt["help"]
  p_usage 0
end

if 0 != ARGV.length
  p_usage
end

# If provided a config path, check it
RHC::Config.check_cpath($opt)

# Need to store the config information so tests can use it
#  since they are technically new objects
$opts_config_path   = @opts_config_path 
$local_config_path  = @local_config_path
$opts_config        = @opts_config
$local_config       = @local_config
$global_config      = @global_config

# Pull in configs from files
$libra_server = get_var('libra_server')
$debug = get_var('debug') == 'false' ? nil : get_var('debug')


if $opt["debug"]
  $debug = true
end
RHC::debug($debug)

RHC::timeout($opt["timeout"], get_var('timeout'))
RHC::connect_timeout($opt["timeout"], get_var('timeout'))


$opt["rhlogin"] = get_var('default_rhlogin') unless $opt["rhlogin"]
if !RHC::check_rhlogin($opt['rhlogin'])
  p_usage
end
$rhlogin = $opt["rhlogin"]

$password = $opt['password']
if !$password
  $password = RHC::get_password
end

#
# Generic Info
#
$debuginfo = {
  'environment' => {
    'Ruby Version' => RUBY_VERSION,
    "host_alias" => (Object.const_get(defined?(RbConfig) ? :RbConfig : :Config))::CONFIG['host_alias'],
  },
  'options' => {
    "Command Line" => $opt,
    "Opts" => $opts_config,
    "Local" => $local_config,
    "Global" => $global_config
  }
}

# Don't actually log the password, but some information to help with troubleshooting
if $opt['password']
  $debuginfo['options']['Command Line']['password'] = {
    "Length" => $opt['password'].length, 
    "Starts with" => $opt['password'][0..0]
  }
end

#
# Check for proxy environment
#
if ENV['http_proxy']
  if ENV['http_proxy']!~/^(\w+):\/\// then
    ENV['http_proxy']="http://" + ENV['http_proxy']
  end
  proxy_uri=URI.parse(ENV['http_proxy'])
  $http = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
  $debuginfo['environment']['proxy'] = "#{proxy_uri.user}:#{proxy_uri.password}@#{proxy_uri.host}:#{proxy_uri.port}"
else
  $http = Net::HTTP
  $debuginfo['environment']['proxy'] = "none"
end

#####################################
#     Done setting up environment   #
#####################################

module TestBase
  def initialize(*args)
    super
    $connectivity ||= false
    # These need to be loaded for each test to be able to access them
    @opts_config_path   = $opts_config_path 
    @local_config_path  = $local_config_path
    @opts_config        = $opts_config
    @local_config       = $local_config
    @global_config      = $global_config
  end

  def fetch_url_json(uri, data)
    json_data = RHC::generate_json(data)
    url = URI.parse("https://#{$libra_server}#{uri}")
    req = $http::Post.new(url.path)
    req.set_form_data({'json_data' => json_data, 'password' => $password})
    http = $http.new(url.host, url.port)
    http.open_timeout = 10
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE

    response = http.start {|http| http.request(req)}
    return response
  ensure
    unless response.nil?
      $debuginfo['fetches'] ||= []
      body = RHC::json_decode(response.body)
      body['data'] = body['data'] && body['data'] != '' ? RHC::json_decode(body['data']) : ''
      $debuginfo['fetches'] << {url.to_s => {
        'body' => body,
        'header' => response.instance_variable_get(:@header)
      }}
    end
  end

  def continue_test
    begin
      yield
    rescue Test::Unit::AssertionFailedError => e
      self.send(:add_failure,e.message,e.backtrace)
    end
  end
end

#####################################################
#                Tests start here                   #
#####################################################
#                                                   #
#  Note: Tests and testcases are run alphabetically #
#                                                   #
#####################################################

def error_for(name,*args)
  # Make sure the name is a downcased symbol
  name = name.to_s.downcase.to_sym

  # Create message template
  message = [:errors,:solutions].map{|x| get_message(x,name) }.compact.join("\n")

  # Params for message template
  params = args.flatten

  # Ensure we have the right number of placeholders
  num_placeholders = message.scan("%s").length
  unless num_placeholders == params.length
    warn("\nWARNING: error_for called with wrong number of args from: \n\t%s\n" % caller[0]) if $debug
    # This will ensure we have the right number of arguments, but should not get here
    params << Array.new(num_placeholders,"???")
  end

  # Return the message
  message % params.flatten
end

def get_message(type,name)
  val = $messages[type][name]
  if val.is_a? Symbol
    val = get_message(type,val)
  end
  val
end

# This test tries to make sure we have the keys unlocked before testing 
#   to ensure nicer workflow
class Test0_SSH_Keys_Unlocked < Test::Unit::TestCase
  include TestBase
  
  def test_ssh_quick
    begin
      # Get user info from OpenShift
      data = {'rhlogin' => $rhlogin}
      response = fetch_url_json("/broker/userinfo", data)
      resp_json = RHC::json_decode(response.body)
      user_info = RHC::json_decode(resp_json['data'].to_s)

      # Get any keys Net::SSH thinks we'll need
      $ssh = Net::SSH
      needed_keys = 
      user_info['app_info'].map do |name,opts|
        host = "%s-%s.%s" % [
          name,
          user_info['user_info']['namespace'],
          user_info['user_info']['rhc_domain']
        ]
        $ssh.configuration_for(host)[:keys].map{|f| File.expand_path(f)}
      end.compact.flatten

      agent_keys = Net::SSH::Authentication::Agent.connect.identities.map{|x| x.comment }
      missing_keys = needed_keys - agent_keys

      unless missing_keys.empty?
        $stderr.puts "\n  NOTE: These tests may require you to unlock one or more of your SSH keys \n\n"
      end
    rescue
    end
  end

end

class Test1_Connectivity < Test::Unit::TestCase
  include TestBase
  def teardown
    # Set global variable in case we cannot connect to the server
    if method_name == 'test_connectivity'
      $connectivity = passed?
    end
  end

  #
  # Checking Connectivity / cart list
  #
  def test_connectivity
    data = {'cart_type' => 'standalone'}

    response = fetch_url_json("/broker/cartlist", data)
    assert_equal 200, response.code.to_i, error_for(:no_connectivity, $libra_server)
  end
end

class Test2_Authentication < Test::Unit::TestCase
  include TestBase
  #
  # Checking Authentication
  #
  def test_authentication
    assert $connectivity, error_for(:cant_connect)

    data = {'rhlogin' => $rhlogin}
    response = fetch_url_json("/broker/userinfo", data)
    resp_json = RHC::json_decode(response.body)

    case response.code.to_i
    when 404
      assert false, error_for(:_404, $rhlogin)     
    when 401
      assert false, error_for(:_401, $rhlogin)     
    when 200
      assert true
    else
      assert false, error_for(:_other_http, resp_json['result'])
    end

    $user_info = RHC::json_decode(resp_json['data'].to_s)
  end
end

#
# Checking ssh key
#
class Test3_SSH < Test::Unit::TestCase


  include TestBase

  def setup
    @libra_kfile = get_kfile(false)
    if @libra_kfile
      @libra_kpfile = get_kpfile(@libra_kfile, false)
    end
  end

  def require_login(test)
    flunk(error_for(:no_account,test)) if $user_info.nil?
  end

  def require_domain(test)
    flunk(error_for(:no_domain,test)) if $user_info['user_info']['domains'].empty?
  end

  def require_remote_keys(test)
    require_login(test)
    require_domain(test)
    @@remote_pub_keys ||= (
      ssh_keys = RHC::get_ssh_keys($libra_server, $rhlogin, $password, $http)
      my_keys = [ssh_keys['ssh_key'], ssh_keys['keys'].values.map{|k| k['key']}].flatten
      my_keys.delete_if{|x| x.length == 0 }
      my_keys
    )

    missing_keys = [:nil?,:empty?].map{|f| @@remote_pub_keys.send(f) rescue false }.inject(:|)

    flunk(error_for(:no_remote_pub_keys,test)) if missing_keys
  end

  def require_agent_keys(fatal = true)
    @@agent_keys ||= (
      begin
        Net::SSH::Authentication::Agent.connect.identities
      rescue
        nil
      end
    )
    flunk(error_for(:no_keys_loaded)) if (fatal && @@agent_keys.nil?)
  end

  def agent_key_names
    @@agent_keys.map{|x| File.expand_path(x.comment) }
  end

  def agent_key_fingerprints
    @@agent_keys.map{|x| x.to_s.split("\n")[1..-2].join('') }
  end

  def libra_public_key
    @@local_ssh_pubkey ||= (
    fp = File.open(@libra_kpfile)
      fp.gets.split(' ')[1]
    )
  ensure
    fp.close if fp
  end

  def test_01_local_files
    [
      [@libra_kfile, /[4-7]00/], # Needs to at least be readable by user and nobody else
      [@libra_kpfile, /.../], # Any permissions are OK
    ].each do |args|
      continue_test{check_permissions(*args)}
    end
  end

  def test_02_ssh_agent
    require_agent_keys

    assert agent_key_names.include?(File.expand_path(@libra_kfile)) ,error_for(:pubkey_not_loaded, ": #{@libra_kpfile}")
  end

  def test_03_remote_ssh_keys
    require_remote_keys("whether your local SSH keys match the ones in your account")
    require_agent_keys(false)

    assert !(@@remote_pub_keys & [agent_key_fingerprints,libra_public_key].flatten).empty? ,error_for(:pubkey_not_loaded," ")
  end

  def test_04_ssh_connect
    require_login("connecting to your applications")
    require_domain("connecting to your applications")

    host_template = "%%s-%s.%s" % [
      $user_info['user_info']['domains'][0]['namespace'],
      $user_info['user_info']['rhc_domain']
    ]
    $user_info['app_info'].each do |k,v|
      uuid = v['uuid']
      hostname = host_template % k
      timeout = 10
      begin
        @ssh = Net::SSH.start(hostname,uuid,{:timeout => timeout})
      rescue Timeout::Error
        if timeout < 30
          timeout += 10
          retry
        end
      ensure
        continue_test{ assert_not_nil @ssh, error_for(:cant_ssh, hostname) }
        @ssh.close if @ssh
      end
    end
  end

  private
  def check_permissions(file,permission)
    file = File.expand_path(file)

    assert File.exists?(file), error_for(:file_not_found,file)

    perms = sprintf('%o',File.stat(file).mode)[-3..-1]

    assert_match permission, perms, error_for(:bad_permissions,[file,perms],permission.source)
  end
end

############################################################
#     Below this line is the custom Test::Runner code      #
# Modification should not be necessary to add/modify tests #
############################################################

class CheckRunner < Test::Unit::UI::Console::TestRunner
  def initialize(*args)
    super
    @output_level = 1
    puts get_message(:status,:started)
  end

  def add_fault(fault)
    @faults << fault
    print(fault.single_character_display)
    @already_outputted = true
  end

  def print_underlined(string)
    string = "||  #{string}  ||"
    line = "=" * string.length
    puts
    puts line
    puts string
    puts line
  end

  def render_message(message)
    lines = message.send(message.respond_to?(:lines) ? :lines : :to_s).to_a
    message = [] 
    @num ||= 0
    message << "#{@num+=1}) #{lines.shift.strip}"
    lines.each do |line|
      # Break if we get the standard assert error information or a blank line
      break if line.match(/^\<.*?> /) or line.match(/^\s*$/)
      message << "\t#{line.strip}"
    end
    message.join("\n")
  end

  def finished(*args)
    super
    $debuginfo['errors'] = @faults

    if @faults.empty?
      print_underlined get_message(:status,:passed)
    else
      print_underlined get_message(:status,:failed)
      @errors = []
      # Need to separate the failures from the errors

      @faults.each do |f|
        case f
        when Test::Unit::Failure
          puts render_message(f.message)
        when Test::Unit::Error
          @errors << f
        end
      end

      # Errors mean something in the test is broken, not just failed
      unless @errors.empty?
        @num = 0
        print_underlined get_message(:status,:error)
        @errors.each do |e| 
          display = e.long_display
          lines = display.send(display.respond_to?(:lines) ? :lines : :to_s).to_a[1..-1]
          puts "#{@num+=1}) #{lines.shift}"
          lines.each{|l| puts "\t#{l}"}
        end 
      end

    end

    if $debug
      Tempfile.open(%w(rhc-chk. .log))do |file|
        ObjectSpace.undefine_finalizer(file) # persist tempfile
        file.write $debuginfo.to_yaml
        puts
        puts "Debugging information dumped to #{file.path}"
      end
    end
  end
end

Test::Unit::AutoRunner::RUNNERS[:console] = proc do |r|
  CheckRunner
end

############################
#  The following are the error messages to be used for different tests
#  You need to specify a value in :errors
#  If there is a solution, it may also be specified in :solutions
#
#  Values in may be reused by specifying another symbol as the value
#   for instance the following will use the solution for :bar as the solution for :foo
#
#   :solutions
#     :foo: :bar
############################
$messages = YAML.load <<-EOF
---
:status:
  :started: Analyzing system
  :passed: Congratulations, your system has passed all tests
  :failed: Your system did not pass all of the tests
  :error: Something went wrong, and not all tests completed
:errors:
  :no_derive: "We were unable to derive your public SSH key and compare it to the remote"
  :no_connectivity: "Cannot connect to server, therefore most tests will not work"
  :no_account: "You must have an account on the server in order to test: %s"
  :no_domain: "You must have a domain associated with this account in order to test: %s"
  :cant_connect: You need to be able to connect to the server in order to test authentication
  :no_match_pub: "Local %s does not match remote pub key, SSH auth will not work"
  :_404: "The user %s does not have an account on this server"
  :no_pubkey: We were unable to read your public SSH key to compare to %s
  :_401: "Invalid user credentials for %s"
  :file_not_found: File %s does not exist, cannot check permissions
  :_other_http: "There was an error communicating with the server: %s"
  :bad_permissions: "File %s has incorrect permissions %s"
  :no_keys_loaded: Either ssh-agent is not running or you do not have any keys loaded 
  :pubkey_not_loaded: "Your public key is not loaded into a running ssh-agent%s"
  :cant_ssh: "Cannot SSH into your app: %s"
  :no_remote_pub_keys: "You do not have any public keys associated with your OpenShift account. Cannot test: %s"
:solutions:
  :no_connectivity: "Ensure that you are able to connect to %s"
  :no_match_pub: "Perhaps you should regenerate your public key, or run 'rhc sshkey add' to add your new key"
  :_404: "Please ensure that you've entered the correct username or that you've created an account"
  :_401: "Please ensure you used the correct password"
  :bad_permissions: "Permissions should match %s"
  :no_remote_pub_keys: "You should try to add your ssh-key by running 'rhc sshkey add'"
  :pubkey_not_loaded: If this is your only error, your connection may still work, depending on your SSH configuration
  :no_keys_loaded: If this is your only error, your connection may still work, depending on your SSH configuration
EOF
