require 'spec_helper'

require 'count_down_latch'
require 'fileutils'
require 'tempfile'

module FSSM::MonitorSpecHelpers
  def create_tmp_dir
    @tmp_dir = FSSM::Pathname.for(Dir.mktmpdir).realpath.to_s
    FileUtils.cp_r File.join(File.dirname(__FILE__), 'root'), @tmp_dir
    # Because git does not track empty directories, create one ourselves.
    FileUtils.mkdir_p @tmp_dir + '/root/yawn'
    @tmp_dir
  end

  def remove_tmp_dir
    FileUtils.remove_entry @tmp_dir
  end

  def create_handler(type, latch)
    lambda do |*args|
      @handler_results[type] << args
      latch.count_down
    end
  end

  def run_monitor(num_events_to_expect=0, options={})
    event_latch      = CountDownLatch.new(num_events_to_expect)
    @handler_results = Hash.new { |hash, key| hash[key] = [] }
    thread           = Thread.new do
      monitor = FSSM::Monitor.new(options)
      monitor.path(@tmp_dir) do |p|
        p.create(&create_handler(:create, event_latch))
        p.update(&create_handler(:update, event_latch))
        p.delete(&create_handler(:delete, event_latch))
      end
      monitor.run
    end
    sleep 1 # give time for monitor to start up
    yield if block_given?
    event_latch.wait
    thread.kill
  end
end

describe "The File System State Monitor" do
  describe "monitor" do
    include FSSM::MonitorSpecHelpers

    before do
      create_tmp_dir
    end

    after do
      remove_tmp_dir
    end

    describe "with default options" do
      it "should call create callback upon file creation" do
        run_monitor(1) do
          file = @tmp_dir + "/newfile.rb"
          File.exists?(file).should be_falsey
          FileUtils.touch file
        end
        @handler_results[:create].should == [[@tmp_dir, 'newfile.rb']]
      end

      it "should call update callback upon file modification" do
        run_monitor(1) do
          FileUtils.touch @tmp_dir + '/root/file.rb'
        end
        @handler_results[:update].should == [[@tmp_dir, 'root/file.rb']]
      end

      it "should call delete callback upon file deletion" do
        run_monitor(1) do
          FileUtils.rm @tmp_dir + "/root/file.rb"
        end
        @handler_results[:delete].should == [[@tmp_dir, 'root/file.rb']]
      end

      it "should call create and delete callbacks upon file renaming in the same directory" do
        run_monitor(2) do
          FileUtils.mv @tmp_dir + "/root/file.rb", @tmp_dir + "/root/old_file.rb"
        end
        @handler_results[:create].should == [[@tmp_dir, 'root/old_file.rb']]
        @handler_results[:delete].should == [[@tmp_dir, 'root/file.rb']]
        @handler_results[:update].should == []
      end

      it "should call create and delete callbacks upon file moving to another directory" do
        run_monitor(2) do
          FileUtils.mv @tmp_dir + "/root/file.rb", @tmp_dir + "/old_file.rb"
        end
        @handler_results[:create].should == [[@tmp_dir, 'old_file.rb']]
        @handler_results[:delete].should == [[@tmp_dir, 'root/file.rb']]
        @handler_results[:update].should == []
      end

      it "should not call callbacks upon directory operations" do
        run_monitor do
          FileUtils.mkdir @tmp_dir + "/another_yawn"
          FileUtils.rmdir @tmp_dir + "/root/yawn"
        end
        @handler_results[:create].should == []
        @handler_results[:delete].should == []
      end
    end

    describe "when configured to consider files and directories" do
      it "should call create callback upon directory creation" do
        run_monitor(1, :directories => true) do
          FileUtils.mkdir @tmp_dir + "/another_yawn"
        end
        @handler_results[:create].should include([@tmp_dir, 'another_yawn', :directory])
      end

      it "should call delete callback upon directory deletion" do
        run_monitor(1, :directories => true) do
          FileUtils.rmdir @tmp_dir + "/root/yawn"
        end
        @handler_results[:delete].should include([@tmp_dir, 'root/yawn', :directory])
      end

      it "should call create, update, and delete callbacks upon directory renaming in the same directory" do
        run_monitor(3, :directories => true) do
          FileUtils.mv @tmp_dir + "/root/yawn", @tmp_dir + "/root/old_yawn"
        end
        @handler_results[:create].should include([@tmp_dir, 'root/old_yawn', :directory])
        @handler_results[:delete].should include([@tmp_dir, 'root/yawn', :directory])
        @handler_results[:update].should include([@tmp_dir, 'root', :directory])
      end

      it "should call create, update, and delete callbacks upon directory moving to another directory" do
        run_monitor(3, :directories => true) do
          FileUtils.mv @tmp_dir + "/root/yawn", @tmp_dir + "/old_yawn"
        end
        @handler_results[:create].should include([@tmp_dir, 'old_yawn', :directory])
        @handler_results[:delete].should include([@tmp_dir, 'root/yawn', :directory])
        @handler_results[:update].should include([@tmp_dir, 'root', :directory])
      end

      it "should call create, update, and delete callbacks upon file renaming in the same directory" do
        run_monitor(3, :directories => true) do
          FileUtils.mv @tmp_dir + "/root/file.rb", @tmp_dir + "/root/old_file.rb"
        end
        @handler_results[:create].should include([@tmp_dir, 'root/old_file.rb', :file])
        @handler_results[:delete].should include([@tmp_dir, 'root/file.rb', :file])
        @handler_results[:update].should include([@tmp_dir, 'root', :directory])
      end

      it "should call create, update, and delete callbacks upon file moving to another directory" do
        run_monitor(3, :directories => true) do
          FileUtils.mv @tmp_dir + "/root/file.rb", @tmp_dir + "/old_file.rb"
        end
        @handler_results[:create].should include([@tmp_dir, 'old_file.rb', :file])
        @handler_results[:delete].should include([@tmp_dir, 'root/file.rb', :file])
        @handler_results[:update].should include([@tmp_dir, 'root', :directory])
      end

      it "should call delete callbacks upon directory structure deletion, in reverse order" do
        expected_delete_events = [
            ['root/yawn', :directory],
            ['root/moo/cow.txt', :file],
            ['root/moo', :directory],
            ['root/file.yml', :file],
            ['root/file.rb', :file],
            ['root/file.css', :file],
            ['root/duck/quack.txt', :file],
            ['root/duck', :directory],
            ['root', :directory]
        ]
        run_monitor(expected_delete_events.size, :directories => true) do
          FileUtils.rm_rf @tmp_dir + '/.'
        end
        @handler_results[:create].should == []
        @handler_results[:delete].should == expected_delete_events.map { |(file, type)| [@tmp_dir, file, type] }
        @handler_results[:update].should == []
      end

      it "should call create callbacks upon directory structure creation, in order" do
        expected_create_events = [
            ['new_root', :directory],
            ['new_root/duck', :directory],
            ['new_root/duck/quack.txt', :file],
            ['new_root/file.css', :file],
            ['new_root/file.rb', :file],
            ['new_root/file.yml', :file],
            ['new_root/moo', :directory],
            ['new_root/moo/cow.txt', :file],
            ['new_root/yawn', :directory]
        ]
        run_monitor(expected_create_events.size, :directories => true) do
          FileUtils.cp_r @tmp_dir + '/root/.', @tmp_dir + '/new_root'
        end
        @handler_results[:create].should == expected_create_events.map { |(file, type)| [@tmp_dir, file, type] }
        @handler_results[:delete].should == []
        @handler_results[:update].should == []
      end
    end
  end
end
