
##
# A collection of custom assertions and helper methods
# to take some of the suck out of testing functionality
# that is based on Sprout features.
#
# Include this module into your test cases to make
# testing Sprout tools easier.
#
#   require 'sprout/test/sprout_test_helper'
#
#   class SomeTestCase < Test::Unit::TestCase
#     include Sprout::TestHelper
#
#     def setup
#       super
#       # do something
#     end
#
#     def teardown
#       super
#       # do something
#     end
#
#     def test_something
#       assert_file File.join(fixtures, 'some_file') do |f|
#         assert_matches /Fred/, f
#       end
#     end
#   end
#
module Sprout::TestHelper

  ##
  # The name of the folder that should contain
  # fixture data.
  FIXTURES_NAME = 'fixtures'

  # Gives us the ability to hide RubyGem output from
  # our test results...
  include Gem::DefaultUserInteraction

  ##
  # Add the skip method that was introduced in Ruby 1.9.1 Test::Unit
  # This doesn't really work all that well...
  if(RUBY_VERSION == '1.8.7')
    def skip message=""
      puts
      puts ">> SproutTestCase.skip called from: #{caller[0]} ( #{message} )"
    end
  end

  ##
  # @return [Dir] Path to a fixtures folder that is next to
  # the text case that calls this method.
  def fixtures from=nil
    @fixtures ||= find_fixtures(from || Sprout.file_from_caller(caller.first))
  end

  ##
  # Override the setup method in order to record the 
  # working directory before the test method runs.
  def setup
    super

    # Prevent log messages from interrupting test output,
    # and create a pipe that test cases can read to ensure
    # user output is provided properly.
    Sprout.stdout = Sprout::OutputBuffer.new
    Sprout.stderr = Sprout::OutputBuffer.new

    @start_path = Dir.pwd
  end

  ##
  # Override the teardown method in order to perform
  # systemic cleanup work like, clearing lingering rake tasks,
  # and removing temporary folders.
  def teardown
    super
    clear_tasks

    remove_file @temp_path
    remove_file @temp_cache

    @temp_path  = nil
    @temp_cache = nil

    if(@start_path && Dir.pwd != @start_path)
      puts "[WARNING] >> SproutTestCase changing dir from #{Dir.pwd} back to: #{@start_path} - Did you mean to leave your working directory in a new place?"
      Dir.chdir @start_path
    end
  end

  protected

  ##
  # Create a temporary folder relative to the
  # test case that calls this method.
  # 
  # @return [Dir] Path to the requested temp folder.
  def temp_path
    caller_file = Sprout.file_from_caller caller.first
    @temp_path ||= make_temp_folder File.dirname(caller_file)
  end

  ##
  # Create a temporary folder relative to the
  # provided path.
  #
  # @param from [Dir] Folder within which a 'tmp' folder should be added.
  #
  # @return [Dir] Path to the requested temp folder.
  def make_temp_folder from
    dir = File.join(fixtures(from), 'tmp')
    FileUtils.mkdir_p dir
    dir
  end

  ##
  # Invoke a Rake task by name.
  #
  # @return [Rake::Task] The task that was invoked.
  def run_task(name)
    t = Rake.application[name]
    t.invoke
    return t
  end
  
  ##
  # Retrieve a registered Rake task by name.
  #
  # @return [Rake::Task] The task that was found.
  def get_task(name)
    return Rake.application[name]
  end

  ##
  # Clear all registered Rake tasks.
  def clear_tasks
    CLEAN.delete_if {|a| true }
    Rake::Task.clear
    Rake.application.clear
  end

  ##
  # Create an empty file at +path+
  #
  # @param [File] The path to the file that should be created.
  # @return [File] The path to the file.
  def create_file path
    dir = File.dirname path
    FileUtils.mkdir_p dir
    FileUtils.touch path
  end

  ##
  # Remove a file if it exists. If no file exists,
  # do nothing.
  #
  # @param path [File] Path to the file that should be removed.
  def remove_file(path=nil)
    if(path && File.exists?(path))
      FileUtils.rm_rf(path)
    end
  end

  ##
  # Assert that a file exists at +path+ and display +message+ 
  # if it does not.
  #
  # @param path [File] Path to the file that should exist.
  # @param message [String] The message that should be displayed if the expected file does not exist.
  # @yield [String] The contents of the file.
  #
  # This method yields the file contents so that you can write 
  # readable tests like:
  #
  #   assert_file File.join(fixtures, 'my_file') do |f|
  #     assert_matches /Johnny/, f
  #   end
  #
  def assert_file(path, message=nil)
    message ||= "Expected file not found at #{path}"
    assert(File.exists?(path), message)
    yield File.read(path) if block_given?
  end

  ##
  # Assert that a directory exists at +path+ and display +message+
  # if it does not.
  #
  # @param path [Dir] Path to the directory that should exist.
  # @param message [String] The message that should be displayed if the expected directory does not exist.
  #
  #    assert_directory File.join(fixtures, 'SomeDir')
  #
  def assert_directory(path, message=nil)
    message ||= "Expected directory not found at #{path}"
    assert(File.directory?(path), message)
  end

  ##
  # Assert that a file exists at +path+ and is not empty. Display
  # +message+ if the file does not exist or if it is empty.
  # @param path [File] Path to the file that should exist.
  # @param message [String] The message that should be displayed if 
  #   the expected file does not exist or is empty.
  #
  #   assert_not_empty File.join(fixtures, 'SomeFile')
  #
  def assert_not_empty(path, message=nil)
    assert_file path, message
    files = FileList["#{path}/*"]
    message ||= " - Expected #{path} to not be empty, but it was"
    assert files.size > 0, message
  end
  
  ##
  # Assert that an expression matches the provided string.
  #
  # This helper mainly makes tests more readable and provides
  # simpler failure messages without extra work.
  #
  #   assert_matches /Fred/, 'Bill, Fred, Bob'
  #
  def assert_matches(expression, string, message='')
    if(expression.is_a?(String))
      expresion = /#{expression}/
    end
    if(!string.match(expression))
      fail "#{message} - '#{string}' should include '#{expression}'"
    end
  end

  ##
  # Create and/or return sprout/cache directory relative to the
  # fixtures folder nearest the file that calls this method.
  # @return [Dir] The path to the cache directory.
  def temp_cache
    dir = File.dirname(Sprout.file_from_caller(caller.first))
    @temp_cache ||= File.join(fixtures(dir), 'sprout', 'cache')
  end

  ##
  # Execute a block as each available Sprout::System, any code
  # within this block that calls Sprout.current_user will
  # receive the currently active Sprout::System.
  #
  #    as_each_system do
  #      puts ">> Sprout.home: #{Sprout.home}"
  #    end
  #
  # This method is primarily used to ensure that we create 
  # system-appropriate paths and processes.
  #
  # NOTE: This process automatically calls Mocha::Mockery.instance.teardown
  # after the yield. This means that any mocks that have been created will
  # no longer be available after the provided block is complete.
  # 
  # @return [Sprout::System::BaseSystem] The concrete class that was created.
  # @yield [Sprout::System::BaseSystem] The concrete subclass of BaseSystem
  #
  def as_each_system
    [
     Sprout::System::VistaSystem.new,
     Sprout::System::WinNixSystem.new,
     Sprout::System::WinSystem.new,
     Sprout::System::JavaSystem.new,
     Sprout::System::OSXSystem.new,
     Sprout::System::UnixSystem.new
    ].each do |sys|
      expectation = Sprout::System.stubs(:create).returns sys
      yield sys if block_given?
      # Ugh - This is way too greedy... We're killing all mocks in here
      # Doing it anyway b/c we need to get Windows support in place...
      # TODO: Implement this feature without clobbering all stubs/mocks
      Mocha::Mockery.instance.teardown
      sys
    end
  end

  ##
  # Execute a block as a UnixSystem.
  #
  #   as_a_unix_system do
  #      puts ">> Sprout.home: #{Sprout.home}"
  #   end
  #
  # @return [Sprout::System::UnixSystem] The Sprout::System that was created.
  # @yield [Sprout::System::UnixSystem] The current Sprout::System.
  def as_a_unix_system
    sys = Sprout::System::UnixSystem.new
    expectation = Sprout::System.stubs(:create).returns sys
    yield sys if block_given?
    # Ugh - This is way too greedy... We're killing all mocks in here
    # Doing it anyway b/c we need to get Windows support in place...
    # TODO: Implement this feature without clobbering all stubs/mocks
    #Mocha::Mockery.instance.teardown
    sys
  end

  ##
  # Execute a block as a OSXSystem.
  #
  #   as_a_mac_system do
  #      puts ">> Sprout.home: #{Sprout.home}"
  #   end
  #
  # @return [Sprout::System::OSXSystem] The Sprout::System that was created.
  # @yield [Sprout::System::OSXSystem] The current Sprout::System.
  def as_a_mac_system
    sys = Sprout::System::OSXSystem.new
    Sprout::System.stubs(:create).returns sys
    yield sys if block_given?
    # Ugh - This is way too greedy... We're killing all mocks in here
    # Doing it anyway b/c we need to get Windows support in place...
    # TODO: Implement this feature without clobbering all stubs/mocks
    #Mocha::Mockery.instance.teardown
    sys
  end
  
  ##
  # Execute a block as a WinSystem.
  #
  #   as_a_windows_system do
  #      puts ">> Sprout.home: #{Sprout.home}"
  #   end
  #
  # @return [Sprout::System::WinSystem] The Sprout::System that was created.
  # @yield [Sprout::System::WinSystem] The current Sprout::System.
  def as_a_windows_system
    sys = Sprout::System::WinSystem.new
    Sprout::System.stubs(:create).returns sys
    yield sys if block_given?
    # Ugh - This is way too greedy... We're killing all mocks in here
    # Doing it anyway b/c we need to get Windows support in place...
    # TODO: Implement this feature without clobbering all stubs/mocks
    #Mocha::Mockery.instance.teardown
    sys
  end

  ##
  # Execute a block as a WinNixSystem.
  #
  #   as_a_win_nix_system do
  #      puts ">> Sprout.home: #{Sprout.home}"
  #   end
  #
  # @return [Sprout::System::WinNixSystem] The Sprout::System that was created.
  # @yield [Sprout::System::WinNixSystem] The current Sprout::System.
  def as_a_win_nix_system
    sys = Sprout::System::WinNixSystem.new
    Sprout::System.stubs(:create).returns sys
    yield sys if block_given?
    # Ugh - This is way too greedy... We're killing all mocks in here
    # Doing it anyway b/c we need to get Windows support in place...
    # TODO: Implement this feature without clobbering all stubs/mocks
    #Mocha::Mockery.instance.teardown
    sys
  end

  
  private

  ##
  # Find the nearest fixtures folder to the provided
  # path by checking each parent directory.
  def find_fixtures path
    # Return nil if path is nil or is not a file:
    find_fixtures_error(path) if(find_reached_root?(path))
    # Get the parent directory if path is a file:
    path = File.dirname(path) if !File.directory? path
    if(should_create_fixtures_for?(path))
      FileUtils.makedirs File.join(path, FIXTURES_NAME)
    end
    # Check for a folder at "#{path}/fixtures":
    fixture_path = File.join(path, FIXTURES_NAME)
    # Return the fixtures folder if found:
    return fixture_path if File.directory? fixture_path
    # Move up one directory and try again:
    return find_fixtures File.dirname(path) unless File.dirname(path) == path
  end

  def should_create_fixtures_for? path
    basename = File.basename path
    return basename == 'test' || basename == 'tests'
  end

  def find_fixtures_error path
    raise Sprout::Errors::UsageError.new "Request to find a fixtures folder failed at: #{path}"
  end

  def find_reached_root? path
    return (path.nil? || !File.exists?(path) || File.dirname(path) == path)
  end

end

  # TODO: Consider adding these:
  # Some generator-related assertions:
  #   assert_generated_file(name, &block) # block passed the file contents
  #   assert_directory_exists(name)
  #   assert_generated_class(name, &block)
  #   assert_generated_module(name, &block)
  #   assert_generated_test_for(name, &block)
  # The assert_generated_(class|module|test_for) &block is passed the body of the class/module within the file
  #   assert_has_method(body, *methods) # check that the body has a list of methods (methods with parentheses not supported yet)
  #
  # Other helper methods are:
  #   app_root_files - put this in teardown to show files generated by the test method (e.g. p app_root_files)
  #   bare_setup - place this in setup method to create the app_root folder for each test
  #   bare_teardown - place this in teardown method to destroy the TMP_ROOT or app_root folder after each test

#Sprout::ProgressBar.debug = true

