# frozen_string_literal: true

require 'rspec/expectations'
require 'rspec/mocks'
require 'rake'
require 'cucumber/rake/task'

class MockKernel
  attr_reader :exit_status

  def exit(status)
    @exit_status = status

    status unless status.zero?
  end
end

class CommandLine
  include ::RSpec::Mocks::ExampleMethods

  def initialize
    ::RSpec::Mocks.setup

    @stdout = StringIO.new
    @stderr = StringIO.new
    @kernel = MockKernel.new
  end

  def stderr
    @stderr.string
  end

  def stdout(format: :string)
    case format
    when :lines then @stdout.string.split("\n")
    when :messages then @stdout.string.split("\n").map { |line| JSON.parse(line) }
    else @stdout.string
    end
  end

  def all_output
    [stdout, stderr].reject(&:empty?).join("\n")
  end

  def exit_status
    @kernel.exit_status || 0
  end

  def capture_stdout
    capture_stream($stdout, @stdout)
    capture_stream($stderr, @stderr)

    yield
  end

  def destroy_mocks
    ::RSpec::Mocks.verify
  ensure
    ::RSpec::Mocks.teardown
  end

  private

  def capture_stream(stream, redirect)
    allow(stream)
      .to receive(:puts)
      .and_wrap_original do |_, *args|
        redirect.puts(*args)
      end

    allow(stream)
      .to receive(:print)
      .and_wrap_original do |_, *args|
        redirect.print(*args)
      end

    allow(stream)
      .to receive(:flush)
      .and_wrap_original do |_, *args|
        redirect.flush(*args)
      end
  end
end

class CucumberCommand < CommandLine
  attr_reader :args

  def execute(args)
    @args = args
    argument_list = make_arg_list(args)

    Cucumber::Cli::Main.new(
      argument_list,
      @stdout,
      @stderr,
      @kernel
    ).execute!
  end

  private

  def make_arg_list(args)
    index = -1
    args.split(/'|"/).map do |chunk|
      index += 1
      index.even? ? chunk.split(' ') : chunk
    end.flatten
  end
end

class RubyCommand < CommandLine
  def execute(file)
    capture_stdout { require file }
  rescue RuntimeError
    # no-op: this is raised when Cucumber fails
  rescue SystemExit
    # No-op: well, we are supposed to exit the rake task
  rescue StandardError
    @kernel.exit(1)
  end
end

class RakeCommand < CommandLine
  def execute(task)
    allow_any_instance_of(Cucumber::Rake::Task)
      .to receive(:fork)
      .and_return(false)

    allow(Cucumber::Cli::Main)
      .to receive(:execute)
      .and_wrap_original do |_, *args|
        Cucumber::Cli::Main.new(
          args[0],
          @stdout,
          @stderr,
          @kernel
        ).execute!
      end

    Rake.with_application do |rake|
      rake.load_rakefile
      capture_stdout { rake[task.strip].invoke }
    end
  rescue RuntimeError
    # no-op: this is raised when Cucumber fails
  rescue SystemExit
    # No-op: well, we are supposed to exit the rake task
  end
end

module CLIWorld
  def execute_cucumber(args)
    execute_command(CucumberCommand, args)
  end

  def execute_extra_cucumber(args)
    execute_extra_command(CucumberCommand, args)
  end

  def execute_ruby(filename)
    execute_command(RubyCommand, "#{Dir.pwd}/#{filename}")
  end

  def execute_rake(task)
    execute_command(RakeCommand, task)
  end

  def execute_command(cls, args)
    @command_line = cls.new
    @command_line.execute(args)
  end

  def execute_extra_command(cls, args)
    @extra_commands = []
    @extra_commands << cls.new
    @extra_commands.last.execute(args)
  end

  def command_line
    @command_line
  end

  def last_extra_command
    @extra_commands&.last
  end
end

World(CLIWorld)

RSpec::Matchers.define :have_succeded do
  match do |cli|
    @actual = cli.exit_status
    @expected = '0 exit code'
    @actual.zero?
  end
end

RSpec::Matchers.define :have_failed do
  match do |cli|
    @actual = cli.exit_status
    @expected = 'non-0 exit code'
    @actual.positive?
  end
end

RSpec::Matchers.define :have_exited_with do |expected|
  match do |cli|
    @actual = cli.exit_status

    if expected.is_a?(ExecutionStatus)
      expected.validates?(@actual)
    else
      @actual == expected
    end
  end
end
