require "spec"
require "./spec_helper"

describe "PROGRAM_NAME" do
  it "works for UTF-8 name", tags: %w[slow] do
    with_tempfile("source_file") do |source_file|
      if ENV["IN_NIX_SHELL"]?
        pending! "Example is broken in Nix shell (#12332)"
      end

      # MSYS2: gcc/ld doesn't support unicode paths
      # https://github.com/msys2/MINGW-packages/issues/17812
      {% if flag?(:windows) %}
        if ENV["MSYSTEM"]?
          pending! "Example is broken in MSYS2 shell"
        end
      {% end %}

      File.write(source_file, "File.basename(PROGRAM_NAME).inspect(STDOUT)")

      compile_file(source_file, bin_name: "×‽😂") do |executable_file|
        output = IO::Memory.new
        Process.run(executable_file, output: output).success?.should be_true
        output.to_s.should eq(File.basename(executable_file).inspect)
      end
    end
  end
end

describe "ARGV" do
  it "accepts UTF-8 command-line arguments", tags: %w[slow] do
    with_tempfile("source_file") do |source_file|
      File.write(source_file, "ARGV.inspect(STDOUT)")

      compile_file(source_file) do |executable_file|
        args = ["×‽😂", "あ×‽😂い"]
        output = IO::Memory.new
        Process.run(executable_file, args, output: output).success?.should be_true
        output.to_s.should eq(args.inspect)
      end
    end
  end
end

describe "exit" do
  it "exits normally with status 0", tags: %w[slow] do
    status, _, _ = compile_and_run_source "exit"
    status.success?.should be_true
  end

  it "exits with given error code", tags: %w[slow] do
    status, _, _ = compile_and_run_source "exit 42"
    status.success?.should be_false
    status.exit_code.should eq(42)
  end

  it "exists with Process::Status", tags: %w[slow] do
    status, _, _ = compile_and_run_source "exit Process::Status.new(0)"
    status.success?.should be_true
  end

  it "exists with abnormal status", tags: %w[slow] do
    status, _, _ = compile_and_run_source "exit Process::Status.new({% if flag?(:unix) %}Signal::INT.value{% else %}LibC::STATUS_CONTROL_C_EXIT{% end %})"
    status.exit_reason.should eq Process::ExitReason::Interrupted
  end
end

describe "at_exit" do
  it "runs handlers on normal program ending", tags: %w[slow] do
    status, output, _ = compile_and_run_source <<-CRYSTAL
      at_exit do
        print "handler code."
      end
    CRYSTAL

    status.success?.should be_true
    output.should eq("handler code.")
  end

  it "runs handlers on explicit program ending", tags: %w[slow] do
    status, output, _ = compile_and_run_source <<-'CRYSTAL'
      at_exit do |exit_code|
        print "handler code, exit code: #{exit_code}."
      end

      exit 42
    CRYSTAL

    status.exit_code.should eq(42)
    output.should eq("handler code, exit code: 42.")
  end

  it "runs handlers in reverse order", tags: %w[slow] do
    status, output, _ = compile_and_run_source <<-CRYSTAL
      at_exit do
        print "first handler code."
      end

      at_exit do
        print "second handler code."
      end
    CRYSTAL

    status.success?.should be_true
    output.should eq("second handler code.first handler code.")
  end

  it "runs all handlers maximum once", tags: %w[slow] do
    status, output, _ = compile_and_run_source <<-CRYSTAL
      at_exit do
        print "first handler code."
      end

      at_exit do
        print "second handler code, explicit exit!"
        exit

        print "not executed."
      end

      at_exit do
        print "third handler code."
      end
    CRYSTAL

    status.success?.should be_true
    output.should eq("third handler code.second handler code, explicit exit!first handler code.")
  end

  it "allows handlers to change the exit code with explicit `exit` call", tags: %w[slow] do
    status, output, _ = compile_and_run_source <<-'CRYSTAL'
      at_exit do |exit_code|
        print "first handler code, exit code: #{exit_code}."
      end

      at_exit do
        print "second handler code, re-exiting."
        exit 42

        print "not executed."
      end

      at_exit do |exit_code|
        print "third handler code, exit code: #{exit_code}."
      end
    CRYSTAL

    status.success?.should be_false
    status.exit_code.should eq(42)
    output.should eq("third handler code, exit code: 0.second handler code, re-exiting.first handler code, exit code: 42.")
  end

  it "allows handlers to change the exit code with explicit `exit` call (2)", tags: %w[slow] do
    status, output, _ = compile_and_run_source <<-'CRYSTAL'
      at_exit do |exit_code|
        print "first handler code, exit code: #{exit_code}."
      end

      at_exit do
        print "second handler code, re-exiting."
        exit 42

        print "not executed."
      end

      at_exit do |exit_code|
        print "third handler code, exit code: #{exit_code}."
      end

      exit 21
    CRYSTAL

    status.success?.should be_false
    status.exit_code.should eq(42)
    output.should eq("third handler code, exit code: 21.second handler code, re-exiting.first handler code, exit code: 42.")
  end

  it "changes final exit code when an handler raises an error", tags: %w[slow] do
    status, output, error = compile_and_run_source <<-'CRYSTAL'
      at_exit do |exit_code|
        print "first handler code, exit code: #{exit_code}."
      end

      at_exit do
        print "second handler code, raising."
        raise "Raised from at_exit handler!"

        print "not executed."
      end

      at_exit do |exit_code|
        print "third handler code, exit code: #{exit_code}."
      end
    CRYSTAL

    status.success?.should be_false
    status.exit_code.should eq(1)
    output.should eq("third handler code, exit code: 0.second handler code, raising.first handler code, exit code: 1.")
    error.should contain("Error running at_exit handler: Raised from at_exit handler!")
  end

  it "shows unhandled exceptions after at_exit handlers", tags: %w[slow] do
    status, _, error = compile_and_run_source <<-CRYSTAL
      at_exit do
        STDERR.print "first handler code."
      end

      at_exit do
        STDERR.print "second handler code."
      end

      raise "Kaboom!"
    CRYSTAL

    status.success?.should be_false
    error.should contain("second handler code.first handler code.Unhandled exception: Kaboom!")
  end

  it "can get unhandled exception in at_exit handler", tags: %w[slow] do
    status, _, error = compile_and_run_source <<-CRYSTAL
      at_exit do |_, ex|
        STDERR.print ex.try &.message
      end

      raise "Kaboom!"
    CRYSTAL

    status.success?.should be_false
    error.should contain("Kaboom!Unhandled exception: Kaboom!")
  end

  it "allows at_exit inside at_exit", tags: %w[slow] do
    status, output, _ = compile_and_run_source <<-CRYSTAL
      at_exit do
        print "1"
        at_exit do
          print "2"
        end
      end

      at_exit do
        print "3"
        at_exit do
          print "4"
        end
      end
    CRYSTAL

    status.success?.should be_true
    output.should eq("3412")
  end

  it "prints unhandled exception with cause", tags: %w[slow] do
    status, _, error = compile_and_run_source <<-CRYSTAL
      raise Exception.new("secondary", cause: Exception.new("primary"))
    CRYSTAL

    status.success?.should be_false
    error.should contain "Unhandled exception: secondary"
    error.should contain "Caused by: primary"
  end
end

{% if flag?(:openbsd) %}
  # FIXME: the segfault handler doesn't work on OpenBSD
  pending "hardware exception"
{% else %}
  describe "hardware exception" do
    it "reports invalid memory access", tags: %w[slow] do
      status, _, error = compile_and_run_source <<-'CRYSTAL'
        puts Pointer(Int64).null.value
      CRYSTAL

      status.success?.should be_false
      error.should contain("Invalid memory access")
      error.should_not contain("Stack overflow")
    end

    {% if flag?(:netbsd) %}
      # FIXME: on netbsd the process crashes with SIGILL after receiving SIGSEGV
      pending "detects stack overflow on the main stack"
      pending "detects stack overflow on a fiber stack"
    {% else %}
      it "detects stack overflow on the main stack", tags: %w[slow] do
        # This spec can take some time under FreeBSD where
        # the default stack size is 0.5G.  Setting a
        # smaller stack size with `ulimit -s 8192`
        # will address this.
        status, _, error = compile_and_run_source <<-'CRYSTAL'
          def foo
            y = StaticArray(Int8, 512).new(0)
            foo
          end
          foo
        CRYSTAL

        status.success?.should be_false
        error.should contain("Stack overflow")
      end

      it "detects stack overflow on a fiber stack", tags: %w[slow] do
        status, _, error = compile_and_run_source <<-'CRYSTAL'
          def foo
            y = StaticArray(Int8, 512).new(0)
            foo
          end

          spawn do
            foo
          end

          sleep 60.seconds
        CRYSTAL

        status.success?.should be_false
        error.should contain("Stack overflow")
      end
    {% end %}
  end
{% end %}
