require 'test-unit'

require 'erb'
require 'yaml'
require 'socket'
require 'rbconfig'
require 'tempfile'

require 'active_ldap'

require File.join(File.expand_path(File.dirname(__FILE__)), "command")

LDAP_ENV = "test" unless defined?(LDAP_ENV)

module AlTestUtils
  def self.included(base)
    base.class_eval do
      include ActiveLdap::GetTextSupport
      include Utilities
      include Config
      include Connection
      include Populate
      include TemporaryEntry
      include CommandSupport
      include MockLogger
    end
  end

  module Utilities
    def dn(string)
      ActiveLdap::DN.parse(string)
    end
  end

  module Config
    def setup
      super
      @base_dir = File.expand_path(File.dirname(__FILE__))
      @top_dir = File.expand_path(File.join(@base_dir, ".."))
      @example_dir = File.join(@top_dir, "examples")
      @fixtures_dir = File.join(@base_dir, "fixtures")
      @config_file = File.join(@base_dir, "config.yaml")
      ActiveLdap::Base.configurations = read_config
    end

    def teardown
      super
    end

    def current_configuration
      ActiveLdap::Base.configurations[LDAP_ENV]
    end

    def read_config
      unless File.exist?(@config_file)
        raise "config file for testing doesn't exist: #{@config_file}"
      end
      erb = ERB.new(File.read(@config_file))
      erb.filename = @config_file
      config = YAML.load(erb.result)
      _adapter = adapter
      config.each do |key, value|
        value["adapter"] = _adapter if _adapter
      end
      config
    end

    def adapter
      ENV["ACTIVE_LDAP_TEST_ADAPTER"]
    end

    def fixture(*components)
      File.join(@fixtures_dir, *components)
    end
  end

  module ExampleFile
    def certificate_path
      File.join(@example_dir, 'example.der')
    end

    @@certificate = nil
    def certificate
      return @@certificate if @@certificate
      if File.exists?(certificate_path)
        @@certificate = read_binary_file(certificate_path)
        return @@certificate
      end

      require 'openssl'
      rsa = OpenSSL::PKey::RSA.new(512)
      comment = "Generated by Ruby/OpenSSL"

      cert = OpenSSL::X509::Certificate.new
      cert.version = 3
      cert.serial = 0
      subject = [["OU", "test"],
                 ["CN", Socket.gethostname]]
      name = OpenSSL::X509::Name.new(subject)
      cert.subject = name
      cert.issuer = name
      cert.not_before = Time.now
      cert.not_after = Time.now + (365*24*60*60)
      cert.public_key = rsa.public_key

      ef = OpenSSL::X509::ExtensionFactory.new(nil, cert)
      ef.issuer_certificate = cert
      cert.extensions = [
        ef.create_extension("basicConstraints","CA:FALSE"),
        ef.create_extension("keyUsage", "keyEncipherment"),
        ef.create_extension("subjectKeyIdentifier", "hash"),
        ef.create_extension("extendedKeyUsage", "serverAuth"),
        ef.create_extension("nsComment", comment),
      ]
      aki = ef.create_extension("authorityKeyIdentifier",
                                "keyid:always,issuer:always")
      cert.add_extension(aki)
      cert.sign(rsa, OpenSSL::Digest::SHA1.new)

      @@certificate = cert.to_der
      @@certificate
    end

    def jpeg_photo_path
      File.join(@example_dir, 'example.jpg')
    end

    def jpeg_photo
      read_binary_file(jpeg_photo_path)
    end

    def read_binary_file(path)
      File.open(path, "rb") do |input|
        input.set_encoding("ascii-8bit") if input.respond_to?(:set_encoding)
        input.read
      end
    end
  end

  module Connection
    def setup
      super
      ActiveLdap::Base.setup_connection
    end

    def teardown
      ActiveLdap::Base.remove_active_connections!
      super
    end
  end

  module Populate
    def setup
      @dumped_data = nil
      super
      begin
        @dumped_data = ActiveLdap::Base.dump(:scope => :sub)
      rescue ActiveLdap::ConnectionError
      end
      ActiveLdap::Base.delete_all(nil, :scope => :sub)
      populate
    end

    def teardown
      if @dumped_data
        ActiveLdap::Base.setup_connection
        ActiveLdap::Base.delete_all(nil, :scope => :sub)
        ActiveLdap::Base.load(@dumped_data)
      end
      super
    end

    def populate
      populate_base
      populate_ou
      populate_user_class
      populate_group_class
      populate_associations
    end

    def populate_base
      ActiveLdap::Populate.ensure_base
    end

    def ou_class(prefix="")
      ou_class = Class.new(ActiveLdap::Base)
      ou_class.ldap_mapping(:dn_attribute => "ou",
                            :prefix => prefix,
                            :classes => ["top", "organizationalUnit"])
      ou_class
    end

    def dc_class(prefix="")
      dc_class = Class.new(ActiveLdap::Base)
      dc_class.ldap_mapping(:dn_attribute => "dc",
                            :prefix => prefix,
                            :classes => ["top", "dcObject", "organization"])
      dc_class
    end

    def entry_class(prefix="")
      entry_class = Class.new(ActiveLdap::Base)
      entry_class.ldap_mapping(:prefix => prefix,
                               :scope => :sub,
                               :classes => ["top"])
      entry_class.dn_attribute = nil
      entry_class
    end

    def populate_ou
      %w(Users Groups).each do |name|
        make_ou(name)
      end
    end

    def make_ou(name)
      ActiveLdap::Populate.ensure_ou(name)
    end

    def make_dc(name)
      ActiveLdap::Populate.ensure_dc(name)
    end

    def populate_user_class
      @user_class = Class.new(ActiveLdap::Base)
      @user_class_classes = ["posixAccount", "person"]
      @user_class.ldap_mapping :dn_attribute => "uid",
                               :prefix => "ou=Users",
                               :scope => :sub,
                               :classes => @user_class_classes
      assign_class_name(@user_class, "User")
    end

    def populate_group_class
      @group_class = Class.new(ActiveLdap::Base)
      @group_class.ldap_mapping :prefix => "ou=Groups",
                                :scope => :sub,
                                :classes => ["posixGroup"]
      assign_class_name(@group_class, "Group")
    end

    def populate_associations
      @user_class.belongs_to :groups, :many => "memberUid"
      @user_class.belongs_to :primary_group,
                             :foreign_key => "gidNumber",
                             :primary_key => "gidNumber"
      @group_class.has_many :members, :wrap => "memberUid"
      @group_class.has_many :primary_members,
                            :foreign_key => "gidNumber",
                            :primary_key => "gidNumber"
      @user_class.set_associated_class(:groups, @group_class)
      @user_class.set_associated_class(:primary_group, @group_class)
      @group_class.set_associated_class(:members, @user_class)
      @group_class.set_associated_class(:primary_members, @user_class)
    end

    def assign_class_name(klass, name)
      singleton_class = class << klass; self; end
      singleton_class.send(:define_method, :name) do
        name
      end
      if Object.const_defined?(klass.name)
        Object.send(:remove_const, klass.name)
      end
      Object.const_set(klass.name, klass)
    end
  end

  module TemporaryEntry
    include ExampleFile

    def setup
      super
      @user_index = 0
      @group_index = 0
    end

    def make_temporary_user(config={})
      @user_index += 1
      uid = config[:uid] || "temp-user#{@user_index}"
      ensure_delete_user(uid) do
        password = config[:password] || "password#{@user_index}"
        uid_number = config[:uid_number] || default_uid
        gid_number = config[:gid_number] || default_gid
        home_directory = config[:home_directory] || "/nonexistent"
        see_also = config[:see_also]
        _wrap_assertion do
          assert(!@user_class.exists?(uid))
          assert_raise(ActiveLdap::EntryNotFound) do
            @user_class.find(uid).dn
          end
          user = @user_class.new(uid)
          assert(user.new_entry?)
          user.cn = user.uid
          user.sn = user.uid
          user.uid_number = uid_number
          user.gid_number = gid_number
          user.home_directory = home_directory
          user.user_password = ActiveLdap::UserPassword.ssha(password)
          user.see_also = see_also
          unless config[:simple]
            user.add_class('shadowAccount', 'inetOrgPerson',
                           'organizationalPerson')
            user.user_certificate = certificate
            user.jpeg_photo = jpeg_photo
          end
          user.save
          assert(!user.new_entry?)
          yield(@user_class.find(user.uid), password)
        end
      end
    end

    def make_temporary_group(config={})
      @group_index += 1
      cn = config[:cn] || "temp-group#{@group_index}"
      ensure_delete_group(cn) do
        gid_number = config[:gid_number] || default_gid
        _wrap_assertion do
          assert(!@group_class.exists?(cn))
          assert_raise(ActiveLdap::EntryNotFound) do
            @group_class.find(cn)
          end
          group = @group_class.new(cn)
          assert(group.new_entry?)
          group.gid_number = gid_number
          assert(group.save)
          assert(!group.new_entry?)
          yield(@group_class.find(group.cn))
        end
      end
    end

    def ensure_delete_user(uid)
      yield(uid)
    ensure
      if @user_class.exists?(uid)
        @user_class.search(:value => uid) do |dn, attribute|
          @user_class.remove_connection(dn)
          @user_class.delete(dn)
        end
      end
    end

    def ensure_delete_group(cn)
      yield(cn)
    ensure
      @group_class.delete(cn) if @group_class.exists?(cn)
    end

    def default_uid
      "10000#{@user_index}"
    end

    def default_gid
      "10000#{@group_index}"
    end
  end

  module CommandSupport
    def setup
      super
      @fakeroot = "fakeroot"
      @ruby = File.join(::RbConfig::CONFIG["bindir"],
                        ::RbConfig::CONFIG["RUBY_INSTALL_NAME"])
      @top_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
      @examples_dir = File.join(@top_dir, "examples")
      @lib_dir = File.join(@top_dir, "lib")
      @ruby_args = [
                    "-I", @examples_dir,
                    "-I", @lib_dir,
                   ]
    end

    def run_command(*args, &block)
      file = Tempfile.new("al-command-support")
      file.open
      file.puts(ActiveLdap::Base.configurations["test"].to_yaml)
      file.close
      run_ruby(*[@command, "--config", file.path, *args], &block)
    end

    def run_ruby(*ruby_args, &block)
      args = [@ruby, *@ruby_args]
      args.concat(ruby_args)
      Command.run(*args, &block)
    end

    def run_ruby_with_fakeroot(*ruby_args, &block)
      args = [@fakeroot, @ruby, *@ruby_args]
      args.concat(ruby_args)
      Command.run(*args, &block)
    end
  end

  module MockLogger
    def make_mock_logger
      logger = Object.new
      class << logger
        def messages(type)
          @messages ||= {}
          @messages[type] ||= []
          @messages[type]
        end

        def info(content=nil)
          messages(:info) << (block_given? ? yield : content)
        end
        def warn(content=nil)
          messages(:warn) << (block_given? ? yield : content)
        end
        def error(content=nil)
          messages(:error) << (block_given? ? yield : content)
        end
      end
      logger
    end

    def with_mock_logger
      original_logger = ActiveLdap::Base.logger
      mock_logger = make_mock_logger
      ActiveLdap::Base.logger = mock_logger
      yield(mock_logger)
    ensure
      ActiveLdap::Base.logger = original_logger
    end
  end
end
