require "spec"
require "spec/helpers/string"

private def exit_status(status)
  {% if flag?(:unix) %}
    status << 8
  {% else %}
    status.to_u32!
  {% end %}
end

private def status_for(exit_reason : Process::ExitReason)
  exit_code = case exit_reason
              when .interrupted?
                {% if flag?(:unix) %}Signal::INT.value{% else %}LibC::STATUS_CONTROL_C_EXIT{% end %}
              else
                raise NotImplementedError.new("status_for")
              end
  Process::Status.new(exit_code)
end

describe Process::Status do
  it "#exit_code" do
    Process::Status.new(exit_status(0)).exit_code.should eq 0
    Process::Status.new(exit_status(1)).exit_code.should eq 1
    Process::Status.new(exit_status(127)).exit_code.should eq 127
    Process::Status.new(exit_status(128)).exit_code.should eq 128
    Process::Status.new(exit_status(255)).exit_code.should eq 255

    expect_raises(RuntimeError, "Abnormal exit has no exit code") do
      status_for(:interrupted).exit_code
    end
  end

  it "#exit_code?" do
    Process::Status.new(exit_status(0)).exit_code?.should eq 0
    Process::Status.new(exit_status(1)).exit_code?.should eq 1
    Process::Status.new(exit_status(127)).exit_code?.should eq 127
    Process::Status.new(exit_status(128)).exit_code?.should eq 128
    Process::Status.new(exit_status(255)).exit_code?.should eq 255

    status_for(:interrupted).exit_code?.should be_nil
  end

  it "#system_exit_status" do
    Process::Status.new(exit_status(0)).system_exit_status.should eq 0_u32
    Process::Status.new(exit_status(1)).system_exit_status.should eq({{ flag?(:unix) ? 0x0100_u32 : 1_u32 }})
    Process::Status.new(exit_status(127)).system_exit_status.should eq({{ flag?(:unix) ? 0x7f00_u32 : 127_u32 }})
    Process::Status.new(exit_status(128)).system_exit_status.should eq({{ flag?(:unix) ? 0x8000_u32 : 128_u32 }})
    Process::Status.new(exit_status(255)).system_exit_status.should eq({{ flag?(:unix) ? 0xFF00_u32 : 255_u32 }})

    status_for(:interrupted).system_exit_status.should eq({% if flag?(:unix) %}Signal::INT.value{% else %}LibC::STATUS_CONTROL_C_EXIT{% end %})
  end

  it "#success?" do
    Process::Status.new(exit_status(0)).success?.should be_true
    Process::Status.new(exit_status(1)).success?.should be_false
    Process::Status.new(exit_status(127)).success?.should be_false
    Process::Status.new(exit_status(128)).success?.should be_false
    Process::Status.new(exit_status(255)).success?.should be_false

    status_for(:interrupted).success?.should be_false
  end

  it "#normal_exit?" do
    Process::Status.new(exit_status(0)).normal_exit?.should be_true
    Process::Status.new(exit_status(1)).normal_exit?.should be_true
    Process::Status.new(exit_status(127)).normal_exit?.should be_true
    Process::Status.new(exit_status(128)).normal_exit?.should be_true
    Process::Status.new(exit_status(255)).normal_exit?.should be_true

    status_for(:interrupted).normal_exit?.should be_false
  end

  it "#abnormal_exit?" do
    Process::Status.new(exit_status(0)).abnormal_exit?.should be_false
    Process::Status.new(exit_status(1)).abnormal_exit?.should be_false
    Process::Status.new(exit_status(127)).abnormal_exit?.should be_false
    Process::Status.new(exit_status(128)).abnormal_exit?.should be_false
    Process::Status.new(exit_status(255)).abnormal_exit?.should be_false

    status_for(:interrupted).abnormal_exit?.should be_true
  end

  it "#signal_exit?" do
    Process::Status.new(exit_status(0)).signal_exit?.should be_false
    Process::Status.new(exit_status(1)).signal_exit?.should be_false
    Process::Status.new(exit_status(127)).signal_exit?.should be_false
    Process::Status.new(exit_status(128)).signal_exit?.should be_false
    Process::Status.new(exit_status(255)).signal_exit?.should be_false

    status_for(:interrupted).signal_exit?.should eq {{ !flag?(:win32) }}
  end

  it "equality" do
    ok1 = Process::Status.new(exit_status(0))
    ok2 = Process::Status.new(exit_status(0))
    err1 = Process::Status.new(exit_status(1))
    err2 = Process::Status.new(exit_status(1))

    ok1.should eq(ok2)
    ok1.should_not eq(err2)
    err1.should_not eq(ok2)
    err1.should eq(err2)

    ok1.hash.should eq(ok2.hash)
    ok1.hash.should_not eq(err2.hash)
    err1.hash.should_not eq(ok2.hash)
    err1.hash.should eq(err2.hash)
  end

  it "#exit_signal?" do
    Process::Status.new(exit_status(0)).exit_signal?.should be_nil
    Process::Status.new(exit_status(1)).exit_signal?.should be_nil

    status_for(:interrupted).exit_signal?.should eq({% if flag?(:unix) %}Signal::INT{% else %}nil{% end %})
  end

  {% if flag?(:unix) && !flag?(:wasi) %}
    it "#exit_signal" do
      Process::Status.new(Signal::HUP.value).exit_signal.should eq Signal::HUP
      Process::Status.new(Signal::INT.value).exit_signal.should eq Signal::INT
      last_signal = Signal.values[-1]
      Process::Status.new(last_signal.value).exit_signal.should eq last_signal

      unknown_signal = Signal.new(126)
      Process::Status.new(unknown_signal.value).exit_signal.should eq unknown_signal
    end

    it "#exit_signal?" do
      Process::Status.new(Signal::HUP.value).exit_signal?.should eq Signal::HUP
      Process::Status.new(Signal::INT.value).exit_signal?.should eq Signal::INT
      last_signal = Signal.values[-1]
      Process::Status.new(last_signal.value).exit_signal?.should eq last_signal

      unknown_signal = Signal.new(126)
      Process::Status.new(unknown_signal.value).exit_signal?.should eq unknown_signal
    end

    it "#normal_exit? with signal code" do
      Process::Status.new(0x00).normal_exit?.should be_true
      Process::Status.new(0x01).normal_exit?.should be_false
      Process::Status.new(0x7e).normal_exit?.should be_false
      Process::Status.new(0x7f).normal_exit?.should be_false
    end

    it "#signal_exit? with signal code" do
      Process::Status.new(0x00).signal_exit?.should be_false
      Process::Status.new(0x01).signal_exit?.should be_true
      Process::Status.new(0x7e).signal_exit?.should be_true
      Process::Status.new(0x7f).signal_exit?.should be_true
    end
  {% end %}

  {% if flag?(:win32) %}
    describe "#exit_reason" do
      it "returns Normal" do
        Process::Status.new(exit_status(0)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(1)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(127)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(128)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(255)).exit_reason.normal?.should be_true

        Process::Status.new(0x3FFFFFFF_u32).exit_reason.normal?.should be_true
        Process::Status.new(0x40001234_u32).exit_reason.normal?.should be_false
        Process::Status.new(0x80001234_u32).exit_reason.normal?.should be_false
        Process::Status.new(0xC0001234_u32).exit_reason.normal?.should be_false
      end

      it "returns Aborted" do
        Process::Status.new(LibC::STATUS_FATAL_APP_EXIT).exit_reason.aborted?.should be_true
      end

      it "returns Interrupted" do
        Process::Status.new(LibC::STATUS_CONTROL_C_EXIT).exit_reason.interrupted?.should be_true
      end

      it "returns Breakpoint" do
        Process::Status.new(LibC::STATUS_BREAKPOINT).exit_reason.breakpoint?.should be_true
      end

      it "returns AccessViolation" do
        Process::Status.new(LibC::STATUS_ACCESS_VIOLATION).exit_reason.access_violation?.should be_true
        Process::Status.new(LibC::STATUS_STACK_OVERFLOW).exit_reason.access_violation?.should be_true
      end

      it "returns BadMemoryAccess" do
        Process::Status.new(LibC::STATUS_DATATYPE_MISALIGNMENT).exit_reason.bad_memory_access?.should be_true
      end

      it "returns BadInstruction" do
        Process::Status.new(LibC::STATUS_ILLEGAL_INSTRUCTION).exit_reason.bad_instruction?.should be_true
        Process::Status.new(LibC::STATUS_PRIVILEGED_INSTRUCTION).exit_reason.bad_instruction?.should be_true
      end

      it "returns FloatException" do
        Process::Status.new(LibC::STATUS_FLOAT_DIVIDE_BY_ZERO).exit_reason.float_exception?.should be_true
        Process::Status.new(LibC::STATUS_FLOAT_INEXACT_RESULT).exit_reason.float_exception?.should be_true
        Process::Status.new(LibC::STATUS_FLOAT_INVALID_OPERATION).exit_reason.float_exception?.should be_true
        Process::Status.new(LibC::STATUS_FLOAT_OVERFLOW).exit_reason.float_exception?.should be_true
        Process::Status.new(LibC::STATUS_FLOAT_UNDERFLOW).exit_reason.float_exception?.should be_true
      end
    end
  {% elsif flag?(:unix) && !flag?(:wasi) %}
    describe "#exit_reason" do
      it "returns Normal" do
        Process::Status.new(exit_status(0)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(1)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(127)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(128)).exit_reason.normal?.should be_true
        Process::Status.new(exit_status(255)).exit_reason.normal?.should be_true

        Process::Status.new(0x01).exit_reason.normal?.should be_false
        Process::Status.new(0x7e).exit_reason.normal?.should be_false

        Process::Status.new(0x017f).exit_reason.normal?.should be_false
        Process::Status.new(0xffff).exit_reason.normal?.should be_false
      end

      it "returns Aborted" do
        Process::Status.new(Signal::ABRT.value).exit_reason.aborted?.should be_true
        Process::Status.new(Signal::KILL.value).exit_reason.aborted?.should be_true
        Process::Status.new(Signal::QUIT.value).exit_reason.aborted?.should be_true
      end

      it "returns TerminalDisconnected" do
        Process::Status.new(Signal::HUP.value).exit_reason.terminal_disconnected?.should be_true
      end

      it "returns SessionEnded" do
        Process::Status.new(Signal::TERM.value).exit_reason.session_ended?.should be_true
      end

      it "returns Interrupted" do
        Process::Status.new(Signal::INT.value).exit_reason.interrupted?.should be_true
      end

      it "returns Breakpoint" do
        Process::Status.new(Signal::TRAP.value).exit_reason.breakpoint?.should be_true
      end

      it "returns AccessViolation" do
        Process::Status.new(Signal::SEGV.value).exit_reason.access_violation?.should be_true
      end

      it "returns BadMemoryAccess" do
        Process::Status.new(Signal::BUS.value).exit_reason.bad_memory_access?.should be_true
      end

      it "returns BadInstruction" do
        Process::Status.new(Signal::ILL.value).exit_reason.bad_instruction?.should be_true
      end

      it "returns FloatException" do
        Process::Status.new(Signal::FPE.value).exit_reason.float_exception?.should be_true
      end
    end
  {% end %}

  describe "#to_s" do
    it "with exit status" do
      assert_prints Process::Status.new(exit_status(0)).to_s, "0"
      assert_prints Process::Status.new(exit_status(1)).to_s, "1"
      assert_prints Process::Status.new(exit_status(127)).to_s, "127"
      assert_prints Process::Status.new(exit_status(128)).to_s, "128"
      assert_prints Process::Status.new(exit_status(255)).to_s, "255"
    end

    it "on abnormal exit" do
      {% if flag?(:win32) %}
        assert_prints status_for(:interrupted).to_s, "STATUS_CONTROL_C_EXIT"
      {% else %}
        assert_prints status_for(:interrupted).to_s, "INT"
      {% end %}
    end

    {% if flag?(:unix) && !flag?(:wasi) %}
      it "with exit signal" do
        assert_prints Process::Status.new(Signal::HUP.value).to_s, "HUP"
        last_signal = Signal.values[-1]
        assert_prints Process::Status.new(last_signal.value).to_s, last_signal.to_s

        assert_prints Process::Status.new(Signal.new(126).value).to_s, "Signal[126]"
      end
    {% end %}

    {% if flag?(:win32) %}
      it "hex format" do
        assert_prints Process::Status.new(UInt16::MAX).to_s, "0x0000FFFF"
        assert_prints Process::Status.new(0x01234567).to_s, "0x01234567"
        assert_prints Process::Status.new(UInt32::MAX).to_s, "0xFFFFFFFF"
      end
    {% end %}
  end

  describe "#inspect" do
    it "with exit status" do
      assert_prints Process::Status.new(exit_status(0)).inspect, "Process::Status[0]"
      assert_prints Process::Status.new(exit_status(1)).inspect, "Process::Status[1]"
      assert_prints Process::Status.new(exit_status(127)).inspect, "Process::Status[127]"
      assert_prints Process::Status.new(exit_status(128)).inspect, "Process::Status[128]"
      assert_prints Process::Status.new(exit_status(255)).inspect, "Process::Status[255]"
    end

    it "on abnormal exit" do
      {% if flag?(:win32) %}
        assert_prints status_for(:interrupted).inspect, "Process::Status[LibC::STATUS_CONTROL_C_EXIT]"
      {% else %}
        assert_prints status_for(:interrupted).inspect, "Process::Status[Signal::INT]"
      {% end %}
    end

    {% if flag?(:unix) && !flag?(:wasi) %}
      it "with exit signal" do
        assert_prints Process::Status.new(Signal::HUP.value).inspect, "Process::Status[Signal::HUP]"
        last_signal = Signal.values[-1]
        assert_prints Process::Status.new(last_signal.value).inspect, "Process::Status[#{last_signal.inspect}]"

        unknown_signal = Signal.new(126)
        assert_prints Process::Status.new(unknown_signal.value).inspect, "Process::Status[Signal[126]]"
      end
    {% end %}

    {% if flag?(:win32) %}
      it "hex format" do
        assert_prints Process::Status.new(UInt16::MAX).inspect, "Process::Status[0x0000FFFF]"
        assert_prints Process::Status.new(0x01234567).inspect, "Process::Status[0x01234567]"
        assert_prints Process::Status.new(UInt32::MAX).inspect, "Process::Status[0xFFFFFFFF]"
      end
    {% end %}
  end

  describe "#description" do
    it "with exit status" do
      Process::Status.new(exit_status(0)).description.should eq "Process exited normally"
      Process::Status.new(exit_status(255)).description.should eq "Process exited normally"
    end

    it "on interrupt" do
      status_for(:interrupted).description.should eq "Process was interrupted"
    end

    {% if flag?(:unix) && !flag?(:wasi) %}
      it "with exit signal" do
        Process::Status.new(Signal::HUP.value).description.should eq "Process terminated abnormally"
        Process::Status.new(Signal::KILL.value).description.should eq "Process terminated abnormally"
        Process::Status.new(Signal::STOP.value).description.should eq "Process received and didn't handle signal STOP"
        last_signal = Signal.values[-1]
        Process::Status.new(last_signal.value).description.should eq "Process received and didn't handle signal #{last_signal}"

        unknown_signal = Signal.new(126)
        Process::Status.new(unknown_signal.value).description.should eq "Process received and didn't handle signal 126"
      end
    {% end %}
  end
end
