# encoding: utf-8
require File.expand_path(File.dirname(__FILE__) + '/../boot.rb')

class LocalizationFilesTest < UnitTestCase

  def assert_no_missing_translations(tags, type)
    clean_our_backtrace do
      missing = tags.uniq.reject(&:has_translation?)
      msg = "Missing #{type} translations:\n" + missing.map(&:inspect).sort.join("\n") + "\n"
      assert_block(msg) { missing.empty? }
    end
  end

################################################################################

  def test_localization_files_exist
    for lang in Language.all
      assert File.exists?(lang.localization_file)
    end
  end

  def test_syntax_of_official_export_file
    errors = []
    lang = Language.official
    Language.clear_verbose_messages
    lang.check_export_syntax
    errors += Language.verbose_messages
    assert_block("Bad syntax in language export files:\n" + errors.join("\n")) { errors.empty? }
  end

  # Make sure all "[:tag]" refs inside the translations exist.
  def test_embedded_refs
    errors = []
    for lang in Language.all
      data = File.open(lang.localization_file, 'r:utf-8') do |fh|
        YAML::load(fh)
      end
      tags = {}
      for tag in data.keys
        tags[tag.to_s.downcase] = true
      end
      for tag, str in data
        if str.is_a?(String)
          str.gsub(/[\[\=]:(\w+)/) do
            unless tags.has_key?($1.downcase)
              errors << "#{lang.locale} :#{tag} [:#{$1}]\n"
            end
          end
        end
      end
    end
    assert_true(errors.empty?, "Found #{errors.length} undefined tag " +
      "reference(s) in language files:\n" + errors.join(''))
  end

  def test_find_missing_tags_and_duplicate_method_defs
    tags = known_tags
    missing_tags = []
    duplicate_function_defs = []
    source_files("#{RAILS_ROOT}/app", "#{RAILS_ROOT}/test") do |file|
      missing_tags += missing_tags_in_file(file, tags)
      duplicate_function_defs += duplicate_function_defs_in_file(file)
    end
    assert_true(missing_tags.empty?, "Found #{missing_tags.length} undefined tag " +
      "reference(s) in source files:\n" + missing_tags.join(''))
    assert_true(duplicate_function_defs.empty?, "Found #{duplicate_function_defs.length} duplicate method " +
      "definition(s) in source files:\n" + duplicate_function_defs.join(''))
  end

  # Get Hash of tags we havbe translations for already.
  def known_tags
    tags = {}
    for tag in Globalite.localizations.keys +
        # these are tags only used in unit tests
        [ :one, :two, :_unit_test_a, :_unit_test_x, :_unit_test_y, :_unit_test_z ]
      tags[tag.to_s.downcase] = true
    end
    return tags
  end

  # Get Array of error messages for tags in +file+ that we don't have translations for.
  def missing_tags_in_file(file, tags)
    errors = []
    n = 0
    for line in File.readlines(file)
      n += 1
      line.sub!(/(^#| # ).*/, '')
      line.gsub(/:(\w+)\.(l|t|tl|tp|tpl| |#|$)(\W|$)/) do
        if !tags.has_key?($1.downcase)
          errors << "#{file} line #{n} [:#{$1}]\n"
        end
      end
    end
    return errors
  end

  # Get Array of error messages for methods in +file+ that are defined more
  # than once.  (I find myself copying and pasting method definitions and
  # forgetting to change the method name frequently.  Ruby does not complain
  # about this, and just overwrites the old method.  This is particularly
  # insidious in the unit tests, because there's absolutely no way to know.) 
  def duplicate_function_defs_in_file(file)
    errors = []
    defs = {}
    stack = []
    n = 0
    for line in File.readlines(file)
      n += 1
      if line.match(/^(\s*)(class|module)\s/)
        space = $1
        stack << [{}, space]
        defs = {}
      elsif line.match(/^(\s*)end(\W|$)/) and stack.any?
        space = $1
        if space == stack[-1][1]
          defs = stack.pop[0]
        end
      elsif line.match(/^\s*def ([^\s()#]+)/)
        if defs[$1]
          errors << "#{file} line #{n} #{$1.inspect}\n"
        else
          defs[$1] = true
        end
      end
    end
    if stack.any?
      errors << "#{file} line #{n} [file didn't parse right, might be due to tabs?]\n"
    end
    return errors
  end

  # Traverse a directory structure looking for source files.
  def source_files(*paths, &block)
    for path in paths
      for file in Dir.glob("#{path}/*")
        if file.match(/\.(rb|rhtml|rxml)$/)
          block.call(file)
        elsif File.directory?(file) and
          file.match(/\/\w+$/)
          source_files(file, &block)
        end
      end
    end
  end

  def test_api_error_translations
    tags = []
    file = "#{RAILS_ROOT}/app/classes/api/errors.rb"
    File.open(file, 'r:utf-8') do |fh|
      fh.each_line do |line|
        if line.match(/^\s*class (\w+) < /) and
           not ['Error', 'ObjectError', 'BadParameterValue'].include?($1)
          tags << "api_#{$1.underscore.gsub('/','_')}".to_sym
        end
      end
    end
    file = "#{RAILS_ROOT}/app/classes/api/parsers.rb"
    File.open(file, 'r:utf-8') do |fh|
      fh.each_line do |line|
        if line.match(/BadParameterValue.new\([^()]*, :(\w+)\)/)
          tags << "api_bad_#{$1}_parameter_value".to_sym
        end
      end
    end
    assert_no_missing_translations(tags, 'API error')
  end

  def test_name_rank_translations
    tags = Name.all_ranks.map do |rank|
      [
        "rank_#{rank.to_s.downcase}".to_sym,
        "rank_plural_#{rank.to_s.downcase}".to_sym,
      ]
    end.flatten
    assert_no_missing_translations(tags, 'name rank')
  end

  def test_image_vote_translations
    tags = Image.all_votes.map do |val|
      [
        "image_vote_long_#{val}".to_sym,
        "image_vote_short_#{val}".to_sym,
        "image_vote_help_#{val}".to_sym,
      ]
    end.flatten
    assert_no_missing_translations(tags, 'image vote')
  end

  def test_review_status_translations
    tags = NameDescription.all_review_statuses.map do |status|
      "review_#{status}".to_sym
    end
    assert_no_missing_translations(tags, 'review status')
  end

  def test_naming_reason_translations
    tags = Naming::Reason.all_reasons.map do |reason|
      "naming_reason_label_#{reason}".to_sym
    end
    assert_no_missing_translations(tags, 'naming reason')
  end

  def test_description_source_translations
    tags = [:public, :foreign, :project, :source, :user].map do |source|
      [
        "description_full_title_#{source}".to_sym,
        "description_part_title_#{source}_with_text".to_sym,
      ]
    end.flatten
    assert_no_missing_translations(tags, 'description source title')
  end

  def test_site_data_translations
    tags = SiteData::ALL_FIELDS.map do |field|
      [
        "user_stats_#{field}".to_sym,
        "site_stats_#{field}".to_sym,
      ]
    end.flatten - [:user_stats_users] # not picking this up for some reason...
    assert_no_missing_translations(tags, 'site data field')
  end
end
