require "../spec_helper"
require "../../support/tempfile"

private class TestRNG(T)
  include Random

  def initialize(@data : Array(T))
    @i = 0
  end

  def next_u : T
    i = @i
    @i = (i + 1) % @data.size
    @data[i]
  end

  def reset
    @i = 0
  end
end

private def normalize_permissions(permissions, *, directory)
  {% if flag?(:win32) %}
    normalized_permissions = 0o444
    normalized_permissions |= 0o222 if permissions.bits_set?(0o200)
    normalized_permissions |= 0o111 if directory
    File::Permissions.new(normalized_permissions)
  {% else %}
    File::Permissions.new(permissions)
  {% end %}
end

describe File do
  describe ".tempname" do
    it "creates a path without creating the file" do
      path = File.tempname

      File.exists?(path).should be_false
      File.dirname(path).should eq Dir.tempdir
    end

    it "accepts single suffix argument" do
      path = File.tempname ".bar"

      File.exists?(path).should be_false
      File.dirname(path).should eq Dir.tempdir
      File.extname(path).should eq(".bar")
    end

    it "accepts prefix and suffix arguments" do
      path = File.tempname "foo", ".bar"

      File.exists?(path).should be_false
      File.dirname(path).should eq Dir.tempdir
      File.extname(path).should eq(".bar")
      File.basename(path).should start_with("foo")
    end

    it "accepts prefix with separator" do
      path = File.tempname "foo/", nil
      File.dirname(path).should eq File.join(Dir.tempdir, "foo")
      File.basename(path).should_not start_with("foo")
    end

    it "accepts dir argument" do
      path = File.tempname(dir: "foo")
      File.dirname(path).should eq("foo")
    end
  end

  describe ".tempfile" do
    it "creates and writes" do
      tempfile = File.tempfile
      tempfile.print "Hello!"
      tempfile.info.permissions.should eq normalize_permissions(0o600, directory: false)
      tempfile.close

      File.exists?(tempfile.path).should be_true
      File.read(tempfile.path).should eq("Hello!")
    ensure
      tempfile.try &.delete
    end

    it "accepts single suffix argument" do
      tempfile = File.tempfile ".bar"
      tempfile.print "Hello!"
      tempfile.info.permissions.should eq normalize_permissions(0o600, directory: false)
      tempfile.close

      File.extname(tempfile.path).should eq(".bar")

      File.exists?(tempfile.path).should be_true
      File.read(tempfile.path).should eq("Hello!")
    ensure
      tempfile.try &.delete
    end

    it "accepts prefix and suffix arguments" do
      tempfile = File.tempfile "foo", ".bar"
      tempfile.print "Hello!"
      tempfile.info.permissions.should eq normalize_permissions(0o600, directory: false)
      tempfile.close

      File.extname(tempfile.path).should eq(".bar")
      File.basename(tempfile.path).should start_with("foo")

      File.exists?(tempfile.path).should be_true
      File.read(tempfile.path).should eq("Hello!")
    ensure
      tempfile.try &.delete
    end

    it "accepts dir argument" do
      file = File.tempfile(dir: datapath)
      File.dirname(file.path).should eq(datapath)
      file.close
    ensure
      file.try &.delete
    end

    it "fails in nonwriteable folder" do
      err_directory = (datapath("non-existing-folder") + Path::SEPARATORS[0]).inspect_unquoted
      expect_raises(File::NotFoundError, "Error creating temporary file: '#{err_directory}") do
        File.tempfile dir: datapath("non-existing-folder")
      end
    end

    it "rejects null byte" do
      expect_raises(ArgumentError, "String contains null byte") do
        File.tempfile("foo\0")
      end
      expect_raises(ArgumentError, "String contains null byte") do
        File.tempfile("foo", "bar\0")
      end
      expect_raises(ArgumentError, "String contains null byte") do
        File.tempfile("foo", "bar", dir: "baz\0")
      end
    end

    describe "with block" do
      it "closes file" do
        filepath = nil
        tempfile = File.tempfile do |tempfile|
          filepath = tempfile.path
        end
        tempfile.path.should eq filepath
        tempfile.closed?.should be_true

        filepath = filepath.not_nil!
        File.exists?(filepath).should be_true
      ensure
        File.delete(filepath) if filepath
      end

      it "accepts single suffix argument" do
        tempfile = File.tempfile(".bar") do |tempfile|
          File.exists?(tempfile.path).should be_true
          tempfile.closed?.should be_false
        end
        tempfile.closed?.should be_true

        File.extname(tempfile.path).should eq(".bar")

        File.exists?(tempfile.path).should be_true
      ensure
        File.delete(tempfile.path) if tempfile
      end

      it "accepts prefix and suffix arguments" do
        tempfile = File.tempfile("foo", ".bar") do |tempfile|
          File.exists?(tempfile.path).should be_true
          tempfile.closed?.should be_false
        end
        tempfile.closed?.should be_true

        File.extname(tempfile.path).should eq(".bar")
        File.basename(tempfile.path).should start_with("foo")

        File.exists?(tempfile.path).should be_true
      ensure
        File.delete(tempfile.path) if tempfile
      end

      it "accepts dir argument" do
        tempfile = File.tempfile(dir: datapath) do |tempfile|
        end
        File.dirname(tempfile.path).should eq(datapath)
      ensure
        File.delete(tempfile.path) if tempfile
      end
    end
  end
end

describe Crystal::System::File do
  describe ".mktemp" do
    it "creates random file name" do
      with_tempfile "random-path" do |tempdir|
        Dir.mkdir tempdir
        fd, path, _ = Crystal::System::File.mktemp("A", "Z", dir: tempdir, random: TestRNG.new([7, 8, 9, 10, 11, 12, 13, 14]))
        path.should eq Path[tempdir, "A789abcdeZ"].to_s
      ensure
        IO::FileDescriptor.new(fd).close if fd
      end
    end

    it "retries when file exists" do
      with_tempfile "retry" do |tempdir|
        Dir.mkdir tempdir
        existing_path = Path[tempdir, "A789abcdeZ"]
        File.touch existing_path
        fd, path, _ = Crystal::System::File.mktemp("A", "Z", dir: tempdir, random: TestRNG.new([7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]))
        path.should eq File.join(tempdir, "AfghijklmZ")
      ensure
        IO::FileDescriptor.new(fd).close if fd
      end
    end

    it "raises when no valid path is found" do
      with_tempfile "random-path" do |tempdir|
        Dir.mkdir tempdir
        File.touch Path[tempdir, "A789abcdeZ"]
        expect_raises(File::AlreadyExistsError, "Error creating temporary file") do
          fd, _, _ = Crystal::System::File.mktemp("A", "Z", dir: tempdir, random: TestRNG.new([7, 8, 9, 10, 11, 12, 13, 14]))
        ensure
          IO::FileDescriptor.new(fd).close if fd
        end
      end
    end
  end
end
