require "spec"
require "../support/tempfile"
require "../support/fibers"
require "../support/win32"
require "../support/wasm32"
require "../support/interpreted"

def datapath(*components)
  File.join("spec", "std", "data", *components)
end

private class Witness
  @checked = false

  def check
    @checked = true
  end

  def checked?
    @checked
  end
end

def spawn_and_wait(before : Proc(_), file = __FILE__, line = __LINE__, &block)
  spawn_and_check(before, file, line) do |w|
    block.call
    w.check
  end
end

def spawn_and_check(before : Proc(_), file = __FILE__, line = __LINE__, &block : Witness -> _)
  done = Channel(Exception?).new
  w = Witness.new

  # State of the "before" filter:
  # 0 - not started
  # 1 - started
  # 2 - completed
  x = Atomic(Int32).new(0)

  before_fiber = spawn do
    x.set(1)

    # This is a workaround to ensure the "before" fiber
    # is unscheduled. Otherwise it might stay alive running the event loop
    {% begin %}
    spawn({% if flag?(:preview_mt) && !flag?(:execution_context) %}same_thread: true{% end %}) do
      while x.get != 2
        Fiber.yield
      end
    end
    {% end %}

    before.call
    x.set(2)
  end

  spawn do
    # Wait until the "before" fiber starts
    while x.get == 0
      Fiber.yield
    end

    # Now wait until the "before" fiber is blocked
    wait_until_blocked before_fiber
    block.call w

    done.send nil
  rescue e
    done.send e
  end

  ex = done.receive
  raise ex if ex
  unless w.checked?
    fail "Failed to stress expected path", file, line
  end
end

def compile_file(source_file, *, bin_name = "executable_file", flags = %w(), file = __FILE__, &)
  # can't use backtick in interpreted code (#12241)
  pending_interpreted! "Unable to compile Crystal code in interpreted code"

  with_temp_executable(bin_name, file: file) do |executable_file|
    compiler = ENV["CRYSTAL_SPEC_COMPILER_BIN"]? || "bin/crystal"
    args = ["build"] + flags + ["-o", executable_file, source_file]
    output = IO::Memory.new
    status = Process.run(compiler, args, env: {
      "CRYSTAL_PATH"         => Crystal::PATH,
      "CRYSTAL_LIBRARY_PATH" => Crystal::LIBRARY_PATH,
      "CRYSTAL_CACHE_DIR"    => Crystal::CACHE_DIR,
    }, output: output, error: output)

    unless status.success?
      fail "Compiler command `#{compiler} #{args.join(" ")}` failed with status #{status}.#{"\n" if output}#{output}"
    end

    File.exists?(executable_file).should be_true

    yield executable_file
  end
end

def assert_compile_error(source, expected_error, *, flags = %w(), file = __FILE__, line = __LINE__)
  # can't use backtick in interpreted code (#12241)
  pending_interpreted! "Unable to compile Crystal code in interpreted code"

  with_tempfile("source_file", file: file) do |source_file|
    File.write(source_file, source)

    bin_name = "executable_file"
    with_temp_executable(bin_name, file: file) do |executable_file|
      compiler = ENV["CRYSTAL_SPEC_COMPILER_BIN"]? || "bin/crystal"
      args = ["build"] + flags + ["-o", executable_file, source_file]
      output = IO::Memory.new
      status = Process.run(compiler, args, env: {
        "CRYSTAL_PATH"         => Crystal::PATH,
        "CRYSTAL_LIBRARY_PATH" => Crystal::LIBRARY_PATH,
        "CRYSTAL_CACHE_DIR"    => Crystal::CACHE_DIR,
        "NO_COLOR"             => "1",
      }, output: output, error: output)

      output.to_s.should contain(expected_error)

      status.success?.should be_false
      File.exists?(executable_file).should be_false
    end
  end
end

def compile_source(source, flags = %w(), file = __FILE__, &)
  with_tempfile("source_file", file: file) do |source_file|
    File.write(source_file, source)
    compile_file(source_file, flags: flags, file: file) do |executable_file|
      yield executable_file
    end
  end
end

def compile_and_run_file(source_file, flags = %w(), runtime_args = %w(), file = __FILE__)
  compile_file(source_file, flags: flags, file: file) do |executable_file|
    output, error = IO::Memory.new, IO::Memory.new
    status = Process.run executable_file, args: runtime_args, output: output, error: error

    {status, output.to_s, error.to_s}
  end
end

def compile_and_run_source(source, flags = %w(), runtime_args = %w(), file = __FILE__)
  with_tempfile("source_file", file: file) do |source_file|
    File.write(source_file, source)
    compile_and_run_file(source_file, flags, runtime_args, file: file)
  end
end

def compile_and_run_source_with_c(c_code, crystal_code, flags = %w(--debug), file = __FILE__, &)
  with_temp_c_object_file(c_code, file: file) do |o_filename|
    yield compile_and_run_source(%(
    require "prelude"

    @[Link(ldflags: #{o_filename.inspect})]
    #{crystal_code}
    ))
  end
end
