module Mocktail
  class DeclaresDryClass
    def initialize
      @handles_dry_call = HandlesDryCall.new
    end

    def declare(type)
      type_type = type_of(type)
      instance_methods = instance_methods_on(type)
      dry_class = Class.new(Object) {
        include type if type_type == :module

        def initialize(*args, **kwargs, &blk)
        end

        define_method :is_a?, ->(thing) {
          type.ancestors.include?(thing)
        }
        alias_method :kind_of?, :is_a?

        if type_type == :class
          define_method :instance_of?, ->(thing) {
            type == thing
          }
        end
      }

      add_stringify_methods!(dry_class, :to_s, type, type_type, instance_methods)
      add_stringify_methods!(dry_class, :inspect, type, type_type, instance_methods)

      define_double_methods!(dry_class, type, instance_methods)

      dry_class
    end

    private

    def define_double_methods!(dry_class, type, instance_methods)
      handles_dry_call = @handles_dry_call
      instance_methods.each do |method|
        dry_class.define_method method, ->(*args, **kwargs, &block) {
          handles_dry_call.handle(Call.new(
            singleton: false,
            double: self,
            original_type: type,
            dry_type: self.class,
            method: method,
            original_method: type.instance_method(method),
            args: args,
            kwargs: kwargs,
            block: block
          ))
        }
      end
    end

    def add_stringify_methods!(dry_class, method_name, type, type_type, instance_methods)
      dry_class.define_singleton_method method_name, -> {
        if (id_matches = super().match(/:([0-9a-fx]+)>$/))
          "#<Class #{"including module " if type.instance_of?(Module)}for mocktail of #{type.name}:#{id_matches[1]}>"
        else
          super()
        end
      }

      unless instance_methods.include?(method_name)
        dry_class.define_method method_name, -> {
          if (id_matches = super().match(/:([0-9a-fx]+)>$/))
            "#<Mocktail of #{type.name}:#{id_matches[1]}>"
          else
            super()
          end
        }
      end
    end

    def type_of(type)
      if type.is_a?(Class)
        :class
      elsif type.is_a?(Module)
        :module
      end
    end

    def instance_methods_on(type)
      type.instance_methods.reject { |m|
        ignored_ancestors.include?(type.instance_method(m).owner)
      }
    end

    def ignored_ancestors
      Object.ancestors
    end
  end
end