# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec

Code.require_file("test_helper.exs", __DIR__)

defmodule SystemTest do
  use ExUnit.Case
  import PathHelpers

  test "build_info/0" do
    build_info = System.build_info()
    assert is_map(build_info)
    assert is_binary(build_info[:build])
    assert is_binary(build_info[:date])
    assert is_binary(build_info[:revision])
    assert is_binary(build_info[:version])
    assert is_binary(build_info[:otp_release])

    if build_info[:revision] != "" do
      assert String.length(build_info[:revision]) >= 7
    end

    version_file = Path.join([__DIR__, "../../../..", "VERSION"]) |> Path.expand()
    {:ok, version} = File.read(version_file)
    assert build_info[:version] == String.trim(version)
    assert build_info[:build] =~ "compiled with Erlang/OTP"
  end

  test "user_home/0" do
    assert is_binary(System.user_home())
    assert is_binary(System.user_home!())
  end

  test "tmp_dir/0" do
    assert is_binary(System.tmp_dir())
    assert is_binary(System.tmp_dir!())
  end

  test "endianness/0" do
    assert System.endianness() in [:little, :big]
    assert System.endianness() == System.compiled_endianness()
  end

  test "pid/0" do
    assert is_binary(System.pid())
  end

  test "argv/0" do
    list = elixir(~c"-e \"IO.inspect System.argv()\" -- -o opt arg1 arg2 --long-opt 10")
    {args, _} = Code.eval_string(list, [])
    assert args == ["-o", "opt", "arg1", "arg2", "--long-opt", "10"]
  end

  @test_var "SYSTEM_ELIXIR_ENV_TEST_VAR"

  test "get_env/put_env/delete_env" do
    assert System.get_env(@test_var) == nil
    assert System.get_env(@test_var, "SAMPLE") == "SAMPLE"
    assert System.fetch_env(@test_var) == :error

    message = "could not fetch environment variable #{inspect(@test_var)} because it is not set"
    assert_raise System.EnvError, message, fn -> System.fetch_env!(@test_var) end

    System.put_env(@test_var, "SAMPLE")

    assert System.get_env(@test_var) == "SAMPLE"
    assert System.get_env()[@test_var] == "SAMPLE"
    assert System.fetch_env(@test_var) == {:ok, "SAMPLE"}
    assert System.fetch_env!(@test_var) == "SAMPLE"

    System.delete_env(@test_var)
    assert System.get_env(@test_var) == nil

    assert_raise ArgumentError, ~r[cannot execute System.put_env/2 for key with \"=\"], fn ->
      System.put_env("FOO=BAR", "BAZ")
    end
  end

  test "put_env/2" do
    System.put_env(%{@test_var => "MAP_STRING"})
    assert System.get_env(@test_var) == "MAP_STRING"

    System.put_env([{String.to_atom(@test_var), "KW_ATOM"}])
    assert System.get_env(@test_var) == "KW_ATOM"

    System.put_env([{String.to_atom(@test_var), nil}])
    assert System.get_env(@test_var) == nil
  end

  test "cmd/2 raises for null bytes" do
    assert_raise ArgumentError, ~r"cannot execute System.cmd/3 for program with null byte", fn ->
      System.cmd("null\0byte", [])
    end
  end

  test "cmd/3 raises with non-binary arguments" do
    assert_raise ArgumentError, ~r"all arguments for System.cmd/3 must be binaries", fn ->
      System.cmd("ls", [~c"/usr"])
    end
  end

  describe "Windows" do
    @describetag :windows

    test "cmd/2" do
      assert {"hello\r\n", 0} = System.cmd("cmd", ~w[/c echo hello])
    end

    test "cmd/3 (with options)" do
      opts = [
        into: [],
        cd: File.cwd!(),
        env: %{"foo" => "bar", "baz" => nil},
        arg0: "echo",
        stderr_to_stdout: true,
        parallelism: true,
        use_stdio: true
      ]

      assert {["hello\r\n"], 0} = System.cmd("cmd", ~w[/c echo hello], opts)
    end

    @echo "echo-elixir-test"
    @tag :tmp_dir
    test "cmd/3 with absolute and relative paths", config do
      echo = Path.join(config.tmp_dir, @echo)
      File.mkdir_p!(Path.dirname(echo))
      File.ln_s!(System.find_executable("cmd"), echo)

      File.cd!(Path.dirname(echo), fn ->
        # There is a bug in OTP where find_executable is finding
        # entries on the current directory. If this is the case,
        # we should avoid the assertion below.
        if !System.find_executable(@echo) do
          assert :enoent = catch_error(System.cmd(@echo, ~w[/c echo hello]))
        end

        assert {"hello\r\n", 0} =
                 System.cmd(Path.join(File.cwd!(), @echo), ~w[/c echo hello], [{:arg0, "echo"}])
      end)
    end

    test "shell/1" do
      assert {"hello\r\n", 0} = System.shell("echo hello")
    end

    test "shell/2 (with options)" do
      opts = [
        into: [],
        cd: File.cwd!(),
        env: %{"foo" => "bar", "baz" => nil},
        stderr_to_stdout: true,
        parallelism: true,
        use_stdio: true
      ]

      assert {["bar\r\n"], 0} = System.shell("echo %foo%", opts)
    end
  end

  describe "Unix" do
    @describetag :unix

    test "cmd/2" do
      assert {"hello\n", 0} = System.cmd("echo", ["hello"])
    end

    test "cmd/3 (with options)" do
      opts = [
        into: [],
        cd: File.cwd!(),
        env: %{"foo" => "bar", "baz" => nil},
        arg0: "echo",
        stderr_to_stdout: true,
        parallelism: true,
        use_stdio: true
      ]

      assert {["hello\n"], 0} = System.cmd("echo", ["hello"], opts)
    end

    test "cmd/3 (can't use `use_stdio: false, stderr_to_stdout: true`)" do
      opts = [
        into: [],
        cd: File.cwd!(),
        env: %{"foo" => "bar", "baz" => nil},
        arg0: "echo",
        stderr_to_stdout: true,
        use_stdio: false
      ]

      message = ~r"cannot use \"stderr_to_stdout: true\" and \"use_stdio: false\""

      assert_raise ArgumentError, message, fn ->
        System.cmd("echo", ["hello"], opts)
      end
    end

    test "cmd/3 by line" do
      assert {["hello", "world"], 0} =
               System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)

      assert {["hello", "world"], 0} =
               System.cmd("echo", ["-n", "hello\nworld"], into: [], lines: 3)
    end

    @echo "echo-elixir-test"
    @tag :tmp_dir
    test "cmd/3 with absolute and relative paths", config do
      echo = Path.join(config.tmp_dir, @echo)
      File.mkdir_p!(Path.dirname(echo))
      File.ln_s!(System.find_executable("echo"), echo)

      File.cd!(Path.dirname(echo), fn ->
        # There is a bug in OTP where find_executable is finding
        # entries on the current directory. If this is the case,
        # we should avoid the assertion below.
        if !System.find_executable(@echo) do
          assert :enoent = catch_error(System.cmd(@echo, ["hello"]))
        end

        assert {"hello\n", 0} =
                 System.cmd(Path.join(File.cwd!(), @echo), ["hello"], [{:arg0, "echo"}])
      end)
    end

    test "shell/1" do
      assert {"hello\n", 0} = System.shell("echo hello")
    end

    test "shell/1 with interpolation" do
      assert {"1\n2\n", 0} = System.shell("x=1; echo $x; echo '2'")
    end

    test "shell/1 with empty string" do
      assert {"", 0} = System.shell("")
      assert {"", 0} = System.shell("  ")
    end

    @tag timeout: 1_000
    test "shell/1 returns when command awaits input" do
      assert {"", 0} = System.shell("cat", close_stdin: true)
    end

    test "shell/1 with comment" do
      assert {"1\n", 0} = System.shell("echo '1' # comment")
    end

    test "shell/2 (with options)" do
      opts = [
        into: [],
        cd: File.cwd!(),
        env: %{"foo" => "bar", "baz" => nil},
        stderr_to_stdout: true,
        use_stdio: true
      ]

      assert {["bar\n"], 0} = System.shell("echo $foo", opts)
    end
  end

  @tag :unix
  test "vm signals" do
    assert System.trap_signal(:sigquit, :example, fn -> :ok end) == {:ok, :example}
    assert System.trap_signal(:sigquit, :example, fn -> :ok end) == {:error, :already_registered}
    assert {:ok, ref} = System.trap_signal(:sigquit, fn -> :ok end)

    assert System.untrap_signal(:sigquit, :example) == :ok
    assert System.trap_signal(:sigquit, :example, fn -> :ok end) == {:ok, :example}
    assert System.trap_signal(:sigquit, ref, fn -> :ok end) == {:error, :already_registered}

    assert System.untrap_signal(:sigusr1, :example) == {:error, :not_found}
    assert System.untrap_signal(:sigquit, :example) == :ok
    assert System.untrap_signal(:sigquit, :example) == {:error, :not_found}
    assert System.untrap_signal(:sigquit, ref) == :ok
    assert System.untrap_signal(:sigquit, ref) == {:error, :not_found}
  end

  @tag :unix
  test "os signals" do
    parent = self()

    assert System.trap_signal(:sighup, :example, fn ->
             send(parent, :sighup_called)
             :ok
           end) == {:ok, :example}

    {"", 0} = System.cmd("kill", ["-s", "hup", System.pid()])

    assert_receive :sighup_called
  after
    System.untrap_signal(:sighup, :example)
  end

  test "find_executable/1" do
    assert System.find_executable("erl")
    assert is_binary(System.find_executable("erl"))
    assert !System.find_executable("does-not-really-exist-from-elixir")

    message = ~r"cannot execute System.find_executable/1 for program with null byte"

    assert_raise ArgumentError, message, fn ->
      System.find_executable("null\0byte")
    end
  end

  test "monotonic_time/0" do
    assert is_integer(System.monotonic_time())
  end

  test "monotonic_time/1" do
    assert is_integer(System.monotonic_time(:nanosecond))
    assert abs(System.monotonic_time(:microsecond)) < abs(System.monotonic_time(:nanosecond))
  end

  test "system_time/0" do
    assert is_integer(System.system_time())
  end

  test "system_time/1" do
    assert is_integer(System.system_time(:nanosecond))
    assert abs(System.system_time(:microsecond)) < abs(System.system_time(:nanosecond))
  end

  test "time_offset/0 and time_offset/1" do
    assert is_integer(System.time_offset())
    assert is_integer(System.time_offset(:second))
  end

  test "os_time/0" do
    assert is_integer(System.os_time())
  end

  test "os_time/1" do
    assert is_integer(System.os_time(:nanosecond))
    assert abs(System.os_time(:microsecond)) < abs(System.os_time(:nanosecond))
  end

  test "unique_integer/0 and unique_integer/1" do
    assert is_integer(System.unique_integer())
    assert System.unique_integer([:positive]) > 0

    assert System.unique_integer([:positive, :monotonic]) <
             System.unique_integer([:positive, :monotonic])
  end

  test "convert_time_unit/3" do
    time = System.monotonic_time(:nanosecond)
    assert abs(System.convert_time_unit(time, :nanosecond, :microsecond)) < abs(time)
  end

  test "schedulers/0" do
    assert System.schedulers() >= 1
  end

  test "schedulers_online/0" do
    assert System.schedulers_online() >= 1
  end

  test "otp_release/0" do
    assert is_binary(System.otp_release())
  end
end
