require "tmpdir"
require "fileutils"
require "timeout"
require "active_support/core_ext/numeric/time"

module Spring
  module Test
    class WatcherTest < ActiveSupport::TestCase
      runnables.delete self # prevent Minitest running this class

      LATENCY = 0.001
      TIMEOUT = 1

      attr_accessor :dir

      def watcher_class
        raise NotImplementedError
      end

      def watcher
        @watcher ||= watcher_class.new(dir, LATENCY)
      end

      def setup
        @dir = File.realpath(Dir.mktmpdir)
      end

      def teardown
        FileUtils.remove_entry_secure @dir
        watcher.stop
      end

      def touch(file, mtime = nil)
        options = {}
        options[:mtime] = mtime if mtime
        FileUtils.touch(file, **options)
      end

      def assert_stale
        timeout = Time.now + TIMEOUT
        sleep LATENCY until watcher.stale? || Time.now > timeout
        assert watcher.stale?
      end

      def assert_not_stale
        sleep LATENCY * 10
        assert !watcher.stale?
      end

      test "starting with no file" do
        file = "#{@dir}/omg"
        touch file, Time.now - 2.seconds

        watcher.start
        watcher.add file

        assert_not_stale
        touch file, Time.now
        assert_stale
      end

      test "is stale when a watched file is updated" do
        file = "#{@dir}/omg"
        touch file, Time.now - 2.seconds

        watcher.add file
        watcher.start

        assert_not_stale
        touch file, Time.now
        assert_stale
      end

      test "is stale when removing files" do
        file = "#{@dir}/omg"
        touch file, Time.now

        watcher.add file
        watcher.start

        assert_not_stale
        FileUtils.rm(file)
        assert_stale
      end

      test "is stale when files are added to a watched directory" do
        subdir = "#{@dir}/subdir"
        FileUtils.mkdir(subdir)

        watcher.add subdir
        watcher.start

        assert_not_stale
        touch "#{subdir}/foo", Time.now - 1.minute
        assert_stale
      end

      test "is stale when a file is changed in a watched directory" do
        subdir = "#{@dir}/subdir"
        FileUtils.mkdir(subdir)
        touch "#{subdir}/foo", Time.now - 1.minute

        watcher.add subdir
        watcher.start

        assert_not_stale
        touch "#{subdir}/foo", Time.now
        assert_stale
      end

      test "adding doesn't wipe stale state" do
        file = "#{@dir}/omg"
        file2 = "#{@dir}/foo"
        touch file, Time.now - 2.seconds
        touch file2, Time.now - 2.seconds

        watcher.add file
        watcher.start

        assert_not_stale

        touch file, Time.now
        watcher.add file2

        assert_stale
      end

      test "on stale" do
        file = "#{@dir}/omg"
        touch file, Time.now - 2.seconds

        stale = false
        watcher.on_stale { stale = true }

        watcher.add file
        watcher.start

        touch file, Time.now

        Timeout.timeout(1) { sleep 0.01 until stale }
        assert stale

        # Check that we only get notified once
        stale = false
        sleep LATENCY * 3
        assert !stale
      end

      test "add relative path" do
        File.write("#{dir}/foo", "foo")
        watcher.add "foo"
        assert_equal ["#{dir}/foo"], watcher.files.keys
      end

      test "add dot relative path" do
        File.write("#{dir}/foo", "foo")
        watcher.add "./foo"
        assert_equal ["#{dir}/foo"], watcher.files.keys
      end

      test "add non existent file" do
        watcher.add './foobar'
        assert watcher.files.empty?
      end

      test "add multiple files" do
        File.write("#{dir}/foo", "foo")
        File.write("#{dir}/bar", "bar")
        watcher.add "foo", "bar"
        assert_equal ["#{dir}/foo", "#{dir}/bar"], watcher.files.keys
      end

      test "add files as nested array" do
        File.write("#{dir}/foo", "foo")
        watcher.add [["foo"]]
        assert_equal ["#{dir}/foo"], watcher.files.keys
      end

      test "add symlink" do
        File.write("#{dir}/bar", "bar")
        File.symlink("#{dir}/bar", "#{dir}/foo")
        watcher.add './foo'
        assert_equal ["#{dir}/bar"], watcher.files.keys
      end

      test "add dangling symlink" do
        File.symlink("#{dir}/bar", "#{dir}/foo")
        watcher.add './foo'
        assert watcher.files.empty?
      end

      test "add directory with dangling symlink" do
        subdir = "#{@dir}/subdir"
        FileUtils.mkdir(subdir)
        File.symlink("dangling", "#{subdir}/foo")

        watcher.add subdir
        assert_not_stale

        # Adding a new file should mark as stale despite the dangling symlink.
        File.write("#{subdir}/new-file", "new")
        watcher.check_stale
        assert_stale
      end
    end
  end
end
