require "suggest/version"
require "set"

module Suggest
  SUGGEST_MODS = Set.new([
    Array,
    BasicObject,
    Comparable,
    Complex,
    Enumerable,
    FalseClass,
    Float,
    Hash,
    Integer,
    Math,
    NilClass,
    Numeric,
    Range,
    Regexp,
    Regexp,
    Set,
    String,
    Struct,
    Symbol,
    Time,
    TrueClass,
  ])

  UNSAFE_WITH_BLOCK = Set.new([
    [Array, :cycle],
    [Enumerable, :cycle]
  ])

  INCONSISTENT = Set.new([
    [Array, :sample],
    [Array, :shuffle],
    [Array, :shuffle!]
  ])

  TOO_COMPLICATED = Set.new([
    [String, :freeze],
    [Set, :freeze],
    [Set, :taint],
    [Set, :untaint],
    [Numeric, :singleton_method_added],
    [Numeric, :clone],
    [Numeric, :dup],
    [BasicObject, :instance_eval],
    [BasicObject, :instance_exec],
    [BasicObject, :__send__],
    [BasicObject, :singleton_method_added],
    [BasicObject, :singleton_method_removed],
    [BasicObject, :singleton_method_undefined]
  ])

  SELECTOR = ->(m) do
    SUGGEST_MODS.include?(m.owner) &&
      !INCONSISTENT.include?([m.owner, m.name]) &&
      !TOO_COMPLICATED.include?([m.owner, m.name])
  end

  module Mixin
    def what_returns?(expected, args: [], allow_mutation: false, allow_not_public: false, &block)
      methods.map(&method(:method)).select(&SELECTOR).select do |m|
        arity = m.arity
        next unless arity < 0 || arity == args.count

        post = clone

        next if block && UNSAFE_WITH_BLOCK.include?([m.owner, m.name])
        result = post.__send__(allow_not_public ? :send : :public_send, m.name, *args, &block) rescue next

        next unless allow_mutation || self == post

        if expected.is_a?(Proc) && expected.lambda?
          expected.call(result) rescue false
        else
          Suggest.eq?(result, expected)
        end
      end.map(&:name)
    end

    def what_mutates?(expected, args: [], allow_not_public: false, **opts, &block)
      methods.map(&method(:method)).select(&SELECTOR).select do |m|
        arity = m.arity
        next unless arity < 0 || arity == args.count

        post = clone

        next if block && UNSAFE_WITH_BLOCK.include?([m.owner, m.name])
        result = post.__send__(allow_not_public ? :send : :public_send, m.name, *args, &block) rescue next

        next if opts.key?(:returns) && !Suggest.eq?(result, opts[:returns])

        Suggest.eq?(post, expected)
      end.map(&:name)
    end
  end

  def self.eq?(result, expected)
    result.is_a?(expected.class) && result == expected
  end

  def self.suggestable!(mod, **corrections) # unsafe_with_block: [], inconsistent: [], too_complicated: []
    raise ArgumentError.new("Must support smart comparison (implement «#{mod}#==»)") if mod.instance_method(:==).owner == BasicObject

    SUGGEST_MODS << mod
    %w[unsafe_with_block inconsistent too_complicated].each do |correction|
      c = Suggest.const_get(correction.upcase)
      [mod].product(corrections.fetch(correction, [])).each(&c.method(:<<))
    end
    mod.include(Suggest::Mixin) unless mod.ancestors.include?(Suggest::Mixin)
  end

  def self.suggestable_methods
    SUGGEST_MODS.each_with_object([]) do |mod, candidates|
      owned_methods = mod.instance_methods.select { |m| mod.instance_method(m).owner == mod }
      next if owned_methods.none?
      candidates += [mod].product(owned_methods)
    end.reject do |m|
      INCONSISTENT.include?(m) || TOO_COMPLICATED.include?(m)
    end
  end
end

Suggest::SUGGEST_MODS.each do |mod|
  mod.include(Suggest::Mixin) unless mod.ancestors.include?(Suggest::Mixin)
end
