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

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

defmodule FileTest do
  use ExUnit.Case
  import PathHelpers

  setup do
    File.mkdir_p!(tmp_path())
    on_exit(fn -> File.rm_rf(tmp_path()) end)
    :ok
  end

  describe "rename" do
    # Following Erlang's underlying implementation
    #
    # Renaming files
    # :ok               -> rename file to existing file default behavior
    # {:error, :eisdir} -> rename file to existing empty dir
    # {:error, :eisdir} -> rename file to existing non-empty dir
    # :ok               -> rename file to non-existing location
    # {:error, :eexist} -> rename file to existing file
    # :ok               -> rename file to itself

    # Renaming dirs
    # {:error, :enotdir} -> rename dir to existing file
    # :ok                -> rename dir to non-existing leaf location
    # {:error, ??}       -> rename dir to non-existing parent location
    # :ok                -> rename dir to itself
    # :ok                -> rename dir to existing empty dir default behavior
    # {:error, :eexist}  -> rename dir to existing empty dir
    # {:error, :einval}  -> rename parent dir to existing sub dir
    # {:error, :einval}  -> rename parent dir to non-existing sub dir
    # {:error, :eexist}  -> rename dir to existing non-empty dir

    # other tests
    # {:error, :enoent} -> rename unknown source
    # :ok               -> rename preserves mode
    test "rename file to existing file default behavior" do
      src = tmp_fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      File.write!(dest, "hello")

      try do
        assert File.exists?(dest)
        assert File.rename(src, dest) == :ok
        refute File.exists?(src)
        assert File.read!(dest) == "FOO\n"
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename file to existing empty dir" do
      src = tmp_fixture_path("file.txt")
      dest = tmp_path("tmp")

      try do
        File.mkdir(dest)
        assert File.rename(src, dest) == {:error, :eisdir}
        assert File.exists?(src)
        refute File.exists?(tmp_path("tmp/file.txt"))
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename file to existing non-empty dir" do
      src = tmp_fixture_path("file.txt")
      dest = tmp_path("tmp")

      try do
        File.mkdir_p(Path.join(dest, "a"))
        assert File.rename(src, dest) in [{:error, :eisdir}, {:error, :eexist}]
        assert File.exists?(src)
        refute File.exists?(Path.join(dest, "file.txt"))
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename file to non-existing location" do
      src = tmp_fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      try do
        refute File.exists?(dest)
        assert File.rename(src, dest) == :ok
        assert File.exists?(dest)
        refute File.exists?(src)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename file to existing file" do
      src = tmp_fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      File.write!(dest, "hello")

      try do
        assert File.exists?(dest)
        assert File.rename(src, dest) == :ok
        refute File.exists?(src)
        assert File.read!(dest) == "FOO\n"
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename file to itself" do
      src = tmp_fixture_path("file.txt")
      dest = src

      try do
        assert File.exists?(src)
        assert File.rename(src, dest) == :ok
        assert File.exists?(src)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename! file to existing file default behavior" do
      src = tmp_fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      File.write!(dest, "hello")

      try do
        assert File.exists?(dest)
        assert File.rename!(src, dest) == :ok
        refute File.exists?(src)
        assert File.read!(dest) == "FOO\n"
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename! with invalid file" do
      src = tmp_fixture_path("invalid.txt")
      dest = tmp_path("tmp.file")

      message =
        "could not rename from #{inspect(src)} to #{inspect(dest)}: no such file or directory"

      assert_raise File.RenameError, message, fn ->
        File.rename!(src, dest)
      end
    end

    test "rename dir to existing file" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("tmp.file")

      try do
        File.touch(dest)
        assert File.rename(src, dest) == {:error, :enotdir}
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename dir to non-existing leaf location" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("tmp")

      try do
        refute File.exists?(tmp_path("tmp/a/1.txt"))
        refute File.exists?(tmp_path("tmp/a/a/2.txt"))
        refute File.exists?(tmp_path("tmp/b/3.txt"))

        assert File.rename(src, dest) == :ok
        {:ok, files} = File.ls(dest)
        assert length(files) == 2
        assert "a" in files

        {:ok, files} = File.ls(tmp_path("tmp/a"))
        assert length(files) == 2
        assert "1.txt" in files

        assert File.exists?(tmp_path("tmp/a/1.txt"))
        assert File.exists?(tmp_path("tmp/a/a/2.txt"))
        assert File.exists?(tmp_path("tmp/b/3.txt"))

        refute File.exists?(src)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename dir to non-existing parent location" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("tmp/a/b")

      try do
        assert File.rename(src, dest) == {:error, :enoent}
        assert File.exists?(src)
        refute File.exists?(dest)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename dir to itself" do
      src = tmp_fixture_path("cp_r")
      dest = src

      try do
        assert File.exists?(src)
        assert File.rename(src, dest) == :ok
        assert File.exists?(src)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename parent dir to existing sub dir" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("cp_r/a")

      try do
        assert File.exists?(src)
        assert File.rename(src, dest) in [{:error, :einval}, {:error, :eexist}]
        assert File.exists?(src)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename parent dir to non-existing sub dir" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("cp_r/x")

      try do
        assert File.exists?(src)
        assert File.rename(src, dest) == {:error, :einval}
        assert File.exists?(src)
        refute File.exists?(dest)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename dir to existing empty dir default behavior" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("tmp")

      File.mkdir(dest)

      try do
        refute File.exists?(tmp_path("tmp/a"))

        assert File.rename(src, dest) == :ok
        {:ok, files} = File.ls(dest)
        assert length(files) == 2
        assert "a" in files

        {:ok, files} = File.ls(tmp_path("tmp/a"))
        assert length(files) == 2
        assert "1.txt" in files

        assert File.exists?(tmp_path("tmp/a/1.txt"))
        assert File.exists?(tmp_path("tmp/a/a/2.txt"))
        assert File.exists?(tmp_path("tmp/b/3.txt"))

        refute File.exists?(src)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename dir to existing empty dir" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("tmp")

      File.mkdir(dest)

      try do
        assert File.exists?(dest)
        assert File.rename(src, dest) == :ok
        refute File.exists?(src)
        assert File.exists?(tmp_path("tmp/a"))
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename dir to existing non-empty dir" do
      src = tmp_fixture_path("cp_r")
      dest = tmp_path("tmp")

      File.mkdir_p(tmp_path("tmp/x"))

      try do
        assert File.exists?(tmp_path("tmp/x"))
        assert File.exists?(src)
        refute File.exists?(tmp_path("tmp/a"))

        assert File.rename(src, dest) == {:error, :eexist}

        assert File.exists?(tmp_path("tmp/x"))
        assert File.exists?(src)
        refute File.exists?(tmp_path("tmp/a"))
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "rename unknown source" do
      src = fixture_path("unknown")
      dest = tmp_path("tmp")

      try do
        assert File.rename(src, dest) == {:error, :enoent}
      after
        File.rm_rf(dest)
      end
    end

    test "rename preserves mode" do
      File.mkdir_p!(tmp_path("tmp"))
      src = tmp_fixture_path("cp_mode")
      dest = tmp_path("tmp/cp_mode")

      try do
        %File.Stat{mode: src_mode} = File.stat!(src)
        File.rename(src, dest)
        %File.Stat{mode: dest_mode} = File.stat!(dest)
        assert src_mode == dest_mode
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    def tmp_fixture_path(extra) do
      src = fixture_path(extra)
      dest = tmp_path(extra)
      File.cp_r(src, dest)
      dest
    end
  end

  describe "cp" do
    test "cp with src file and dest file" do
      src = fixture_path("file.txt")
      dest = tmp_path("sample.txt")

      File.touch(dest)

      try do
        assert File.exists?(dest)
        assert File.cp(src, dest) == :ok
        assert File.exists?(dest)
      after
        File.rm(dest)
      end
    end

    test "cp with src file and dest dir" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp")

      File.mkdir(dest)

      try do
        assert File.cp(src, dest) == {:error, :eisdir}
      after
        File.rm_rf(dest)
      end
    end

    test "cp with src file and dest unknown" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      try do
        refute File.exists?(dest)
        assert File.cp(src, dest) == :ok
        assert File.exists?(dest)
      after
        File.rm_rf(dest)
      end
    end

    test "cp with src dir" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp.file")
      assert File.cp(src, dest) == {:error, :eisdir}
    end

    test "cp with conflict" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      File.write!(dest, "hello")

      try do
        assert File.exists?(dest)
        assert File.cp(src, dest) == :ok
        assert File.read!(dest) == "FOO\n"
      after
        File.rm_rf(dest)
      end
    end

    test "cp with conflict with function" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      File.write!(dest, "hello")

      try do
        assert File.exists?(dest)

        assert File.cp(src, dest,
                 on_conflict: fn src_file, dest_file ->
                   assert src_file == src
                   assert dest_file == dest
                   false
                 end
               ) == :ok

        assert File.read!(dest) == "hello"
      after
        File.rm_rf(dest)
      end
    end

    test "cp! with src file and dest file" do
      src = fixture_path("file.txt")
      dest = tmp_path("sample.txt")

      File.touch(dest)

      try do
        assert File.exists?(dest)
        assert File.cp!(src, dest) == :ok
        assert File.exists?(dest)
      after
        File.rm(dest)
      end
    end

    test "cp! with src dir" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp.file")

      message =
        "could not copy from #{inspect(src)} to #{inspect(dest)}: illegal operation on a directory"

      assert_raise File.CopyError, message, fn ->
        File.cp!(src, dest)
      end
    end

    test "cp itself" do
      src = dest = tmp_path("tmp.file")

      File.write!(src, "here")

      try do
        assert File.cp(src, dest) == :ok
        assert File.read!(dest) == "here"
        assert File.cp_r(src, dest) == {:ok, []}
      after
        File.rm(dest)
      end
    end

    test "cp_r raises on path with null byte" do
      assert_raise ArgumentError, ~r/null byte/, fn -> File.cp_r("source", "foo\0bar") end
      assert_raise ArgumentError, ~r/null byte/, fn -> File.cp_r("foo\0bar", "dest") end
    end

    test "cp_r with src file and dest file" do
      src = fixture_path("file.txt")
      dest = tmp_path("sample.txt")

      File.touch(dest)

      try do
        assert File.exists?(dest)
        assert File.cp_r(src, dest) == {:ok, [dest]}
        assert File.exists?(dest)
      after
        File.rm(dest)
      end
    end

    test "cp_r with src file and dest dir" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp")

      File.mkdir(dest)

      try do
        assert io_error?(File.cp_r(src, dest))
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src file and dest unknown" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp.file")

      try do
        refute File.exists?(dest)
        assert File.cp_r(src, dest) == {:ok, [dest]}
        assert File.exists?(dest)
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src dir and dest dir" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp")

      File.mkdir(dest)

      try do
        refute File.exists?(tmp_path("tmp/a/1.txt"))
        refute File.exists?(tmp_path("tmp/a/a/2.txt"))
        refute File.exists?(tmp_path("tmp/b/3.txt"))

        {:ok, files} = File.cp_r(src, dest)
        assert length(files) == 7
        assert tmp_path("tmp/a") in files
        assert tmp_path("tmp/a/1.txt") in files

        assert File.exists?(tmp_path("tmp/a/1.txt"))
        assert File.exists?(tmp_path("tmp/a/a/2.txt"))
        assert File.exists?(tmp_path("tmp/b/3.txt"))
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src dir and dest file" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp.file")

      try do
        File.touch!(dest)
        assert File.cp_r(src, dest) |> io_error?()
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src dir and dest unknown" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp")

      try do
        refute File.exists?(tmp_path("tmp/a/1.txt"))
        refute File.exists?(tmp_path("tmp/a/a/2.txt"))
        refute File.exists?(tmp_path("tmp/b/3.txt"))

        {:ok, files} = File.cp_r(src, dest)
        assert length(files) == 7

        assert File.exists?(tmp_path("tmp/a/1.txt"))
        assert File.exists?(tmp_path("tmp/a/a/2.txt"))
        assert File.exists?(tmp_path("tmp/b/3.txt"))
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src unknown" do
      src = fixture_path("unknown")
      dest = tmp_path("tmp")
      assert File.cp_r(src, dest) == {:error, :enoent, src}
    end

    test "cp_r with absolute symlink" do
      linked_src = fixture_path("cp_r")
      src = tmp_path("tmp/src")
      dest = tmp_path("tmp/dest")

      File.mkdir_p!(src)
      :ok = :file.make_symlink(Path.join(linked_src, "a"), Path.join(src, "sym"))

      try do
        {:ok, files} = File.cp_r(src, dest)
        assert length(files) == 2

        assert File.exists?(tmp_path("tmp/dest/sym/1.txt"))
        assert File.exists?(tmp_path("tmp/dest/sym/a/2.txt"))
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "cp_r with dereference absolute symlink" do
      linked_src = fixture_path("cp_r")
      src = tmp_path("tmp/src")
      dest = tmp_path("tmp/dest")

      File.mkdir_p!(src)
      :ok = :file.make_symlink(Path.join(linked_src, "a"), Path.join(src, "sym"))

      try do
        {:ok, files} = File.cp_r(src, dest, dereference_symlinks: true)
        assert length(files) == 5

        assert File.exists?(tmp_path("tmp/dest/sym/1.txt"))
        assert File.exists?(tmp_path("tmp/dest/sym/a/2.txt"))
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    @tag :unix
    test "cp_r with relative symlink" do
      doc = tmp_path("tmp/doc")
      src = tmp_path("tmp/src")
      dest = tmp_path("tmp/dest")

      File.mkdir_p!(src)
      File.write!(doc, "hello")
      :ok = :file.make_symlink("../doc", Path.join(src, "sym"))

      try do
        {:ok, files} = File.cp_r(src, dest)
        assert length(files) == 2
        assert File.lstat!(tmp_path("tmp/dest/sym")).type == :symlink
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    @tag :unix
    test "cp_r with dereference relative symlink" do
      doc = tmp_path("tmp/doc")
      src = tmp_path("tmp/src")
      dest = tmp_path("tmp/dest")

      File.mkdir_p!(src)
      File.write!(doc, "hello")
      :ok = :file.make_symlink("../doc", Path.join(src, "sym"))

      try do
        {:ok, files} = File.cp_r(src, dest, dereference_symlinks: true)
        assert length(files) == 2
        assert File.lstat!(tmp_path("tmp/dest/sym")).type == :regular
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    @tag :unix
    test "cp_r with dereference symlink cycle returns eloop error" do
      src = tmp_path("tmp/src")
      dest = tmp_path("tmp/dest")

      File.mkdir_p!(src)
      :ok = :file.make_symlink(Path.join(src, "b"), Path.join(src, "a"))
      :ok = :file.make_symlink(Path.join(src, "a"), Path.join(src, "b"))

      try do
        assert {:error, :eloop, _} = File.cp_r(src, dest, dereference_symlinks: true)
      after
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end

    test "cp_r with dir and file conflict" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp")

      try do
        File.mkdir(dest)
        File.write!(Path.join(dest, "a"), "hello")
        assert io_error?(File.cp_r(src, dest))
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src dir and dest dir using lists" do
      src = fixture_path("cp_r") |> to_charlist()
      dest = tmp_path("tmp") |> to_charlist()

      File.mkdir(dest)

      try do
        refute File.exists?(tmp_path("tmp/a/1.txt"))
        refute File.exists?(tmp_path("tmp/a/a/2.txt"))
        refute File.exists?(tmp_path("tmp/b/3.txt"))

        {:ok, files} = File.cp_r(src, dest)
        assert length(files) == 7
        assert Enum.all?(files, &is_binary/1)

        assert File.exists?(tmp_path("tmp/a/1.txt"))
        assert File.exists?(tmp_path("tmp/a/a/2.txt"))
        assert File.exists?(tmp_path("tmp/b/3.txt"))
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src with file conflict" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp")

      File.mkdir_p(tmp_path("tmp/a"))
      File.write!(tmp_path("tmp/a/1.txt"), "hello")

      try do
        assert File.exists?(tmp_path("tmp/a/1.txt"))
        File.cp_r(src, dest)
        assert File.read!(tmp_path("tmp/a/1.txt")) == ""
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r with src with file conflict callback" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp")

      File.mkdir_p(tmp_path("tmp/a"))
      File.write!(tmp_path("tmp/a/1.txt"), "hello")

      try do
        assert File.exists?(tmp_path("tmp/a/1.txt"))

        File.cp_r(src, dest,
          on_conflict: fn src_file, dest_file ->
            assert src_file == fixture_path("cp_r/a/1.txt")
            assert dest_file == tmp_path("tmp/a/1.txt")
            false
          end
        )

        assert File.read!(tmp_path("tmp/a/1.txt")) == "hello"
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r!" do
      src = fixture_path("cp_r")
      dest = tmp_path("tmp")

      File.mkdir(dest)

      try do
        refute File.exists?(tmp_path("tmp/a/1.txt"))
        refute File.exists?(tmp_path("tmp/a/a/2.txt"))
        refute File.exists?(tmp_path("tmp/b/3.txt"))

        assert length(File.cp_r!(src, dest)) == 7

        assert File.exists?(tmp_path("tmp/a/1.txt"))
        assert File.exists?(tmp_path("tmp/a/a/2.txt"))
        assert File.exists?(tmp_path("tmp/b/3.txt"))
      after
        File.rm_rf(dest)
      end
    end

    test "cp_r! with src unknown" do
      src = fixture_path("unknown")
      dest = tmp_path("tmp")

      message =
        "could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{src}: no such file or directory"

      assert_raise File.CopyError, message, fn ->
        File.cp_r!(src, dest)
      end
    end

    test "cp_r! with file src and dest unknown" do
      src = fixture_path("cp_r/a/1.txt")
      dest = tmp_path("tmp/unknown/")

      message =
        "could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: no such file or directory"

      assert_raise File.CopyError, message, fn ->
        File.cp_r!(src, dest)
      end
    end

    test "cp_r with destination inside source returns error" do
      src = tmp_path("tmp/src")
      dest = tmp_path("tmp/src/subdir/dest")

      File.mkdir_p!(src)
      File.write!(Path.join(src, "file.txt"), "hello")

      try do
        assert File.cp_r(src, dest) == {:error, :einval, dest}
        refute File.exists?(dest)
      after
        File.rm_rf(src)
      end
    end

    test "cp_r! with destination inside source raises" do
      src = tmp_path("tmp/src")
      dest = tmp_path("tmp/src/subdir/dest")

      File.mkdir_p!(src)
      File.write!(Path.join(src, "file.txt"), "hello")

      try do
        message =
          "could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: invalid argument"

        assert_raise File.CopyError, message, fn ->
          File.cp_r!(src, dest)
        end

        refute File.exists?(dest)
      after
        File.rm_rf(src)
      end
    end

    test "cp preserves mode" do
      File.mkdir_p!(tmp_path("tmp"))
      src = fixture_path("cp_mode")
      dest = tmp_path("tmp/cp_mode")

      File.cp!(src, dest)
      %File.Stat{mode: src_mode} = File.stat!(src)
      %File.Stat{mode: dest_mode} = File.stat!(dest)
      assert src_mode == dest_mode

      # On overwrite
      File.cp!(src, dest, on_conflict: fn _, _ -> true end)
      %File.Stat{mode: src_mode} = File.stat!(src)
      %File.Stat{mode: dest_mode} = File.stat!(dest)
      assert src_mode == dest_mode
    end

    test "cp_r preserves directory mode with preserve_directory_permissions: true" do
      src = tmp_path("tmp/src_dir")
      dest = tmp_path("tmp/dest_dir")
      inner = Path.join(src, "inner")

      File.mkdir_p!(inner)
      File.write!(Path.join(inner, "hello"), "world")
      File.chmod!(inner, 0o500)

      try do
        File.cp_r!(src, dest, preserve_directory_permissions: true)

        %File.Stat{mode: src_mode} = File.stat!(inner)
        %File.Stat{mode: dest_mode} = File.stat!(Path.join(dest, "inner"))
        assert src_mode == dest_mode

        assert File.read!(Path.join(dest, "inner/hello")) == "world"
      after
        File.chmod!(inner, 0o700)
        File.chmod!(Path.join(dest, "inner"), 0o700)
        File.rm_rf!(src)
        File.rm_rf!(dest)
      end
    end

    @tag :unix
    test "cp_r skips sockets and other special files" do
      # We use tmpdir because macOS has a limit on socket paths
      src = Path.join(System.tmp_dir(), "src_with_socket")
      dest = tmp_path("dest_with_socket")
      socket_path = Path.join(src, "test.sock")
      regular_path = Path.join(src, "regular.txt")

      File.mkdir_p!(src)
      File.write!(regular_path, "content")

      {:ok, socket} = :gen_tcp.listen(0, [:local, {:ifaddr, {:local, socket_path}}])

      try do
        assert File.exists?(socket_path)
        assert :elixir_utils.read_link_type(socket_path) == {:ok, :other}

        {:ok, copied_files} = File.cp_r(src, dest)
        assert Path.join(dest, "regular.txt") in copied_files

        refute File.exists?(Path.join(dest, "test.sock"))
        refute Path.join(dest, "test.sock") in copied_files
      after
        :gen_tcp.close(socket)
        File.rm_rf(src)
        File.rm_rf(dest)
      end
    end
  end

  defmodule Queries do
    use ExUnit.Case

    test "regular" do
      assert File.regular?(__ENV__.file)
      assert File.regular?(String.to_charlist(__ENV__.file))
      refute File.regular?("#{__ENV__.file}.unknown")
    end

    test "exists" do
      assert File.exists?(__ENV__.file)
      assert File.exists?(fixture_path())
      assert File.exists?(fixture_path("file.txt"))

      refute File.exists?(fixture_path("missing.txt"))
      refute File.exists?("_missing.txt")
    end

    test "exists with dangling symlink" do
      invalid_file = tmp_path("invalid_file")
      dest = tmp_path("dangling_symlink")
      File.ln_s(invalid_file, dest)

      try do
        refute File.exists?(dest)
      after
        File.rm(dest)
      end
    end
  end

  test "ls" do
    {:ok, value} = File.ls(fixture_path())
    assert "code_sample.exs" in value
    assert "file.txt" in value

    {:error, :enoent} = File.ls(fixture_path("non-existent-subdirectory"))
  end

  test "ls!" do
    value = File.ls!(fixture_path())
    assert "code_sample.exs" in value
    assert "file.txt" in value

    assert_raise File.Error, fn ->
      File.ls!(fixture_path("non-existent-subdirectory"))
    end
  end

  describe "open-read-write" do
    test "read with binary" do
      assert {:ok, "FOO\n"} = File.read(fixture_path("file.txt"))
      assert {:error, :enoent} = File.read(fixture_path("missing.txt"))
    end

    test "read with list" do
      assert {:ok, "FOO\n"} = File.read(Path.expand(~c"fixtures/file.txt", __DIR__))
      assert {:error, :enoent} = File.read(Path.expand(~c"fixtures/missing.txt", __DIR__))
    end

    test "read with UTF-8" do
      assert {:ok, "Русский\n日\n"} = File.read(Path.expand(~c"fixtures/utf8.txt", __DIR__))
    end

    test "read with :raw options" do
      assert {:ok, "FOO\n"} = File.read(fixture_path("file.txt"), [:raw])
    end

    test "read!" do
      assert File.read!(fixture_path("file.txt")) == "FOO\n"
      expected_message = "could not read file \"fixtures/missing.txt\": no such file or directory"

      assert_raise File.Error, expected_message, fn ->
        File.read!("fixtures/missing.txt")
      end
    end

    test "write ASCII content" do
      fixture = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(fixture)
        assert File.write(fixture, ~c"test text") == :ok
        assert File.read(fixture) == {:ok, "test text"}
      after
        File.rm(fixture)
      end
    end

    test "write UTF-8" do
      fixture = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(fixture)
        assert File.write(fixture, "Русский\n日\n") == :ok
        assert {:ok, "Русский\n日\n"} == File.read(fixture)
      after
        File.rm(fixture)
      end
    end

    test "write with options" do
      fixture = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(fixture)
        assert File.write(fixture, "Русский\n日\n") == :ok
        assert File.write(fixture, "test text", [:append]) == :ok
        assert {:ok, "Русский\n日\ntest text"} == File.read(fixture)
      after
        File.rm(fixture)
      end
    end

    test "open file without modes" do
      {:ok, file} = File.open(fixture_path("file.txt"))
      assert IO.gets(file, "") == "FOO\n"
      assert File.close(file) == :ok
    end

    test "open file with charlist" do
      {:ok, file} = File.open(fixture_path("file.txt"), [:charlist])
      assert IO.gets(file, "") == ~c"FOO\n"
      assert File.close(file) == :ok
    end

    test "open UTF-8 by default" do
      {:ok, file} = File.open(fixture_path("utf8.txt"), [:utf8])
      assert IO.gets(file, "") == "Русский\n"
      assert File.close(file) == :ok
    end

    test "open readonly by default" do
      {:ok, file} = File.open(fixture_path("file.txt"))
      assert_raise ArgumentError, fn -> IO.write(file, "foo") end
      assert File.close(file) == :ok
    end

    test "open with write permission" do
      fixture = tmp_path("tmp_text.txt")

      try do
        {:ok, file} = File.open(fixture, [:write])
        assert IO.write(file, "foo") == :ok
        assert File.close(file) == :ok
        assert File.read(fixture) == {:ok, "foo"}
      after
        File.rm(fixture)
      end
    end

    test "open with binwrite permission" do
      fixture = tmp_path("tmp_text.txt")

      try do
        {:ok, file} = File.open(fixture, [:write])
        assert IO.binwrite(file, "Русский") == :ok
        assert File.close(file) == :ok
        assert_raise ErlangError, fn -> IO.binwrite(file, "Русский") end
        assert File.read(fixture) == {:ok, "Русский"}
      after
        File.rm(fixture)
      end
    end

    test "open UTF-8 and charlist" do
      {:ok, file} = File.open(fixture_path("utf8.txt"), [:charlist, :utf8])
      assert IO.gets(file, "") == [1056, 1091, 1089, 1089, 1082, 1080, 1081, 10]
      assert File.close(file) == :ok
    end

    test "open respects encoding" do
      {:ok, file} = File.open(fixture_path("utf8.txt"), [{:encoding, :latin1}])

      data =
        <<195, 144, 194, 160, 195, 145, 194, 131, 195, 145, 194, 129, 195, 145>> <>
          <<194, 129, 195, 144, 194, 186, 195, 144, 194, 184, 195, 144, 194, 185, 10>>

      assert IO.gets(file, "") == data
      assert File.close(file) == :ok
    end

    test "open a missing file" do
      assert File.open(~c"missing.txt") == {:error, :enoent}
    end

    test "open a file with function" do
      file = fixture_path("file.txt")
      assert File.open(file, &IO.read(&1, :line)) == {:ok, "FOO\n"}
    end

    test "open! a missing file" do
      message = "could not open \"missing.txt\": no such file or directory"

      assert_raise File.Error, message, fn ->
        File.open!(~c"missing.txt")
      end
    end

    test "open! a file with function" do
      file = fixture_path("file.txt")
      assert File.open!(file, &IO.read(&1, :line)) == "FOO\n"
    end
  end

  describe "mkdir" do
    test "mkdir with binary" do
      fixture = tmp_path("tmp_test")

      try do
        refute File.exists?(fixture)
        assert File.mkdir(fixture) == :ok
        assert File.exists?(fixture)
      after
        File.rmdir(fixture)
      end
    end

    test "mkdir with list" do
      fixture = tmp_path("tmp_test") |> to_charlist()

      try do
        refute File.exists?(fixture)
        assert File.mkdir(fixture) == :ok
        assert File.exists?(fixture)
      after
        File.rmdir(fixture)
      end
    end

    test "mkdir with invalid path" do
      fixture = fixture_path("file.txt")
      invalid = Path.join(fixture, "test")
      assert File.exists?(fixture)
      assert io_error?(File.mkdir(invalid))
      refute File.exists?(invalid)
    end

    test "mkdir!" do
      fixture = tmp_path("tmp_test")

      try do
        refute File.exists?(fixture)
        assert File.mkdir!(fixture) == :ok
        assert File.exists?(fixture)
      after
        File.rmdir(fixture)
      end
    end

    test "mkdir! with invalid path" do
      fixture = fixture_path("file.txt")
      invalid = Path.join(fixture, "test")
      assert File.exists?(fixture)

      message =
        ~r"\Acould not make directory #{inspect(invalid)}: (not a directory|no such file or directory)"

      assert_raise File.Error, message, fn ->
        File.mkdir!(invalid)
      end
    end

    test "mkdir_p with one directory" do
      fixture = tmp_path("tmp_test")

      try do
        refute File.exists?(fixture)
        assert File.mkdir_p(fixture) == :ok
        assert File.exists?(fixture)
      after
        File.rm_rf(fixture)
      end
    end

    test "mkdir_p with nested directory and binary" do
      base = tmp_path("tmp_test")
      fixture = Path.join(base, "test")
      refute File.exists?(base)

      try do
        assert File.mkdir_p(fixture) == :ok
        assert File.exists?(base)
        assert File.exists?(fixture)
      after
        File.rm_rf(base)
      end
    end

    test "mkdir_p with nested directory and list" do
      base = tmp_path("tmp_test") |> to_charlist()
      fixture = Path.join(base, "test")
      refute File.exists?(base)

      try do
        assert File.mkdir_p(fixture) == :ok
        assert File.exists?(base)
        assert File.exists?(fixture)
      after
        File.rm_rf(base)
      end
    end

    test "mkdir_p with nested directory and existing parent" do
      base = tmp_path("tmp_test")
      fixture = Path.join(base, "test")

      File.mkdir(base)

      try do
        assert File.mkdir_p(fixture) == :ok
        assert File.exists?(base)
        assert File.exists?(fixture)
      after
        File.rm_rf(base)
      end
    end

    test "mkdir_p with invalid path" do
      assert File.exists?(fixture_path("file.txt"))
      invalid = Path.join(fixture_path("file.txt"), "test/foo")
      assert io_error?(File.mkdir(invalid))
      refute File.exists?(invalid)
    end

    test "mkdir_p!" do
      fixture = tmp_path("tmp_test")

      try do
        refute File.exists?(fixture)
        assert File.mkdir_p!(fixture) == :ok
        assert File.exists?(fixture)
      after
        File.rm_rf(fixture)
      end
    end

    test "mkdir_p! with invalid path" do
      fixture = fixture_path("file.txt")
      invalid = Path.join(fixture, "test")
      assert File.exists?(fixture)

      message =
        ~r"\Acould not make directory \(with -p\) #{inspect(invalid)}: (not a directory|no such file or directory)"

      assert_raise File.Error, message, fn ->
        File.mkdir_p!(invalid)
      end
    end

    @tag :unix
    test "mkdir_p with non-accessible parent directory" do
      fixture = tmp_path("tmp_test_parent")

      try do
        refute File.exists?(fixture)
        assert File.mkdir_p!(fixture) == :ok
        %File.Stat{mode: orig_mode} = File.stat!(fixture)
        assert File.chmod!(fixture, 0o000) == :ok

        child = Path.join(fixture, "child")
        refute File.exists?(child)

        assert File.mkdir_p(child) == {:error, :eacces}
        refute File.exists?(child)

        assert File.chmod!(fixture, orig_mode) == :ok
      after
        File.rm_rf(fixture)
      end
    end
  end

  describe "rm" do
    test "rm file" do
      fixture = tmp_path("tmp_test.txt")
      File.write(fixture, "test")
      assert File.exists?(fixture)
      assert File.rm(fixture) == :ok
      refute File.exists?(fixture)
    end

    test "rm read only file" do
      fixture = tmp_path("tmp_test.txt")
      File.write(fixture, "test")
      assert File.exists?(fixture)
      File.chmod(fixture, 0o100444)
      assert File.rm(fixture) == :ok
      refute File.exists?(fixture)
    end

    test "rm file with dir" do
      assert File.rm(fixture_path()) == {:error, :eperm}
    end

    test "rm nonexistent file" do
      assert File.rm(~c"missing.txt") == {:error, :enoent}
    end

    test "rm!" do
      fixture = tmp_path("tmp_test.txt")
      File.write(fixture, "test")
      assert File.exists?(fixture)
      assert File.rm!(fixture) == :ok
      refute File.exists?(fixture)
    end

    test "rm! with invalid file" do
      message = "could not remove file \"missing.file\": no such file or directory"

      assert_raise File.Error, message, fn ->
        File.rm!("missing.file")
      end
    end

    test "rmdir" do
      fixture = tmp_path("tmp_test")
      File.mkdir_p(fixture)
      assert File.dir?(fixture)
      assert File.rmdir(fixture) == :ok
      refute File.exists?(fixture)
    end

    test "rmdir with file" do
      assert io_error?(File.rmdir(fixture_path("file.txt")))
    end

    test "rmdir!" do
      fixture = tmp_path("tmp_test")
      File.mkdir_p(fixture)
      assert File.dir?(fixture)
      assert File.rmdir!(fixture) == :ok
      refute File.exists?(fixture)
    end

    test "rmdir! with file" do
      fixture = fixture_path("file.txt")
      message = ~r"\Acould not remove directory #{inspect(fixture)}: (not a directory|I/O error)"

      assert_raise File.Error, message, fn ->
        File.rmdir!(fixture)
      end
    end

    test "rmdir! error messages" do
      fixture = tmp_path("tmp_test")
      File.mkdir_p(fixture)
      File.touch(fixture <> "/file")

      # directory is not empty
      dir_not_empty_message =
        "could not remove directory #{inspect(fixture)}: directory is not empty"

      assert_raise File.Error, dir_not_empty_message, fn ->
        File.rmdir!(fixture)
      end

      # directory does not exist
      non_existent_dir = fixture <> "/non_existent_dir"

      non_existent_dir_message =
        ~r"\Acould not remove directory #{inspect(non_existent_dir)}: (not a directory|no such file or directory)"

      assert_raise File.Error, non_existent_dir_message, fn ->
        File.rmdir!(non_existent_dir)
      end

      File.rm_rf(fixture)
    end

    test "rm_rf" do
      fixture = tmp_path("tmp")
      File.mkdir(fixture)
      File.cp_r!(fixture_path("cp_r"), fixture)

      assert File.exists?(tmp_path("tmp/a/1.txt"))
      assert File.exists?(tmp_path("tmp/a/a/2.txt"))
      assert File.exists?(tmp_path("tmp/b/3.txt"))

      {:ok, files} = File.rm_rf(fixture)
      assert length(files) == 7
      assert fixture in files
      assert tmp_path("tmp/a/1.txt") in files

      refute File.exists?(tmp_path("tmp/a/1.txt"))
      refute File.exists?(tmp_path("tmp/a/a/2.txt"))
      refute File.exists?(tmp_path("tmp/b/3.txt"))
      refute File.exists?(fixture)
    end

    test "rm_rf raises on path with null byte" do
      assert_raise ArgumentError, ~r/null byte/, fn -> File.rm_rf("foo\0bar") end
    end

    test "rm_rf with symlink" do
      from = tmp_path("tmp/from")
      to = tmp_path("tmp/to")

      File.mkdir_p!(to)
      File.write!(Path.join(to, "hello"), "world")
      :file.make_symlink(to, from)

      if File.exists?(from) or not windows?() do
        assert File.exists?(from)

        {:ok, files} = File.rm_rf(from)
        assert length(files) == 1

        assert File.exists?(Path.join(to, "hello"))
        refute File.exists?(from)
      end
    after
      File.rm(tmp_path("tmp/from"))
    end

    test "rm_rf with charlist" do
      fixture = tmp_path("tmp") |> to_charlist()
      File.mkdir(fixture)
      File.cp_r!(fixture_path("cp_r"), fixture)

      assert File.exists?(tmp_path("tmp/a/1.txt"))
      assert File.exists?(tmp_path("tmp/a/a/2.txt"))
      assert File.exists?(tmp_path("tmp/b/3.txt"))

      {:ok, files} = File.rm_rf(fixture)
      assert length(files) == 7
      assert tmp_path("tmp") in files
      assert Enum.all?(files, &is_binary/1)

      refute File.exists?(tmp_path("tmp/a/1.txt"))
      refute File.exists?(tmp_path("tmp/a/a/2.txt"))
      refute File.exists?(tmp_path("tmp/b/3.txt"))
      refute File.exists?(fixture)
    end

    test "rm_rf with file" do
      fixture = tmp_path("tmp")
      File.write(fixture, "hello")
      assert File.rm_rf(fixture) == {:ok, [fixture]}
    end

    test "rm_rf with write-only subdir" do
      dir = tmp_path("tmp")
      subdir = Path.join(dir, "write-only")
      File.mkdir_p!(subdir)
      File.chmod!(subdir, 0o222)
      assert File.rm_rf(dir) == {:ok, [dir, subdir]}
    end

    test "rm_rf with unknown" do
      fixture = tmp_path("tmp.unknown")
      assert File.rm_rf(fixture) == {:ok, []}
    end

    test "rm_rf with invalid" do
      fixture = fixture_path("file.txt/path")
      assert File.rm_rf(fixture) == {:ok, []}
    end

    test "rm_rf!" do
      fixture = tmp_path("tmp")
      File.mkdir(fixture)
      File.cp_r!(fixture_path("cp_r"), fixture)

      assert File.exists?(tmp_path("tmp/a/1.txt"))
      assert File.exists?(tmp_path("tmp/a/a/2.txt"))
      assert File.exists?(tmp_path("tmp/b/3.txt"))

      files = File.rm_rf!(fixture)
      assert length(files) == 7
      assert fixture in files
      assert tmp_path("tmp/a/1.txt") in files

      refute File.exists?(tmp_path("tmp/a/1.txt"))
      refute File.exists?(tmp_path("tmp/a/a/2.txt"))
      refute File.exists?(tmp_path("tmp/b/3.txt"))
      refute File.exists?(fixture)
    end

    test "rm_rf! with invalid path" do
      fixture = fixture_path("file.txt/path")
      assert File.rm_rf!(fixture) == []
    end
  end

  describe "stat" do
    test "stat" do
      {:ok, info} = File.stat(__ENV__.file)
      assert info.mtime
    end

    test "stat!" do
      assert File.stat!(__ENV__.file).mtime
    end

    test "stat with invalid file" do
      assert {:error, _} = File.stat("./invalid_file")
    end

    test "stat! with invalid_file" do
      assert_raise File.Error, fn ->
        File.stat!("./invalid_file")
      end
    end

    test "lstat" do
      {:ok, info} = File.lstat(__ENV__.file)
      assert info.mtime
    end

    test "lstat!" do
      assert File.lstat!(__ENV__.file).mtime
    end

    test "lstat with invalid file" do
      invalid_file = tmp_path("invalid_file")
      assert {:error, _} = File.lstat(invalid_file)
    end

    test "lstat! with invalid file" do
      invalid_file = tmp_path("invalid_file")

      assert_raise File.Error, fn ->
        File.lstat!(invalid_file)
      end
    end

    test "lstat with dangling symlink" do
      invalid_file = tmp_path("invalid_file")
      dest = tmp_path("dangling_symlink")
      File.ln_s(invalid_file, dest)

      try do
        assert {:ok, info} = File.lstat(dest)
        assert info.type == :symlink
      after
        File.rm(dest)
      end
    end

    test "lstat! with dangling symlink" do
      invalid_file = tmp_path("invalid_file")
      dest = tmp_path("dangling_symlink")
      File.ln_s(invalid_file, dest)

      try do
        assert File.lstat!(dest).type == :symlink
      after
        File.rm(dest)
      end
    end
  end

  describe "IO stream" do
    test "IO stream UTF-8" do
      src = File.open!(fixture_path("file.txt"), [:utf8])
      dest = tmp_path("tmp_test.txt")

      try do
        stream = IO.stream(src, :line)

        File.open(dest, [:write], fn target ->
          Enum.into(stream, IO.stream(target, :line), &String.replace(&1, "O", "A"))
        end)

        assert File.read(dest) == {:ok, "FAA\n"}
      after
        File.rm(dest)
      end
    end

    test "IO stream" do
      src = File.open!(fixture_path("file.txt"))
      dest = tmp_path("tmp_test.txt")

      try do
        stream = IO.binstream(src, :line)

        File.open(dest, [:write], fn target ->
          Enum.into(stream, IO.binstream(target, :line), &String.replace(&1, "O", "A"))
        end)

        assert File.read(dest) == {:ok, "FAA\n"}
      after
        File.rm(dest)
      end
    end
  end

  describe "links" do
    test "read_link with regular file" do
      dest = tmp_path("symlink")
      File.touch(dest)

      try do
        assert File.read_link(dest) == {:error, :einval}
      after
        File.rm(dest)
      end
    end

    test "read_link with nonexistent file" do
      dest = tmp_path("does_not_exist")
      assert File.read_link(dest) == {:error, :enoent}
    end

    test "read_link! with nonexistent file" do
      dest = tmp_path("does_not_exist")
      assert_raise File.Error, fn -> File.read_link!(dest) end
    end

    @tag :unix
    test "read_link with symlink" do
      target = tmp_path("does_not_need_to_exist")
      dest = tmp_path("symlink")
      File.ln_s(target, dest)

      try do
        assert File.read_link(dest) == {:ok, target}
      after
        File.rm(dest)
      end
    end

    @tag :unix
    test "read_link! with symlink" do
      target = tmp_path("does_not_need_to_exist")
      dest = tmp_path("symlink")
      File.ln_s(target, dest)

      try do
        assert File.read_link!(dest) == target
      after
        File.rm(dest)
      end
    end

    test "ln" do
      existing = fixture_path("file.txt")
      new = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(new)
        assert File.ln(existing, new) == :ok
        assert File.read(new) == {:ok, "FOO\n"}
      after
        File.rm(new)
      end
    end

    test "ln with existing destination" do
      existing = fixture_path("file.txt")
      assert File.ln(existing, existing) == {:error, :eexist}
    end

    test "ln! with existing destination" do
      assert_raise File.LinkError, fn ->
        existing = fixture_path("file.txt")
        File.ln!(existing, existing)
      end
    end

    test "ln_s" do
      existing = fixture_path("file.txt")
      new = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(new)
        assert File.ln_s(existing, new) == :ok
        assert File.read(new) == {:ok, "FOO\n"}
      after
        File.rm(new)
      end
    end

    test "ln_s with existing destination" do
      existing = fixture_path("file.txt")
      assert File.ln_s(existing, existing) == {:error, :eexist}
    end

    test "ln_s! with existing destination" do
      existing = fixture_path("file.txt")

      assert_raise File.LinkError, fn ->
        File.ln_s!(existing, existing)
      end
    end
  end

  describe "copy" do
    test "copy" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(dest)
        assert File.copy(src, dest) == {:ok, 4}
        assert File.read(dest) == {:ok, "FOO\n"}
      after
        File.rm(dest)
      end
    end

    test "copy with an io_device" do
      {:ok, src} = File.open(fixture_path("file.txt"))
      dest = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(dest)
        assert File.copy(src, dest) == {:ok, 4}
        assert File.read(dest) == {:ok, "FOO\n"}
      after
        File.close(src)
        File.rm(dest)
      end
    end

    test "copy with raw io_device" do
      {:ok, src} = File.open(fixture_path("file.txt"), [:raw])
      dest = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(dest)
        assert File.copy(src, dest) == {:ok, 4}
        assert File.read(dest) == {:ok, "FOO\n"}
      after
        File.close(src)
        File.rm(dest)
      end
    end

    test "copy with ram io_device" do
      {:ok, src} = File.open("FOO\n", [:ram])
      dest = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(dest)
        assert File.copy(src, dest) == {:ok, 4}
        assert File.read(dest) == {:ok, "FOO\n"}
      after
        File.close(src)
        File.rm(dest)
      end
    end

    test "copy with bytes count" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(dest)
        assert File.copy(src, dest, 2) == {:ok, 2}
        assert {:ok, "FO"} == File.read(dest)
      after
        File.rm(dest)
      end
    end

    test "copy with invalid file" do
      src = fixture_path("invalid.txt")
      dest = tmp_path("tmp_test.txt")
      assert File.copy(src, dest, 2) == {:error, :enoent}
    end

    test "copy!" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(dest)
        assert File.copy!(src, dest) == 4
        assert {:ok, "FOO\n"} == File.read(dest)
      after
        File.rm(dest)
      end
    end

    test "copy! with bytes count" do
      src = fixture_path("file.txt")
      dest = tmp_path("tmp_test.txt")

      try do
        refute File.exists?(dest)
        assert File.copy!(src, dest, 2) == 2
        assert {:ok, "FO"} == File.read(dest)
      after
        File.rm(dest)
      end
    end

    test "copy! with invalid file" do
      src = fixture_path("invalid.txt")
      dest = tmp_path("tmp_test.txt")

      message =
        "could not copy from #{inspect(src)} to #{inspect(dest)}: no such file or directory"

      assert_raise File.CopyError, message, fn ->
        File.copy!(src, dest, 2)
      end
    end
  end

  describe "cwd and cd" do
    test "cwd and cd" do
      {:ok, current} = File.cwd()

      try do
        assert File.cd(fixture_path()) == :ok
        assert File.exists?("file.txt")
      after
        File.cd!(current)
      end
    end

    if :file.native_name_encoding() == :utf8 do
      test "cwd and cd with UTF-8" do
        File.mkdir_p(tmp_path("héllò"))

        File.cd!(tmp_path("héllò"), fn ->
          assert Path.basename(File.cwd!()) == "héllò"
        end)
      after
        File.rm_rf(tmp_path("héllò"))
      end
    end

    test "invalid cd" do
      assert io_error?(File.cd(fixture_path("file.txt")))
    end

    test "invalid_cd!" do
      message =
        ~r"\Acould not set current working directory to #{inspect(fixture_path("file.txt"))}: (not a directory|no such file or directory|I/O error)"

      assert_raise File.Error, message, fn ->
        File.cd!(fixture_path("file.txt"))
      end
    end

    test "cd with function" do
      assert File.cd!(fixture_path(), fn ->
               assert File.exists?("file.txt")
               :cd_result
             end) == :cd_result
    end
  end

  describe "touch" do
    test "touch with no file" do
      fixture = tmp_path("tmp_test.txt")
      time = {{2010, 4, 17}, {14, 0, 0}}

      try do
        refute File.exists?(fixture)
        assert File.touch(fixture, time) == :ok
        assert {:ok, ""} == File.read(fixture)
        assert File.stat!(fixture).mtime == time
      after
        File.rm(fixture)
      end
    end

    test "touch with Erlang timestamp" do
      fixture = tmp_path("tmp_erlang_touch.txt")

      try do
        assert File.touch!(fixture, :erlang.universaltime()) == :ok
        stat = File.stat!(fixture)

        assert File.touch!(fixture, last_year()) == :ok
        assert stat.mtime > File.stat!(fixture).mtime
      after
        File.rm(fixture)
      end
    end

    test "touch with posix timestamp" do
      fixture = tmp_path("tmp_posix_touch.txt")

      try do
        assert File.touch!(fixture, System.os_time(:second)) == :ok
        stat = File.stat!(fixture)

        assert File.touch!(fixture, last_year()) == :ok
        assert stat.mtime > File.stat!(fixture).mtime
      after
        File.rm(fixture)
      end
    end

    test "touch with dir" do
      assert File.touch(fixture_path()) == :ok
    end

    test "touch with failure" do
      fixture = fixture_path("file.txt/bar")
      assert io_error?(File.touch(fixture))
    end

    test "touch! raises" do
      fixture = fixture_path("file.txt/bar")

      message =
        ~r"\Acould not touch #{inspect(fixture)}: (not a directory|no such file or directory)"

      assert_raise File.Error, message, fn ->
        File.touch!(fixture)
      end
    end
  end

  describe "ch*" do
    test "chmod with success" do
      fixture = tmp_path("tmp_test.txt")

      File.touch(fixture)

      try do
        assert File.chmod(fixture, 0o100666) == :ok
        stat = File.stat!(fixture)
        assert stat.mode == 0o100666

        if not windows?() do
          assert File.chmod(fixture, 0o100777) == :ok
          stat = File.stat!(fixture)
          assert stat.mode == 0o100777
        end
      after
        File.rm(fixture)
      end
    end

    test "chmod! with success" do
      fixture = tmp_path("tmp_test.txt")

      File.touch(fixture)

      try do
        assert File.chmod!(fixture, 0o100666) == :ok
        stat = File.stat!(fixture)
        assert stat.mode == 0o100666

        if not windows?() do
          assert File.chmod!(fixture, 0o100777) == :ok
          stat = File.stat!(fixture)
          assert stat.mode == 0o100777
        end
      after
        File.rm(fixture)
      end
    end

    test "chmod with failure" do
      fixture = tmp_path("tmp_test.txt")
      File.rm(fixture)

      assert File.chmod(fixture, 0o100777) == {:error, :enoent}
    end

    test "chmod! with failure" do
      fixture = tmp_path("tmp_test.txt")
      File.rm(fixture)

      message = ~r"could not change mode for #{inspect(fixture)}: no such file or directory"

      assert_raise File.Error, message, fn ->
        File.chmod!(fixture, 0o100777)
      end
    end

    test "chgrp with failure" do
      fixture = tmp_path("tmp_test.txt")
      File.rm(fixture)

      assert File.chgrp(fixture, 1) == {:error, :enoent}
    end

    test "chgrp! with failure" do
      fixture = tmp_path("tmp_test.txt")
      File.rm(fixture)

      message = ~r"could not change group for #{inspect(fixture)}: no such file or directory"

      assert_raise File.Error, message, fn ->
        File.chgrp!(fixture, 1)
      end
    end

    test "chown with failure" do
      fixture = tmp_path("tmp_test.txt")
      File.rm(fixture)

      assert File.chown(fixture, 1) == {:error, :enoent}
    end

    test "chown! with failure" do
      fixture = tmp_path("tmp_test.txt")
      File.rm(fixture)

      message = ~r"could not change owner for #{inspect(fixture)}: no such file or directory"

      assert_raise File.Error, message, fn ->
        File.chown!(fixture, 1)
      end
    end
  end

  defp last_year do
    System.os_time(:second) - 365 * 24 * 60 * 60
  end

  defp io_error?(result) do
    elem(result, 1) in [:enotdir, :eio, :enoent, :eisdir]
  end
end
