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
      include Omittable
    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")
      current_config_file = File.expand_path("config.yaml")
      test_config_file = File.join(@base_dir, "config.yaml")
      if File.exist?(current_config_file)
        @config_file = current_config_file
      else
        @config_file = test_config_file
      end
      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.exist?(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
      @configurations_on_dump = nil
      super
      begin
        @dumped_data = ActiveLdap::Base.dump(:scope => :sub)
        @configurations_on_dump = ActiveLdap::Base.configurations.dup
      rescue ActiveLdap::ConnectionError
      end
      ActiveLdap::Base.delete_all(nil, :scope => :sub)
      populate
    end

    def teardown
      if @dumped_data
        ActiveLdap::Base.configurations = @configurations_on_dump
        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_group_of_urls_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 GroupOfURLsSet).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_group_of_urls_class
      @group_of_urls_class = Class.new(ActiveLdap::Base)
      @group_of_urls_class.ldap_mapping :prefix => "ou=GroupOfURLsSet",
                                        :scope => :sub,
                                        :classes => ["groupOfURLs"]
      assign_class_name(@group_of_urls_class, "GroupOfURLs")
    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
      @group_of_urls_index = 0
      @temporary_uids = []
    end

    def teardown
      @temporary_uids.each do |uid|
        delete_temporary_user(uid)
      end
      super
    end

    def delete_temporary_user(uid)
      return unless @user_class.exists?(uid)
      @user_class.search(:value => uid) do |dn, attribute|
        @user_class.remove_connection(dn)
        @user_class.delete(dn)
      end
    end

    def build_temporary_user(config={})
      uid = config[:uid] || "temp-user#{@user_index}"
      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]
      user = nil
      _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?)
      end
      [@user_class.find(user.uid), password]
    end

    def make_temporary_user(config={})
      @user_index += 1
      config = config.merge(uid: config[:uid] || "temp-user#{@user_index}")
      uid = config[:uid]
      @temporary_uids << uid
      if block_given?
        ensure_delete_user(uid) do
          yield(*build_temporary_user(config))
        end
      else
        build_temporary_user(config)
      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 make_temporary_group_of_urls(config={})
      @group_of_urls_index += 1
      cn = config[:cn] || "temp-group-of-urls-#{@group_of_urls_index}"
      ensure_delete_group_of_urls(cn) do
        _wrap_assertion do
          assert(!@group_of_urls_class.exists?(cn))
          assert_raise(ActiveLdap::EntryNotFound) do
            @group_of_urls_class.find(cn)
          end
          group_of_urls = @group_of_urls_class.new(cn)
          assert(group_of_urls.new_entry?)
          group_of_urls.member_url = config[:member_url]
          assert(group_of_urls.save!)
          assert(!group_of_urls.new_entry?)
          yield(@group_of_urls_class.find(group_of_urls.cn))
        end
      end
    end

    def ensure_delete_user(uid)
      yield(uid)
    ensure
      delete_temporary_user(uid)
      @temporary_uids.delete(uid)
    end

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

    def ensure_delete_group_of_urls(cn)
      yield(cn)
    ensure
      @group_of_urls_class.delete(cn) if @group_of_urls_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)
      if RUBY_VERSION >= "2.7"
        omit("Need to fix an optional arguments warning in net-ldap: " +
            "ruby-ldap/ruby-net-ldap/pull/342")
      end
      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

  module Omittable
    def omit_if_jruby(message=nil)
      return unless RUBY_PLATFORM == "java"
      omit(message || "This test is not for JRuby")
    end

    def omit_unless_jruby(message=nil)
      return if RUBY_PLATFORM == "java"
      omit(message || "This test is only for JRuby")
    end

    def omit_if_ldap(message=nil)
      return if current_configuration[:adapter] == "ldap"
      omit(message || "This test is not for ruby-ldap")
    end
  end
end
