class ArchetypeTest < MiniTest::Unit::TestCase
  private

  UPDATING_TESTS = ENV['UPDATING_TESTS'] and not ENV['UPDATING_TESTS'].empty? and not ENV['CI']
  FAIL_STATUS = UPDATING_TESTS ? :skip : :fail
  SELECTIVE_TESTS = (ENV['ARCHETYPE_TESTS'] and not ENV['ARCHETYPE_TESTS'].empty? and not ENV['CI']) ? ENV['ARCHETYPE_TESTS'].split(',') : nil

  def assert_no_errors(css_file, project_name)
    file = css_file[(css_path(project_name).size+1)..-1]
    msg = "Syntax Error found in #{file}. Results saved into #{save_path(project_name)}/#{file}"
    assert_equal 0, open(css_file).readlines.grep(/Sass::SyntaxError/).size, msg
  end

  def report_and_fail(name, msg, status = FAIL_STATUS)
    ArchetypeTestHelpers.report status, name
    if UPDATING_TESTS
      record_updated_test name, msg
    else
      assert false, msg
    end
  end

  def record_updated_test(name, msg)
    (@updated_tests ||= []) << {
      :name => name,
      :type => @current_file_update
    }
    puts msg
  end

  def assert_renders_correctly(*arguments)
    options = arguments.last.is_a?(Hash) ? arguments.pop : {}
    for name in arguments
      actual_result_file = File.join(css_path, "#{name}.css")
      expected_result_file = File.join(expectation_path, "#{name}.css")

      results_exist = File.exist?(expected_result_file)

      unless UPDATING_TESTS || results_exist
        report_and_fail name, "no expectation set for `#{expected_result_file}`, run `rake test:update` first"
      end

      actual_result = File.read(actual_result_file)
      expected_result = results_exist ? File.read(expected_result_file) : ''

      @current_file_update = results_exist ? :updated : :added

      assert_no_css_diff(expected_result, actual_result, name, "Error in #{expectation_path}/#{name}.css\n")

      ArchetypeTestHelpers.report :pass, name
    end
  end

  def compile_project(project_name, config_block = nil)
    @current_project = project_name
    with_each_configuration_file do |config|

      add_config_default(:expected_dir, 'expected')
      add_config_default(:assets_dir, 'assets')

      cleanup_project_space!

      msg = ["\nCompiling project #{project_name.colorize(:cyan)}"]
      if File.exist?(config)
        Compass.add_configuration(config)
        msg << "with configuration #{config.split('/').slice(-2..-1).join('/').colorize(:cyan)}"
      end

      puts "#{msg.join(' ')}..."

      assert_on_stylesheet_saved

      Compass.configuration.project_path = project_path(project_name)
      Compass.configuration.environment = :production
      args = Compass.configuration.to_compiler_arguments(:logger => Compass::NullLogger.new)

      install_extension_with_config(project_name, config)

      config_block.call(Compass.configuration) if config_block

      if Compass.configuration.sass_path && File.exist?(Compass.configuration.sass_path)
        compiler = Compass::Compiler.new *args
        compiler.clean!
        if SELECTIVE_TESTS
          each_sass_file do |name, path|
            next unless is_test_selected(name)
            dest = File.join(Compass.configuration.css_path, "#{name}.css")
            FileUtils.mkdir_p(File.dirname(dest))
            compiler.compile(path, dest)
          end
        else
          compiler.run
        end
      end

      yield(Compass.configuration) if block_given?

      cleanup_project_space!
      reset!
    end
  rescue
    save_output(project_name)
    raise
  end

  def each_css_file(dir, &block)
    Dir.glob("#{dir}/**/*.css").each(&block)
  end

  def each_sass_file(sass_dir = nil)
    sass_dir ||= template_path
    Dir.glob("#{sass_dir}/**/[^_]*.s[ac]ss").each do |sass_file|
      yield(sass_file[(sass_dir.length+1)..-6], sass_file)
    end
  end

  def save_output(dir)
    FileUtils.rm_rf(save_path(dir))
    FileUtils.cp_r(css_path(dir), save_path(dir)) if File.exist?(css_path(dir))
  end

  def with_each_project
    @all_projects ||= Dir.glob(File.join(all_projects_path, '*'))
    @all_projects.each do |name|
      yield(File.basename(name)) if block_given?
    end
  end

  def with_each_configuration_file
    configs = Dir.glob(File.join(project_path, 'config*.rb'))
    configs = ['config_none.rb'] if configs.empty?
    configs.each do |config|
      yield(config) if block_given?
    end
  end

  def all_projects_path
    absolutize("fixtures/stylesheets")
  end

  def project_path(project_name = @current_project)
    File.join(all_projects_path, project_name.to_s)
  end

  def css_path(project_name = @current_project)
    File.join(project_path(project_name), Compass.configuration.css_dir)
  end

  def assets_path(project_name = @current_project)
    File.join(project_path(project_name), Compass.configuration.assets_dir)
  end

  def template_path(project_name = @current_project)
    File.join(project_path(project_name), Compass.configuration.sass_dir)
  end

  def expectation_path(project_name = @current_project)
    File.join(project_path(project_name), Compass.configuration.expected_dir)
  end

  def save_path(project_name = @current_project)
    File.join(project_path(project_name), "saved")
  end

  def is_test_selected(name)
    return true unless SELECTIVE_TESTS
    SELECTIVE_TESTS.select {|t| return true if File.join(@current_project, name).include?(t)}
    return false
  end

  def assert_no_css_diff(expected, actual, name, msg = nil)
    with_new_diff(cleanup_css(expected), cleanup_css(actual)) do |diff|
      # if there are any lines that were additions or deletions...
      if diff.select { |line| line =~ /^[\+\-]/ }.any?
        # get the full diff, colorize it, and strip out newline warnings
        diff = diff.to_s(:color).gsub(/\n?\\ No newline at end of file/, '').strip
        msg = UPDATING_TESTS ? "#{name}.css has been #{colorize_expection_update}" : msg || ''
        report_and_fail name, "\n#{msg}\n#{'-'*20}\n#{diff}\n#{'-'*20}"
      end
    end
  end

  def cleanup_css(css)
    # strip extra whitespace
    css.strip!
    # remove charset rules...
    css.gsub!(/^@charset[^;]+;/, '')
    # remove sourceMappingURL comments (for Sass 3.4 compat)
    css.gsub!(/\s*\/\*\#\s*sourceMappingURL=.+\*\//, '')
    # encode unicode sequences (for Sass 3.4 compat)
    css.gsub!(/\\u?([\da-fA-F]{4})/, ['\1'.hex].pack('U*'))
    # expand colors if needed (for Sass 3.4 compat)
    css.gsub!(/(\:[^;\}]*\#)([\da-fA-F]{3})([^\da-fA-F])/) do |m|
      pieces = [$1, $2, $3]
      pieces[0] + pieces[1].split('').map{|c| c*2}.join('') + pieces[2]
    end
    # make all hex colors lowercase
    css.gsub!(/(\:[^;\}]*\#)([\da-fA-F]{6})([^\da-fA-F])/){|m| $1 + $2.downcase + $3}
    # strip once more and return
    return css.strip
  end

  def colorize_expection_update(type = @current_file_update)
    colors = {
      :added    => :green,
      :removed  => :red,
      :updated  => :cyan
    }
    return type.to_s.colorize(colors[type])
  end

  def get_relative_file_name(file, path)
    return file.chomp(File.extname(file)).sub(File.join(path, ''), '')
  end

  def install_extension_with_config(extension, config)
    require File.expand_path(File.join(
      File.dirname(__FILE__), '..', '..',
      'extensions', extension,
      'lib', extension
    )) unless extension == 'archetype'

    compass_install = "cd #{project_path(extension)} && compass install #{extension}"
    compass_install << " -c #{config}" if File.exist?(config)
    system compass_install
  end

  def update_expectations
    checkmark = "\u2713 "
    if @updated_tests.nil? or @updated_tests.empty?
      puts "\n#{checkmark}Cool! Looks like all the tests are up to date for #{@current_project}".colorize(:green)
    else
      puts "\n\nThe following tests have been updated for #{@current_project}:".colorize(:yellow)
      @updated_tests.each do |test|
        puts " - #{test[:name]} (#{colorize_expection_update(test[:type])})"
      end
      puts "Are all of these changes expected? [y/n]".colorize(:yellow)
      if (($stdin.gets.chomp)[0] == 'y')
        if SELECTIVE_TESTS
          @updated_tests.each do |test|
            file = "#{test[:name]}.css"
            FileUtils.rm_rf(File.join(expectation_path, file))
            FileUtils.mkdir_p(File.dirname(File.join(expectation_path, file)))
            FileUtils.cp_r(File.join(css_path, file), File.join(expectation_path, file)) unless test[:type] == :removed
          end
        else
          FileUtils.rm_rf(File.join(expectation_path, '.'))
          FileUtils.cp_r(File.join(css_path, '.'), expectation_path)
        end
        puts "#{checkmark}Thanks! The test expectations for #{@current_project} have been updated".colorize(:green)
      else
        puts "Please manually update the test cases and expectations for #{@current_project}".colorize(:red)
      end
    end
    @updated_tests = nil
  end

  def cleanup_project_space!
    ::FileUtils.rm_rf css_path
    ::FileUtils.rm_rf assets_path
  end

  def reset!
    ::Archetype::SassExtensions::Styleguide.reset!
    ::Archetype::Functions::StyleguideMemoizer.reset!
    Compass.reset_configuration!
  end

  def with_new_diff(*arguments)
    begin
      yield Diffy::Diff.new(*arguments) if block_given?
    rescue Errno::EBADF => e # rescue from JRuby's `Bad file descriptor` (see https://github.com/samg/diffy/issues/36)
      sleep(0.05) # sleep for 50ms
      send(__method__, *arguments) # and try again
    end
  end

  def add_config_default(property, value)
    Compass::Configuration.add_configuration_property(property, "config for #{property.to_s}") do
      value
    end
  end
end
