require 'test_helper'

class TC_testHelp < Clean::Test::TestCase
  include TestHelper

  def setup
    @option_index = 0
    @real_columns = ENV['COLUMNS']
    ENV['COLUMNS'] = '1024'
    @output = StringIO.new
    @error = StringIO.new
    @command_names_used = []
    # Reset help command to its default state
    GLI::Commands::Help.skips_pre    = true
    GLI::Commands::Help.skips_post   = true
    GLI::Commands::Help.skips_around = true
  end

  def teardown
    ENV['COLUMNS'] = @real_columns
  end

  class TestApp
    include GLI::App
  end

  test_that "the help command is configured properly when created" do
    Given {
      app = TestApp.new
      app.subcommand_option_handling :normal
      @command = GLI::Commands::Help.new(app,@output,@error)
    }
    Then {
      assert_equal   'help',@command.name.to_s
      assert_nil     @command.aliases
      assert_equal   'command',@command.arguments_description
      assert_not_nil @command.description
      assert_not_nil @command.long_description
      assert         @command.skips_pre
      assert         @command.skips_post
      assert         @command.skips_around
    }
  end

  test_that "the help command can be configured to skip things declaratively" do
    Given {
      app = TestApp.new
      app.subcommand_option_handling :normal
      @command = GLI::Commands::Help.new(app,@output,@error)
      GLI::Commands::Help.skips_pre    = false
      GLI::Commands::Help.skips_post   = false
      GLI::Commands::Help.skips_around = false
    }
    Then {
      assert !@command.skips_pre
      assert !@command.skips_post
      assert !@command.skips_around
    }
  end

  test_that "the help command can be configured to skip things declaratively regardless of when it the object was created" do
    Given {
      GLI::Commands::Help.skips_pre    = false
      GLI::Commands::Help.skips_post   = false
      GLI::Commands::Help.skips_around = false
      app = TestApp.new
      app.subcommand_option_handling :normal
      @command = GLI::Commands::Help.new(app,@output,@error)
    }
    Then {
      assert !@command.skips_pre
      assert !@command.skips_post
      assert !@command.skips_around
    }
  end

  test_that "invoking help with no arguments results in listing all commands and global options" do
    Given a_GLI_app
    And {
      @command = GLI::Commands::Help.new(@app,@output,@error)
    }
    When {
      @command.execute({},{},[])
    }
    Then {
      assert_top_level_help_output
    }
  end

  test_that "invoking help with a command that doesn't exist shows an error" do
    Given a_GLI_app
    And {
      @command = GLI::Commands::Help.new(@app,@output,@error)
      @unknown_command_name = any_command_name
    }
    When {
      @command.execute({},{},[@unknown_command_name])
    }
    Then {
      assert_error_contained(/error: Unknown command '#{@unknown_command_name}'./)
    }
  end

  test_that "invoking help with a known command shows help for that command" do
    Given a_GLI_app
    And {
      @command_name = cm = any_command_name
      @desc         = d  = any_desc
      @long_desc    = ld = any_desc
      @switch       = s  = any_option
      @switch_desc  = sd = any_desc
      @flag         = f  = any_option
      @flag_desc    = fd = any_desc

      @app.instance_eval do
        desc d
        long_desc ld
        command cm do |c|

          c.desc sd
          c.switch s

          c.desc fd
          c.flag f

          c.action {}
        end
      end
      @command = GLI::Commands::Help.new(@app,@output,@error)
    }
    When {
      @command.execute({},{},[@command_name])
    }
    Then {
      assert_output_contained(@command_name,"Name of the command")
      assert_output_contained(@desc,"Short description")
      assert_output_contained(@long_desc,"Long description")
      assert_output_contained("-" + @switch,"command switch")
      assert_output_contained(@switch_desc,"switch description")
      assert_output_contained("-" + @flag,"command flag")
      assert_output_contained(@flag_desc,"flag description")
    }
  end

  test_that 'invoking help for an app with no global options omits [global options] from the usage string' do
    Given a_GLI_app(:no_options)
    And {
      @command = GLI::Commands::Help.new(@app,@output,@error)
    }
    When {
      @command.execute({},{},[])
    }
    Then {
      refute_output_contained(/\[global options\] command \[command options\] \[arguments\.\.\.\]/)
      refute_output_contained('GLOBAL OPTIONS')
      assert_output_contained(/command \[command options\] \[arguments\.\.\.\]/)
    }
  end

  test_that "invoking help with a known command when no global options are present omits [global options] from the usage string" do
    Given a_GLI_app(:no_options)
    And {
      @command_name = cm = any_command_name
      @desc         = d  = any_desc
      @long_desc    = ld = any_desc
      @switch       = s  = any_option
      @switch_desc  = sd = any_desc
      @flag         = f  = any_option
      @flag_desc    = fd = any_desc

      @app.instance_eval do
        desc d
        long_desc ld
        command cm do |c|

          c.desc sd
          c.switch s

          c.desc fd
          c.flag f

          c.action {}
        end
      end
      @command = GLI::Commands::Help.new(@app,@output,@error)
    }
    When {
      @command.execute({},{},[@command_name])
    }
    Then {
      refute_output_contained(/\[global options\]/)
      assert_output_contained(/\[command options\]/)
      assert_output_contained('COMMAND OPTIONS')
    }
  end

  test_that "invoking help with a known command when no global options nor command options are present omits [global options] and [command options] from the usage string" do
    Given a_GLI_app(:no_options)
    And {
      @command_name = cm = any_command_name
      @desc         = d  = any_desc
      @long_desc    = ld = any_desc

      @app.instance_eval do
        desc d
        long_desc ld
        command cm do |c|
          c.action {}
        end
      end
      @command = GLI::Commands::Help.new(@app,@output,@error)
    }
    When {
      @command.execute({},{},[@command_name])
    }
    Then {
      refute_output_contained(/\[global options\]/)
      refute_output_contained(/\[command options\]/)
      refute_output_contained('COMMAND OPTIONS')
    }
  end

  test_that "invoking help with a known command that has no command options omits [command options] from the usage string" do
    Given a_GLI_app
    And {
      @command_name = cm = any_command_name
      @desc         = d  = any_desc
      @long_desc    = ld = any_desc

      @app.instance_eval do
        desc d
        long_desc ld
        command cm do |c|
          c.action {}
        end
      end
      @command = GLI::Commands::Help.new(@app,@output,@error)
    }
    When {
      @command.execute({},{},[@command_name])
    }
    Then {
      assert_output_contained(/\[global options\]/)
      refute_output_contained(/\[command options\]/)
      refute_output_contained('COMMAND OPTIONS')
    }
  end

  test_that "omitting default_description doesn't blow up" do
    Given {
      app = TestApp.new
      app.instance_eval do
        subcommand_option_handling :normal
        command :top do |top|
          top.command :list do |list|
            list.action do |g,o,a|
            end
          end

          top.command :new do |new|
            new.action do |g,o,a|
            end
          end

          top.default_command :list
        end
      end
      @command = GLI::Commands::Help.new(app,@output,@error)
    }
    When {
      @code = lambda { @command.execute({},{},['top']) }
    }
    Then {
      assert_nothing_raised(&@code)
    }
  end

private

  def a_GLI_app(omit_options=false)
    lambda {
      @program_description = program_description = any_desc
      @flags = flags = [
        [any_desc.strip,any_arg_name,[any_option]],
        [any_desc.strip,any_arg_name,[any_option,any_long_option]],
      ]
      @switches = switches = [
        [any_desc.strip,[any_option]],
        [any_desc.strip,[any_option,any_long_option]],
      ]

      @commands = commands = [
        [any_desc.strip,[any_command_name]],
        [any_desc.strip,[any_command_name,any_command_name]],
      ]

      @app = TestApp.new
      @app.instance_eval do
        program_desc program_description
        subcommand_option_handling :normal

        unless omit_options
          flags.each do |(description,arg,flag_names)|
            desc description
            arg_name arg
            flag flag_names
          end

          switches.each do |(description,switch_names)|
            desc description
            switch switch_names
          end
        end

        commands.each do |(description,command_names)|
          desc description
          command command_names do |c|
            c.action {}
          end
        end
      end
    }
  end

  def assert_top_level_help_output
    assert_output_contained(@program_description)

    @commands.each do |(description,command_names)|
      assert_output_contained(/#{command_names.join(', ')}\s+-\s+#{description}/,"For command #{command_names.join(',')}")
    end
    assert_output_contained(/help\s+-\s+#{@command.description}/)

    @switches.each do |(description,switch_names)|
      expected_switch_names = switch_names.map { |_| _.length == 1 ? "-#{_}" : "--\\[no-\\]#{_}" }.join(', ')
      assert_output_contained(/#{expected_switch_names}\s+-\s+#{description}/,"For switch #{switch_names.join(',')}")
    end

    @flags.each do |(description,arg,flag_names)|
      expected_flag_names = flag_names.map { |_| _.length == 1 ? "-#{_}" : "--#{_}" }.join(', ')
      assert_output_contained(/#{expected_flag_names}[ =]#{arg}\s+-\s+#{description}/,"For flag #{flag_names.join(',')}")
    end

    assert_output_contained('GLOBAL OPTIONS')
    assert_output_contained('COMMANDS')
    assert_output_contained(/\[global options\] command \[command options\] \[arguments\.\.\.\]/)
  end

  def assert_error_contained(string_or_regexp,desc='')
    string_or_regexp = /#{string_or_regexp}/ if string_or_regexp.kind_of?(String)
    assert_match string_or_regexp,@error.string,desc
  end

  def assert_output_contained(string_or_regexp,desc='')
    string_or_regexp = /#{string_or_regexp}/ if string_or_regexp.kind_of?(String)
    assert_match string_or_regexp,@output.string,desc
  end

  def refute_output_contained(string_or_regexp,desc='')
    string_or_regexp = /#{string_or_regexp}/ if string_or_regexp.kind_of?(String)
    assert_no_match string_or_regexp,@output.string,desc
  end

  def any_option
    ('a'..'z').to_a[@option_index].tap { @option_index += 1 }
  end

  def any_long_option
    Faker::Lorem.words(10)[rand(10)]
  end

  def any_arg_name
    any_string :max => 20
  end

  def any_desc
    Faker::Lorem.words(10).join(' ')[0..30].gsub(/\s*$/,'')
  end

  def any_command_name
    command_name = Faker::Lorem.words(10)[rand(10)]
    while @command_names_used.include?(command_name)
      command_name = Faker::Lorem.words(10)[rand(10)]
    end
    @command_names_used << command_name
    command_name
  end
end
