require 'assert'
require 'dk/cli'

require 'dk/config'
require 'dk/dk_runner'
require 'dk/dry_runner'
require 'dk/null_logger'
require 'dk/task'
require 'dk/tree_runner'
require 'dk/version'

class Dk::CLI

  class UnitTests < Assert::Context
    desc "Dk::CLI"
    setup do
      @cli_class = Dk::CLI
    end
    subject{ @cli_class }

    should have_imeths :run

  end

  class InitTests < UnitTests
    desc "when init"
    setup do
      @kernel_spy = KernelSpy.new

      Dk.reset
      ENV['DK_CONFIG'] = ROOT_PATH.join('test/support/config/dk.rb').to_s
      @cli = Dk::CLI.new(@kernel_spy)

      null_logger = Dk::NullLogger.new
      Assert.stub(Dk.config, :dk_logger){ null_logger }
    end
    teardown do
      Dk.reset
    end
    subject{ @cli }

    should have_readers :clirb
    should have_imeths :run

    should "know its clirb" do
      assert_instance_of Dk::CLIRB, subject.clirb
    end

  end

  class RunTests < InitTests
    desc "and run with a configured task name"
    setup do
      @runner_init_with        = nil
      @log_cli_run_called_with = nil
      @log_cli_task_runs       = []
      @runner_runs             = []
      Assert.stub(Dk::DkRunner, :new) do |*args|
        @runner_init_with = args

        runner = Assert.stub_send(Dk::DkRunner, :new, *args)
        Assert.stub(runner, :run){ |*args| @runner_runs << args }
        Assert.stub(runner, :log_cli_run) do |*args, &block|
          @log_cli_run_called_with = args
          block.call
        end
        Assert.stub(runner, :log_cli_task_run) do |*args, &block|
          @log_cli_task_runs << args
          block.call
        end
        runner
      end
      @cli_args = ['cli-test-task', 'cli-other-task']
      @cli.run(*@cli_args)
    end

    should "build a live runner, log the CLI args, run the named tasks and exit" do
      assert_equal [Dk.config],           @runner_init_with
      assert_equal [@cli_args.join(' ')], @log_cli_run_called_with

      assert_equal 2, @log_cli_task_runs.size
      assert_equal [@cli_args.first], @log_cli_task_runs.first
      assert_equal [@cli_args.last],  @log_cli_task_runs.last

      assert_equal 2, @runner_runs.size
      assert_equal [CLITestTask],  @runner_runs.first
      assert_equal [CLIOtherTask], @runner_runs.last

      assert_equal 0, @kernel_spy.exit_status
    end

  end

  class RunWithDryRunFlagTests < InitTests
    desc "and run with the --dry-run flag"
    setup do
      @runner_init_with = nil
      @runner_run_with  = nil
      Assert.stub(Dk::DryRunner, :new) do |*args|
        @runner_init_with = args

        runner = Assert.stub_send(Dk::DryRunner, :new, *args)
        Assert.stub(runner, :run){ |*args| @runner_run_with = args }
        runner
      end
      @cli.run('cli-test-task', '--dry-run')
    end

    should "build dry runner and run the named task and exit" do
      assert_equal [Dk.config],   @runner_init_with
      assert_equal [CLITestTask], @runner_run_with

      assert_equal 0, @kernel_spy.exit_status
    end

  end

  class RunWithTreeFlagTests < InitTests
    desc "and run with the --tree flag"
    setup do
      @runner_init_with = nil
      @runner_run_with  = nil
      Assert.stub(Dk::TreeRunner, :new) do |*args|
        @runner_init_with = args

        runner = Assert.stub_send(Dk::TreeRunner, :new, *args)
        Assert.stub(runner, :run){ |*args| @runner_run_with = args }
        runner
      end
      @cli.run('cli-test-task', '--tree')
    end

    should "build tree runner and run the named task and exit" do
      assert_equal [Dk.config, @kernel_spy], @runner_init_with
      assert_equal [CLITestTask],            @runner_run_with

      assert_equal 0, @kernel_spy.exit_status
      assert_match /^building task tree\.\.\./, @kernel_spy.output

      kernel_spy = KernelSpy.new
      cli = Dk::CLI.new(kernel_spy)
      cli.run('cli-test-task', 'cli-test-task', '--tree')
      assert_match /^building task trees\.\.\./, kernel_spy.output
    end

  end

  class RunWithDashTFlagTests < InitTests
    desc "and run with the -T flag"
    setup do
      @cli.run(['-T', '--list-tasks'].sample)
    end

    should "list out the callable task details and exit" do
      max   = Dk.config.tasks.keys.map(&:size).max
      tasks = Dk.config.tasks.map do |(name, task_class)|
        "#{name.ljust(max)} # #{task_class.description}"
      end

      exp = "#{tasks.sort.join("\n")}\n"
      assert_equal exp, @kernel_spy.output
      assert_equal 0,   @kernel_spy.exit_status
    end

  end

  class RunWithVerboseFlagTests < InitTests
    desc "and run with the --verbose flag"
    setup do
      @cli.run('--verbose')
    end

    should "set the stdout log level to 'debug'" do
      assert_equal 'debug', Dk.config.stdout_log_level
    end

  end

  class RunWithHelpFlagTests < InitTests
    desc "and run with the --help flag"
    setup do
      @cli.run('--help')
    end

    should "print some help info and exit" do
      max   = Dk.config.tasks.keys.map(&:size).max
      tasks = Dk.config.tasks.map do |(name, task_class)|
        "    #{name.ljust(max)} # #{task_class.description}"
      end

      exp = "Usage: dk [TASKS] [options]\n\n" \
            "Tasks:\n" \
            "#{tasks.sort.join("\n")}\n\n" \
            "Options: #{subject.clirb.to_s}"
      assert_equal exp, @kernel_spy.output
      assert_equal 0,   @kernel_spy.exit_status
    end

  end

  class RunWithVersionFlagTests < InitTests
    desc "and run with the --version flag"
    setup do
      @cli.run('--version')
    end

    should "print some help info and exit" do
      exp = "#{Dk::VERSION}\n"
      assert_equal exp, @kernel_spy.output
      assert_equal 0,   @kernel_spy.exit_status
    end

  end

  class RunWithUnknownTaskTests < InitTests
    desc "and run with an unknown task(s)"
    setup do
      @task_names = Factory.integer(3).times.map{ Factory.string }
      @cli.run(*@task_names)
    end

    should "output to the user that the task is not known and exit" do
      exp = "No task named #{@task_names.map{ |n| "`#{n}`"}.join(', ')}"
      assert_includes exp, @kernel_spy.output
      assert_equal 1, @kernel_spy.exit_status
    end

  end

  class RunWithInvalidOptionTests < InitTests
    desc "and run with an invalid option"
    setup do
      @option_name = "--#{Factory.string}"
      @cli.run(@option_name)
    end

    should "output to the user that the option is invalid and exit" do
      exp = "invalid option: #{@option_name}"
      assert_includes exp, @kernel_spy.output
      assert_equal 1, @kernel_spy.exit_status
    end

  end

  class RunWithNoticeErrorTests < InitTests
    desc "and run with an error notification"
    setup do
      @exception = nil
      Assert.stub(subject.clirb, :parse!) do
        begin
          raise Dk::NoticeError, Factory.string
        rescue Dk::NoticeError => err
          @exception = err
          raise @exception
        end
      end
      @cli.run
    end

    should "output the error and exit" do
      exp = "\n\n#{@exception.message}\n\n#{@exception.backtrace.first}\n"
      assert_includes exp, @kernel_spy.output
      assert_equal 1, @kernel_spy.exit_status
    end

  end

  class RunWithAnErrorTests < InitTests
    desc "and run with an error"
    setup do
      Assert.stub(subject.clirb, :parse!){ raise StandardError, 'test' }
      @exception = nil
      Assert.stub(subject.clirb, :parse!) do
        begin
          raise StandardError, Factory.string
        rescue StandardError => err
          @exception = err
          raise @exception
        end
      end
      @cli.run
    end

    should "output the error and exit" do
      exp = "\n\nStandardError: #{@exception.message}\n" \
            "#{@exception.backtrace.join("\n")}"
      assert_includes exp, @kernel_spy.output
      assert_equal 1, @kernel_spy.exit_status
    end

  end

  class KernelSpy
    def initialize
      @output = StringIO.new
      @exit_statuses = []
    end

    def output
      @output.rewind
      @output.read
    end

    def puts(message)
      @output.puts(message)
    end

    def exit(code)
      @exit_statuses << code
    end

    def exit_status
      @exit_statuses.first
    end
  end

end
