# encoding: UTF-8

require File.join(File.expand_path(File.dirname(__FILE__)), 'test_utils')
require 'fileutils'
require 'pathname'
require 'tmpdir'

include TZInfo

class TCZoneinfoDataSource < Minitest::Test
  ZONEINFO_DIR = File.join(File.expand_path(File.dirname(__FILE__)), 'zoneinfo').untaint
  
  def setup
    @orig_search_path = ZoneinfoDataSource.search_path.clone
    @orig_alternate_iso3166_tab_search_path = ZoneinfoDataSource.alternate_iso3166_tab_search_path.clone
    @orig_pwd = FileUtils.pwd
    
    # A zoneinfo directory containing files needed by the tests.
    # The symlinks in this directory are set up in test_utils.rb.
    @data_source = ZoneinfoDataSource.new(ZONEINFO_DIR)
  end
  
  def teardown
    ZoneinfoDataSource.search_path = @orig_search_path
    ZoneinfoDataSource.alternate_iso3166_tab_search_path = @orig_alternate_iso3166_tab_search_path
    FileUtils.chdir(@orig_pwd)
  end
  
  def test_default_search_path
    assert_equal(['/usr/share/zoneinfo', '/usr/share/lib/zoneinfo', '/etc/zoneinfo'], ZoneinfoDataSource.search_path)
    assert_equal(false, ZoneinfoDataSource.search_path.frozen?)
  end
  
  def test_set_search_path_default
    ZoneinfoDataSource.search_path = ['/tmp/zoneinfo1', '/tmp/zoneinfo2']
    assert_equal(['/tmp/zoneinfo1', '/tmp/zoneinfo2'], ZoneinfoDataSource.search_path)
    
    ZoneinfoDataSource.search_path = nil
    assert_equal(['/usr/share/zoneinfo', '/usr/share/lib/zoneinfo', '/etc/zoneinfo'], ZoneinfoDataSource.search_path)
    assert_equal(false, ZoneinfoDataSource.search_path.frozen?)
  end
  
  def test_set_search_path_array
    path = ['/tmp/zoneinfo1', '/tmp/zoneinfo2']
    ZoneinfoDataSource.search_path = path
    assert_equal(['/tmp/zoneinfo1', '/tmp/zoneinfo2'], ZoneinfoDataSource.search_path)
    refute_same(path, ZoneinfoDataSource.search_path)
  end
  
  def test_set_search_path_array_to_s  
    ZoneinfoDataSource.search_path = [Pathname.new('/tmp/zoneinfo3')]
    assert_equal(['/tmp/zoneinfo3'], ZoneinfoDataSource.search_path)
  end
  
  def test_set_search_path_string
    ZoneinfoDataSource.search_path = ['/tmp/zoneinfo4', '/tmp/zoneinfo5'].join(File::PATH_SEPARATOR)
    assert_equal(['/tmp/zoneinfo4', '/tmp/zoneinfo5'], ZoneinfoDataSource.search_path)
  end
  
  def test_default_alternate_iso3166_tab_search_path
    assert_equal(['/usr/share/misc/iso3166.tab', '/usr/share/misc/iso3166'], ZoneinfoDataSource.alternate_iso3166_tab_search_path)
    assert_equal(false, ZoneinfoDataSource.alternate_iso3166_tab_search_path.frozen?)
  end
  
  def test_set_alternate_iso3166_tab_search_path_default
    ZoneinfoDataSource.alternate_iso3166_tab_search_path = ['/tmp/iso3166.tab', '/tmp/iso3166']
    assert_equal(['/tmp/iso3166.tab', '/tmp/iso3166'], ZoneinfoDataSource.alternate_iso3166_tab_search_path)
    
    ZoneinfoDataSource.alternate_iso3166_tab_search_path = nil
    assert_equal(['/usr/share/misc/iso3166.tab', '/usr/share/misc/iso3166'], ZoneinfoDataSource.alternate_iso3166_tab_search_path)
    assert_equal(false, ZoneinfoDataSource.alternate_iso3166_tab_search_path.frozen?)
  end
  
  def test_set_alternate_iso3166_tab_search_path_array
    path = ['/tmp/iso3166.tab', '/tmp/iso3166']
    ZoneinfoDataSource.alternate_iso3166_tab_search_path = path
    assert_equal(['/tmp/iso3166.tab', '/tmp/iso3166'], ZoneinfoDataSource.alternate_iso3166_tab_search_path)
    refute_same(path, ZoneinfoDataSource.alternate_iso3166_tab_search_path)
  end
  
  def test_set_alternate_iso3166_tab_search_path_array_to_s  
    ZoneinfoDataSource.alternate_iso3166_tab_search_path = [Pathname.new('/tmp/iso3166.tab')]
    assert_equal(['/tmp/iso3166.tab'], ZoneinfoDataSource.alternate_iso3166_tab_search_path)
  end
  
  def test_set_alternate_iso3166_tab_search_path_string
    ZoneinfoDataSource.alternate_iso3166_tab_search_path = ['/tmp/iso3166.tab', '/tmp/iso3166'].join(File::PATH_SEPARATOR)
    assert_equal(['/tmp/iso3166.tab', '/tmp/iso3166'], ZoneinfoDataSource.alternate_iso3166_tab_search_path)
  end
  
  def test_new_search
    Dir.mktmpdir('tzinfo_test_dir1') do |dir1|
      Dir.mktmpdir('tzinfo_test_dir2') do |dir2|
        Dir.mktmpdir('tzinfo_test_dir3') do |dir3|
          Dir.mktmpdir('tzinfo_test_dir4') do |dir4|
            file = File.join(dir1, 'file')
            FileUtils.touch(File.join(dir2, 'zone.tab'))
            FileUtils.touch(File.join(dir3, 'iso3166.tab'))
            FileUtils.touch(File.join(dir4, 'zone.tab'))
            FileUtils.touch(File.join(dir4, 'iso3166.tab'))
            
            ZoneinfoDataSource.search_path = [file, dir2, dir3, dir4]
            ZoneinfoDataSource.alternate_iso3166_tab_search_path = []
            
            data_source = ZoneinfoDataSource.new
            assert_equal(dir4, data_source.zoneinfo_dir)
          end
        end
      end
    end
  end

  def test_new_search_zone1970
    Dir.mktmpdir('tzinfo_test_dir1') do |dir1|
      Dir.mktmpdir('tzinfo_test_dir2') do |dir2|
        Dir.mktmpdir('tzinfo_test_dir3') do |dir3|
          Dir.mktmpdir('tzinfo_test_dir4') do |dir4|
            file = File.join(dir1, 'file')
            FileUtils.touch(File.join(dir2, 'zone1970.tab'))
            FileUtils.touch(File.join(dir3, 'iso3166.tab'))
            FileUtils.touch(File.join(dir4, 'zone1970.tab'))
            FileUtils.touch(File.join(dir4, 'iso3166.tab'))

            ZoneinfoDataSource.search_path = [file, dir2, dir3, dir4]
            ZoneinfoDataSource.alternate_iso3166_tab_search_path = []

            data_source = ZoneinfoDataSource.new
            assert_equal(dir4, data_source.zoneinfo_dir)
          end
        end
      end
    end
  end
  
  def test_new_search_solaris_tab_files
    # Solaris names the tab files 'tab/country.tab' (iso3166.tab) and 
    # 'tab/zone_sun.tab' (zone.tab).
    
    Dir.mktmpdir('tzinfo_test_dir') do |dir|
      tab = File.join(dir, 'tab')
      FileUtils.mkdir(tab)
      FileUtils.touch(File.join(tab, 'country.tab'))
      FileUtils.touch(File.join(tab, 'zone_sun.tab'))

      ZoneinfoDataSource.search_path = [dir]
      ZoneinfoDataSource.alternate_iso3166_tab_search_path = []

      data_source = ZoneinfoDataSource.new
      assert_equal(dir, data_source.zoneinfo_dir)
    end
  end
  
  def test_new_search_alternate_iso3166_path
    Dir.mktmpdir('tzinfo_test_dir_zoneinfo') do |zoneinfo_dir|
      Dir.mktmpdir('tzinfo_test_dir_tab') do |tab_dir|
        FileUtils.touch(File.join(zoneinfo_dir, 'zone.tab'))
        
        tab_file = File.join(tab_dir, 'iso3166')
        
        ZoneinfoDataSource.search_path = [zoneinfo_dir]
        ZoneinfoDataSource.alternate_iso3166_tab_search_path = [tab_file]
        
        assert_raises(ZoneinfoDirectoryNotFound) do
          ZoneinfoDataSource.new
        end
        
        FileUtils.touch(tab_file)
      
        data_source = ZoneinfoDataSource.new
        assert_equal(zoneinfo_dir, data_source.zoneinfo_dir)
      end
    end
  end
  
  def test_new_search_not_found
    Dir.mktmpdir('tzinfo_test_dir1') do |dir1|
      Dir.mktmpdir('tzinfo_test_dir2') do |dir2|
        Dir.mktmpdir('tzinfo_test_dir3') do |dir3|
          Dir.mktmpdir('tzinfo_test_dir4') do |dir4|
            Dir.mktmpdir('tzinfo_test_dir5') do |dir5|
              file = File.join(dir1, 'file')
              FileUtils.touch(file)
              FileUtils.touch(File.join(dir2, 'zone.tab'))
              FileUtils.touch(File.join(dir3, 'zone1970.tab'))
              FileUtils.touch(File.join(dir4, 'iso3166.tab'))

              ZoneinfoDataSource.search_path = [file, dir2, dir3, dir4, dir5]
              ZoneinfoDataSource.alternate_iso3166_tab_search_path = []

              assert_raises(ZoneinfoDirectoryNotFound) do
                ZoneinfoDataSource.new
              end
            end
          end
        end
      end
    end
  end
  
  def test_new_search_relative
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      FileUtils.chdir(dir)
      
      ZoneinfoDataSource.search_path = ['.']
      ZoneinfoDataSource.alternate_iso3166_tab_search_path = []
      data_source = ZoneinfoDataSource.new
      assert_equal(Pathname.new(dir).realpath.to_s, data_source.zoneinfo_dir)
      
      # Change out of the directory to allow it to be deleted on Windows.
      FileUtils.chdir(@orig_pwd)
    end
  end
  
  def test_new_dir
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      data_source = ZoneinfoDataSource.new(dir)
      assert_equal(dir, data_source.zoneinfo_dir)
    end
  end

  def test_new_dir_zone1970
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone1970.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))

      data_source = ZoneinfoDataSource.new(dir)
      assert_equal(dir, data_source.zoneinfo_dir)
    end
  end
  
  def test_new_dir_solaris_tab_files
    # Solaris names the tab files 'tab/country.tab' (iso3166.tab) and 
    # 'tab/zone_sun.tab' (zone.tab).
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      tab = File.join(dir, 'tab')
      FileUtils.mkdir(tab)
      FileUtils.touch(File.join(tab, 'country.tab'))
      FileUtils.touch(File.join(tab, 'zone_sun.tab'))
            
      data_source = ZoneinfoDataSource.new(dir)
      assert_equal(dir, data_source.zoneinfo_dir)
    end
  end
  
  def test_new_dir_alternate_iso3166_path
    Dir.mktmpdir('tzinfo_test_dir_zoneinfo') do |zoneinfo_dir|
      Dir.mktmpdir('tzinfo_test_dir_tab') do |tab_dir|
        FileUtils.touch(File.join(zoneinfo_dir, 'zone.tab'))
        
        tab_file = File.join(tab_dir, 'iso3166')
        FileUtils.touch(tab_file)
        
        ZoneinfoDataSource.alternate_iso3166_tab_search_path = [tab_file]
        
        assert_raises(InvalidZoneinfoDirectory) do
          # The alternate_iso3166_tab_search_path should not be used. This should raise 
          # an exception.
          ZoneinfoDataSource.new(zoneinfo_dir)
        end
        
        data_source = ZoneinfoDataSource.new(zoneinfo_dir, tab_file)
        assert_equal(zoneinfo_dir, data_source.zoneinfo_dir)
      end
    end
  end
  
  def test_new_dir_invalid
    Dir.mktmpdir('tzinfo_test') do |dir|
      assert_raises(InvalidZoneinfoDirectory) do
        ZoneinfoDataSource.new(dir)
      end
    end
  end
  
  def test_new_dir_invalid_alternate_iso3166_path
    Dir.mktmpdir('tzinfo_test_dir_zoneinfo') do |zoneinfo_dir|
      Dir.mktmpdir('tzinfo_test_dir_tab') do |tab_dir|
        FileUtils.touch(File.join(zoneinfo_dir, 'zone.tab'))
        
        assert_raises(InvalidZoneinfoDirectory) do
          ZoneinfoDataSource.new(zoneinfo_dir, File.join(tab_dir, 'iso3166'))
        end
      end
    end
  end
  
  def test_new_dir_invalid_alternate_iso3166_path_overrides_valid
    Dir.mktmpdir('tzinfo_test_dir_zoneinfo') do |zoneinfo_dir|
      Dir.mktmpdir('tzinfo_test_dir_tab') do |tab_dir|
        FileUtils.touch(File.join(zoneinfo_dir, 'iso3166.tab'))
        FileUtils.touch(File.join(zoneinfo_dir, 'zone.tab'))
        
        assert_raises(InvalidZoneinfoDirectory) do
          ZoneinfoDataSource.new(zoneinfo_dir, File.join(tab_dir, 'iso3166'))
        end
      end
    end
  end
  
  def test_new_file
    Dir.mktmpdir('tzinfo_test') do |dir|
      file = File.join(dir, 'file')
      FileUtils.touch(file)
      
      assert_raises(InvalidZoneinfoDirectory) do
        ZoneinfoDataSource.new(file)
      end
    end
  end

  def test_new_dir_relative
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      FileUtils.chdir(dir)
      
      data_source = ZoneinfoDataSource.new('.')
      assert_equal(Pathname.new(dir).realpath.to_s, data_source.zoneinfo_dir)
      
      # Change out of the directory to allow it to be deleted on Windows.
      FileUtils.chdir(@orig_pwd)
    end
  end
  
  def test_zoneinfo_dir
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      data_source = ZoneinfoDataSource.new(dir)
      assert_equal(dir, data_source.zoneinfo_dir)
      assert_equal(true, data_source.zoneinfo_dir.frozen?)
    end
  end
  
  def test_load_timezone_info_data
    info = @data_source.load_timezone_info('Europe/London')
    assert_kind_of(ZoneinfoTimezoneInfo, info)
    assert_equal('Europe/London', info.identifier)
  end
  
  def test_load_timezone_info_linked
    info = @data_source.load_timezone_info('UTC')
    
    # On platforms that don't support symlinks, 'UTC' will be created as a copy.
    # Either way, a ZoneinfoTimezoneInfo should be returned.
    
    assert_kind_of(ZoneinfoTimezoneInfo, info)
    assert_equal('UTC', info.identifier)
  end
  
  def test_load_timezone_info_does_not_exist
    assert_raises(InvalidTimezoneIdentifier) do
      @data_source.load_timezone_info('Nowhere/Special')
    end
  end
  
  def test_load_timezone_info_invalid
    assert_raises(InvalidTimezoneIdentifier) do
      @data_source.load_timezone_info('../Definitions/Europe/London')
    end
  end
  
  def test_load_timezone_info_ignored_file
    assert_raises(InvalidTimezoneIdentifier) do
      @data_source.load_timezone_info('localtime')
    end
  end
  
  def test_load_timezone_info_ignored_plus_version_file
    # Mac OS X includes a file named +VERSION containing the tzdata version.
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      File.open(File.join(dir, '+VERSION'), 'w') do |f|
        f.binmode
        f.write("2013a\n")
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      assert_raises(InvalidTimezoneIdentifier) do
        data_source.load_timezone_info('+VERSION')
      end
    end
  end
  
  def test_load_timezone_info_nil
    assert_raises(InvalidTimezoneIdentifier) do
      @data_source.load_timezone_info(nil)
    end
  end
  
  def test_load_timezone_info_case
    assert_raises(InvalidTimezoneIdentifier) do
      @data_source.load_timezone_info('europe/london')
    end
  end

  def test_load_timezone_info_permission_denied
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      file = File.join(dir, 'UTC')
      FileUtils.touch(file)
      FileUtils.chmod(0200, file)      
      
      data_source = ZoneinfoDataSource.new(dir)
      
      assert_raises(InvalidTimezoneIdentifier) do
        data_source.load_timezone_info('UTC')
      end
    end
  end
  
  def test_load_timezone_info_directory
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      subdir = File.join(dir, 'Subdir')
      FileUtils.mkdir(subdir)     
      
      data_source = ZoneinfoDataSource.new(dir)
      
      assert_raises(InvalidTimezoneIdentifier) do
        data_source.load_timezone_info('Subdir')
      end
    end
  end
  
  def test_load_timezone_info_linked_absolute_outside
    Dir.mktmpdir('tzinfo_test') do |dir|
      Dir.mktmpdir('tzinfo_test') do |outside|
        outside_file = File.join(outside, 'EST')
        FileUtils.cp(File.join(@data_source.zoneinfo_dir, 'EST'), outside_file)
        
        FileUtils.touch(File.join(dir, 'zone.tab'))
        FileUtils.touch(File.join(dir, 'iso3166.tab'))
        
        file = File.join(dir, 'EST')
        
        begin
          FileUtils.ln_s(outside_file, file)
        rescue NotImplementedError
          # Symlinks not supported on this platform - skip test
          return
        end
        
        data_source = ZoneinfoDataSource.new(dir)
        
        info = data_source.load_timezone_info('EST')
        assert_kind_of(ZoneinfoTimezoneInfo, info)
        assert_equal('EST', info.identifier)
      end
    end
  end
  
  def test_load_timezone_info_linked_absolute_inside
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      FileUtils.cp(File.join(@data_source.zoneinfo_dir, 'EST'), File.join(dir, 'EST'))
      
      link = File.join(dir, 'Link')
      
      begin  
        FileUtils.ln_s(File.join(File.expand_path(dir), 'EST'), link)      
      rescue NotImplementedError
        # Symlinks not supported on this platform - skip test
        return
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      info = data_source.load_timezone_info('Link')
      assert_kind_of(ZoneinfoTimezoneInfo, info)
      assert_equal('Link', info.identifier)
    end
  end

  def test_load_timezone_info_linked_relative_outside
    Dir.mktmpdir('tzinfo_test') do |root|
      FileUtils.cp(File.join(@data_source.zoneinfo_dir, 'EST'), File.join(root, 'outside'))
      
      dir = File.join(root, 'zoneinfo')
      FileUtils.mkdir(dir)
      
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      link = File.join(dir, 'Link')      
      
      begin
        FileUtils.ln_s('../outside', link)
      rescue NotImplementedError
        # Symlinks not supported on this platform - skip test
        return
      end
      
      subdir = File.join(dir, 'Subdir')
      subdir_link = File.join(subdir, 'Link')
      FileUtils.mkdir(subdir)
      FileUtils.ln_s('../../outside', subdir_link)
      
      data_source = ZoneinfoDataSource.new(dir)
      
      info = data_source.load_timezone_info('Link')
      assert_kind_of(ZoneinfoTimezoneInfo, info)
      assert_equal('Link', info.identifier)
      
      info = data_source.load_timezone_info('Subdir/Link')
      assert_kind_of(ZoneinfoTimezoneInfo, info)
      assert_equal('Subdir/Link', info.identifier)
    end
  end
  
  def test_load_timezone_info_linked_relative_parent_inside
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      
      FileUtils.cp(File.join(@data_source.zoneinfo_dir, 'EST'), File.join(dir, 'EST'))
       
      subdir = File.join(dir, 'Subdir')
      FileUtils.mkdir(subdir)
      FileUtils.cp(File.join(@data_source.zoneinfo_dir, 'EST'), File.join(subdir, 'EST'))
      
      subdir_link = File.join(subdir, 'Link')
      begin
        FileUtils.ln_s('../Subdir/EST', subdir_link)
      rescue NotImplementedError
        # Symlinks not supported on this platform - skip test
        return
      end
      
      subdir_link2 = File.join(subdir, 'Link2')
      FileUtils.ln_s('../EST', subdir_link2)
      
      subdir2 = File.join(dir, 'Subdir2')
      FileUtils.mkdir(subdir2)
      subdir2_link = File.join(subdir2, 'Link')
      FileUtils.ln_s('../Subdir/EST', subdir2_link)
      
      data_source = ZoneinfoDataSource.new(dir)
      
      info = data_source.load_timezone_info('Subdir/Link')
      assert_kind_of(ZoneinfoTimezoneInfo, info)
      assert_equal('Subdir/Link', info.identifier)
      
      info = data_source.load_timezone_info('Subdir/Link2')
      assert_kind_of(ZoneinfoTimezoneInfo, info)
      assert_equal('Subdir/Link2', info.identifier)
      
      info = data_source.load_timezone_info('Subdir2/Link')
      assert_kind_of(ZoneinfoTimezoneInfo, info)
      assert_equal('Subdir2/Link', info.identifier)
    end
  end
  
  def test_load_timezone_info_invalid_file
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
  
      File.open(File.join(dir, 'Zone'), 'wb') do |file|
        file.write('NotAValidTZifFile')        
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      assert_raises(InvalidTimezoneIdentifier) do
        data_source.load_timezone_info('Zone')
      end      
    end
  end
    
  def test_load_timezone_info_invalid_file_2
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))

      zone = File.join(dir, 'Zone')      
      
      File.open(File.join(@data_source.zoneinfo_dir, 'EST')) do |src|
        # Change header to TZif1 (which is not a valid header).
        File.open(zone, 'wb') do |dest|
          dest.write('TZif1')
          src.pos = 5
          FileUtils.copy_stream(src, dest)
        end
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      assert_raises(InvalidTimezoneIdentifier) do
        data_source.load_timezone_info('Zone')
      end      
    end
  end

  def test_load_timezone_info_tainted
    safe_test do
      identifier = 'Europe/Amsterdam'.taint
      assert(identifier.tainted?)
      info = @data_source.load_timezone_info(identifier)
      assert_equal('Europe/Amsterdam', info.identifier)
      assert(identifier.tainted?)
    end
  end
  
  def test_load_timezone_info_tainted_and_frozen
    safe_test do
      info = @data_source.load_timezone_info('Europe/Amsterdam'.taint.freeze)
      assert_equal('Europe/Amsterdam', info.identifier)
    end
  end
  
  def test_load_timezone_info_tainted_zoneinfo_dir_safe_mode
    safe_test(:unavailable => :skip) do
      assert_raises(SecurityError) do
        ZoneinfoDataSource.new(@data_source.zoneinfo_dir.dup.taint)
      end
    end
  end
  
  def test_load_timezone_info_tainted_zoneinfo_dir
    data_source = ZoneinfoDataSource.new(@data_source.zoneinfo_dir.dup.taint)
    info = data_source.load_timezone_info('Europe/London')
    assert_kind_of(ZoneinfoTimezoneInfo, info)
    assert_equal('Europe/London', info.identifier)
  end
  
  def get_timezone_filenames(directory)
    entries = Dir.glob(File.join(directory, '**', '*'))
    
    entries = entries.select do |file|
      file.untaint
      File.file?(file)
    end
       
    entries = entries.collect {|file| file[directory.length + File::SEPARATOR.length, file.length - directory.length - File::SEPARATOR.length]}

    # Exclude right (with leapseconds) and posix (copy) directories; .tab files; localtime, posixrules and Factory zones
    entries = entries.select do |file| 
      file !~ /\A(posix|right)\// &&
        file !~ /\.tab\z/ &&
        !%w(localtime posixrules Factory).include?(file)
    end
    
    entries.sort
  end

  def test_timezone_identifiers
    expected = get_timezone_filenames(@data_source.zoneinfo_dir)
    all = @data_source.timezone_identifiers
    assert_kind_of(Array, all)
    assert_array_same_items(expected, all)
    assert_equal(true, all.frozen?)
  end
  
  def test_data_timezone_identifiers
    expected = get_timezone_filenames(@data_source.zoneinfo_dir)
    all_data = @data_source.data_timezone_identifiers
    assert_kind_of(Array, all_data)
    assert_array_same_items(expected, all_data)
    assert_equal(true, all_data.frozen?)
  end
  
  def test_linked_timezone_identifiers
    all_linked = @data_source.linked_timezone_identifiers
    assert_kind_of(Array, all_linked)
    assert_equal(true, all_linked.empty?)
    assert_equal(true, all_linked.frozen?)
  end
  
  def test_timezone_identifiers_safe_mode
    safe_test do
      expected = get_timezone_filenames(@data_source.zoneinfo_dir)
      all = @data_source.timezone_identifiers
      assert_kind_of(Array, all)
      assert_array_same_items(expected, all)
      assert_equal(true, all.frozen?)
    end
  end
  
  def test_timezone_identifiers_ignored_plus_version_file
    # Mac OS X includes a file named +VERSION containing the tzdata version.
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      FileUtils.cp(File.join(@data_source.zoneinfo_dir, 'EST'), File.join(dir, 'EST'))
      
      File.open(File.join(dir, '+VERSION'), 'w') do |f|
        f.binmode
        f.write("2013a\n")
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      assert_array_same_items(['EST'], data_source.timezone_identifiers)
    end
  end
  
  def test_timezone_identifiers_ignored_src_directory
    # Solaris includes a src directory containing the source timezone data files
    # from the tzdata distribution. These should be ignored.
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      FileUtils.touch(File.join(dir, 'zone.tab'))
      FileUtils.touch(File.join(dir, 'iso3166.tab'))
      FileUtils.cp(File.join(@data_source.zoneinfo_dir, 'EST'), File.join(dir, 'EST'))
      
      src_dir = File.join(dir, 'src')
      FileUtils.mkdir(src_dir)
      
      File.open(File.join(src_dir, 'europe'), 'w') do |f|
        f.binmode
        f.write("Zone\tEurope/London\t0:00\tEU\tGMT/BST\n")
      end      
      
      data_source = ZoneinfoDataSource.new(dir)
      assert_array_same_items(['EST'], data_source.timezone_identifiers)
    end
  end
  
  def test_load_country_info
    info = @data_source.load_country_info('GB')
    assert_equal('GB', info.code)
    assert_equal('Britain (UK)', info.name)
  end
    
  def test_load_country_info_not_exist
    assert_raises(InvalidCountryCode) do
      @data_source.load_country_info('ZZ')
    end
  end
  
  def test_load_country_info_invalid
    assert_raises(InvalidCountryCode) do
      @data_source.load_country_info('../Countries/GB')
    end
  end
  
  def test_load_country_info_nil
    assert_raises(InvalidCountryCode) do
      @data_source.load_country_info(nil)
    end
  end
  
  def test_load_country_info_case
    assert_raises(InvalidCountryCode) do
      @data_source.load_country_info('gb')
    end
  end
  
  def test_load_country_info_tainted
    safe_test do
      code = 'NL'.taint
      assert(code.tainted?)
      info = @data_source.load_country_info(code)
      assert_equal('NL', info.code)
      assert(code.tainted?)
    end
  end
  
  def test_load_country_info_tainted_and_frozen
    safe_test do
      info = @data_source.load_country_info('NL'.taint.freeze)
      assert_equal('NL', info.code)
    end
  end
  
  def test_load_country_info_check_zones
    Dir.mktmpdir('tzinfo_test') do |dir|
      RubyCoreSupport.open_file(File.join(dir, 'iso3166.tab'), 'w', :external_encoding => 'UTF-8') do |iso3166|      
        iso3166.puts('# iso3166.tab')
        iso3166.puts('')
        iso3166.puts("FC\tFake Country")
        iso3166.puts("OC\tOther Country")
      end
      
      RubyCoreSupport.open_file(File.join(dir, 'zone.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts('# zone.tab')
        zone.puts('')
        zone.puts("FC\t+513030-0000731\tFake/One\tDescription of one")
        zone.puts("FC\t+353916+1394441\tFake/Two\tAnother description")
        zone.puts("FC\t-2332-04637\tFake/Three\tThis is Three")
        zone.puts("OC\t+5005+01426\tOther/One")
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      info = data_source.load_country_info('FC')
      assert_equal('FC', info.code)
      assert_equal('Fake Country', info.name)
      assert_equal(['Fake/One', 'Fake/Two', 'Fake/Three'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([
        CountryTimezone.new('Fake/One', Rational(6181, 120), Rational(-451, 3600), 'Description of one'),
        CountryTimezone.new('Fake/Two', Rational(32089, 900), Rational(503081, 3600), 'Another description'),
        CountryTimezone.new('Fake/Three', Rational(-353, 15), Rational(-2797, 60), 'This is Three')], info.zones)
      assert_equal(true, info.zones.frozen?)
      
      info = data_source.load_country_info('OC')
      assert_equal('OC', info.code)
      assert_equal('Other Country', info.name)
      assert_equal(['Other/One'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([CountryTimezone.new('Other/One', Rational(601, 12), Rational(433, 30))], info.zones)
      assert_equal(true, info.zones.frozen?)
    end
  end

  def test_load_country_info_check_zones_zone1970
    Dir.mktmpdir('tzinfo_test') do |dir|
      RubyCoreSupport.open_file(File.join(dir, 'iso3166.tab'), 'w', :external_encoding => 'UTF-8') do |iso3166|
        iso3166.puts('# iso3166.tab')
        iso3166.puts('')
        iso3166.puts("AC\tAnother Country")
        iso3166.puts("FC\tFake Country")
        iso3166.puts("OC\tOther Country")
      end

      # zone.tab will be ignored.
      RubyCoreSupport.open_file(File.join(dir, 'zone.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts('# zone.tab')
        zone.puts('')
        zone.puts("FC\t+513030-0000731\tFake/One\tDescription of one")
        zone.puts("FC\t+353916+1394441\tFake/Two\tAnother description")
        zone.puts("FC\t-2332-04637\tFake/Three\tThis is Three")
        zone.puts("OC\t+5005+01426\tOther/One")
      end

      # zone1970.tab will be used.
      RubyCoreSupport.open_file(File.join(dir, 'zone1970.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts('# zone1970.tab')
        zone.puts('')
        zone.puts("AC,OC\t+0000+00000\tMiddle/Another/One\tAnother's One")
        zone.puts("FC\t+513030-0000731\tFake/One\tDescription of one")
        zone.puts("FC,OC\t+353916+1394441\tFake/Two\tAnother description")
        zone.puts("FC,OC\t-2332-04637\tFake/Three\tThis is Three")
        zone.puts("OC\t+5005+01426\tOther/One")
        zone.puts("OC\t+5015+11426\tOther/Two")
      end

      data_source = ZoneinfoDataSource.new(dir)

      info = data_source.load_country_info('AC')
      assert_equal('AC', info.code)
      assert_equal('Another Country', info.name)
      assert_equal(['Middle/Another/One'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([CountryTimezone.new('Middle/Another/One', Rational(0, 1), Rational(0, 1), "Another's One")], info.zones)
      assert_equal(true, info.zones.frozen?)

      info = data_source.load_country_info('FC')
      assert_equal('FC', info.code)
      assert_equal('Fake Country', info.name)
      assert_equal(['Fake/One', 'Fake/Two', 'Fake/Three'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([
        CountryTimezone.new('Fake/One', Rational(6181, 120), Rational(-451, 3600), 'Description of one'),
        CountryTimezone.new('Fake/Two', Rational(32089, 900), Rational(503081, 3600), 'Another description'),
        CountryTimezone.new('Fake/Three', Rational(-353, 15), Rational(-2797, 60), 'This is Three')], info.zones)
      assert_equal(true, info.zones.frozen?)

      # Testing the ordering of zones. A zone can either be primary (country
      # code is the first in the first column), or secondary (country code is
      # not the first). Should return all the primaries first in the order they
      # appeared in the file, followed by all the secondaries in the order they
      # appeared in file.

      info = data_source.load_country_info('OC')
      assert_equal('OC', info.code)
      assert_equal('Other Country', info.name)
      assert_equal(['Other/One', 'Other/Two', 'Middle/Another/One', 'Fake/Two', 'Fake/Three'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([
        CountryTimezone.new('Other/One', Rational(601, 12), Rational( 433, 30)),
        CountryTimezone.new('Other/Two', Rational(201,  4), Rational(3433, 30)),
        CountryTimezone.new('Middle/Another/One', Rational(0, 1), Rational(0, 1), "Another's One"),
        CountryTimezone.new('Fake/Two', Rational(32089, 900), Rational(503081, 3600), 'Another description'),
        CountryTimezone.new('Fake/Three', Rational(-353, 15), Rational(-2797, 60), 'This is Three')], info.zones)
      assert_equal(true, info.zones.frozen?)
    end
  end
  
  def test_load_country_info_check_zones_solaris_tab_files
    # Solaris uses 5 columns instead of the usual 4 in zone_sun.tab.
    # An extra column before the comment gives an optional linked/alternate
    # timezone identifier (or '-' if not set).
    #
    # Additionally, there is a section at the end of the file for timezones
    # covering regions. These are given lower-case "country" codes. The timezone
    # identifier column refers to a continent instead of an identifier. These
    # lines will be ignored by TZInfo.
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      tab_dir = File.join(dir, 'tab')
      FileUtils.mkdir(tab_dir)
    
      RubyCoreSupport.open_file(File.join(tab_dir, 'country.tab'), 'w', :external_encoding => 'UTF-8') do |country|
        country.puts('# country.tab')
        country.puts('# Solaris')
        country.puts("FC\tFake Country")
        country.puts("OC\tOther Country")
      end
      
      RubyCoreSupport.open_file(File.join(tab_dir, 'zone_sun.tab'), 'w', :external_encoding => 'UTF-8') do |zone_sun|
        zone_sun.puts('# zone_sun.tab')
        zone_sun.puts('# Solaris')
        zone_sun.puts('# Countries')
        zone_sun.puts("FC\t+513030-0000731\tFake/One\t-\tDescription of one")
        zone_sun.puts("FC\t+353916+1394441\tFake/Two\tFake/Alias/Two\tAnother description")
        zone_sun.puts("FC\t-2332-04637\tFake/Three\tFake/Alias/Three\tThis is Three")
        zone_sun.puts("OC\t+5005+01426\tOther/One\tOther/Linked/One")
        zone_sun.puts("OC\t+5015+01436\tOther/Two\t-")
        zone_sun.puts('# Regions')
        zone_sun.puts("ee\t+0000+00000\tEurope/\tEET")
        zone_sun.puts("me\t+0000+00000\tEurope/\tMET")
        zone_sun.puts("we\t+0000+00000\tEurope/\tWET")
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      info = data_source.load_country_info('FC')
      assert_equal('FC', info.code)
      assert_equal('Fake Country', info.name)
      assert_equal(['Fake/One', 'Fake/Two', 'Fake/Three'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([
        CountryTimezone.new('Fake/One', Rational(6181, 120), Rational(-451, 3600), 'Description of one'),
        CountryTimezone.new('Fake/Two', Rational(32089, 900), Rational(503081, 3600), 'Another description'),
        CountryTimezone.new('Fake/Three', Rational(-353, 15), Rational(-2797, 60), 'This is Three')], info.zones)
      assert_equal(true, info.zones.frozen?)
      
      info = data_source.load_country_info('OC')
      assert_equal('OC', info.code)
      assert_equal('Other Country', info.name)
      assert_equal(['Other/One', 'Other/Two'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([
        CountryTimezone.new('Other/One', Rational(601, 12), Rational(433, 30)),
        CountryTimezone.new('Other/Two', Rational(201, 4), Rational(73, 5))], info.zones)
      assert_equal(true, info.zones.frozen?)
    end
  end
  
  def test_load_country_info_check_zones_alternate_iso3166_file
    Dir.mktmpdir('tzinfo_test') do |dir|
      zoneinfo_dir = File.join(dir, 'zoneinfo')
      tab_dir = File.join(dir, 'tab')
      FileUtils.mkdir(zoneinfo_dir)
      FileUtils.mkdir(tab_dir)
      
      tab_file = File.join(tab_dir, 'iso3166')
      RubyCoreSupport.open_file(tab_file, 'w', :external_encoding => 'UTF-8') do |iso3166|
        # Use the BSD 4 column format (alternate iso3166 is used on BSD).
        iso3166.puts("FC\tFCC\t001\tFake Country")
        iso3166.puts("OC\tOCC\t002\tOther Country")
      end
      
      RubyCoreSupport.open_file(File.join(zoneinfo_dir, 'zone.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts("FC\t+513030-0000731\tFake/One\tDescription of one")
        zone.puts("FC\t+353916+1394441\tFake/Two\tAnother description")
        zone.puts("FC\t-2332-04637\tFake/Three\tThis is Three")
        zone.puts("OC\t+5005+01426\tOther/One")
      end
      
      data_source = ZoneinfoDataSource.new(zoneinfo_dir, tab_file)
      
      info = data_source.load_country_info('FC')
      assert_equal('FC', info.code)
      assert_equal('Fake Country', info.name)
      assert_equal(['Fake/One', 'Fake/Two', 'Fake/Three'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([
        CountryTimezone.new('Fake/One', Rational(6181, 120), Rational(-451, 3600), 'Description of one'),
        CountryTimezone.new('Fake/Two', Rational(32089, 900), Rational(503081, 3600), 'Another description'),
        CountryTimezone.new('Fake/Three', Rational(-353, 15), Rational(-2797, 60), 'This is Three')], info.zones)
      assert_equal(true, info.zones.frozen?)
      
      info = data_source.load_country_info('OC')
      assert_equal('OC', info.code)
      assert_equal('Other Country', info.name)
      assert_equal(['Other/One'], info.zone_identifiers)
      assert_equal(true, info.zone_identifiers.frozen?)
      assert_equal([CountryTimezone.new('Other/One', Rational(601, 12), Rational(433, 30))], info.zones)
      assert_equal(true, info.zones.frozen?)
    end
  end
  
  def test_load_country_info_four_column_iso31611
    # OpenBSD and FreeBSD use a 4 column iso3166.tab file that includes
    # alpha-3 and numeric-3 codes in addition to the alpha-2 and name in the
    # tz database version.
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      RubyCoreSupport.open_file(File.join(dir, 'iso3166.tab'), 'w', :external_encoding => 'UTF-8') do |iso3166|
        iso3166.puts("FC\tFCC\t001\tFake Country")
        iso3166.puts("OC\tOCC\t002\tOther Country")
      end
      
      RubyCoreSupport.open_file(File.join(dir, 'zone.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts("FC\t+513030-0000731\tFake/One\tDescription of one")
        zone.puts("OC\t+5005+01426\tOther/One")
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      info = data_source.load_country_info('FC')
      assert_equal('FC', info.code)
      assert_equal('Fake Country', info.name)
      
      info = data_source.load_country_info('OC')
      assert_equal('OC', info.code)
      assert_equal('Other Country', info.name)
    end
  end

  def test_load_country_info_utf8
    # iso3166.tab is currently in ASCII (as of tzdata 2014f), but will be
    # changed to UTF-8 in the future.

    # zone.tab is in ASCII, with no plans to change. Since ASCII is a subset of
    # UTF-8, test that this is loaded in UTF-8 anyway.
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      RubyCoreSupport.open_file(File.join(dir, 'iso3166.tab'), 'w', :external_encoding => 'UTF-8') do |iso3166|
        iso3166.puts("UT\tUnicode Test ✓")
      end
      
      RubyCoreSupport.open_file(File.join(dir, 'zone.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts("UT\t+513030-0000731\tUnicode✓/One\tUnicode Description ✓")
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      info = data_source.load_country_info('UT')
      assert_equal('UT', info.code)
      assert_equal('Unicode Test ✓', info.name)
      assert_equal(['Unicode✓/One'], info.zone_identifiers)
      assert_equal([CountryTimezone.new('Unicode✓/One', Rational(6181, 120), Rational(-451, 3600), 'Unicode Description ✓')], info.zones)
    end
  end

  def test_load_country_info_utf8_zone1970
    # iso3166.tab is currently in ASCII (as of tzdata 2014f), but will be
    # changed to UTF-8 in the future.

    # zone1970.tab is in UTF-8.

    Dir.mktmpdir('tzinfo_test') do |dir|
      RubyCoreSupport.open_file(File.join(dir, 'iso3166.tab'), 'w', :external_encoding => 'UTF-8') do |iso3166|
        iso3166.puts("UT\tUnicode Test ✓")
      end

      RubyCoreSupport.open_file(File.join(dir, 'zone1970.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts("UT\t+513030-0000731\tUnicode✓/One\tUnicode Description ✓")
      end

      data_source = ZoneinfoDataSource.new(dir)

      info = data_source.load_country_info('UT')
      assert_equal('UT', info.code)
      assert_equal('Unicode Test ✓', info.name)
      assert_equal(['Unicode✓/One'], info.zone_identifiers)
      assert_equal([CountryTimezone.new('Unicode✓/One', Rational(6181, 120), Rational(-451, 3600), 'Unicode Description ✓')], info.zones)
    end
  end
  
  def test_country_codes
    file_codes = []
        
    RubyCoreSupport.open_file(File.join(@data_source.zoneinfo_dir, 'iso3166.tab'), 'r', :external_encoding => 'UTF-8', :internal_encoding => 'UTF-8') do |file|
      file.each_line do |line|
        line.chomp!
        file_codes << $1 if line =~ /\A([A-Z]{2})\t/
      end
    end
    
    codes = @data_source.country_codes
    assert_array_same_items(file_codes, codes)
    assert_equal(true, codes.frozen?)
  end
  
  def test_country_codes_four_column_iso3166
    # OpenBSD and FreeBSD use a 4 column iso3166.tab file that includes
    # alpha-3 and numeric-3 codes in addition to the alpha-2 and name in the
    # tz database version.
    
    Dir.mktmpdir('tzinfo_test') do |dir|
      RubyCoreSupport.open_file(File.join(dir, 'iso3166.tab'), 'w', :external_encoding => 'UTF-8') do |iso3166|
        iso3166.puts("FC\tFCC\t001\tFake Country")
        iso3166.puts("OC\tOCC\t002\tOther Country")
      end
      
      RubyCoreSupport.open_file(File.join(dir, 'zone.tab'), 'w', :external_encoding => 'UTF-8') do |zone|
        zone.puts("FC\t+513030-0000731\tFake/One\tDescription of one")
        zone.puts("OC\t+5005+01426\tOther/One")
      end
      
      data_source = ZoneinfoDataSource.new(dir)
      
      codes = data_source.country_codes
      assert_array_same_items(%w(FC OC), codes)
    end
  end
end
