require_relative "share/stringifies_method_name"
require_relative "share/stringifies_call"

module Mocktail
  class ExplainsThing
    def initialize
      @stringifies_method_name = StringifiesMethodName.new
      @stringifies_call = StringifiesCall.new
    end

    def explain(thing)
      if (double = Mocktail.cabinet.double_for_instance(thing))
        double_explanation(double)
      elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
        replaced_type_explanation(type_replacement)
      elsif (fake_method_explanation = fake_method_explanation_for(thing))
        fake_method_explanation
      else
        no_explanation(thing)
      end
    end

    private

    def fake_method_explanation_for(thing)
      return unless thing.is_a?(Method)
      method = thing
      receiver = thing.receiver

      receiver_data = if (double = Mocktail.cabinet.double_for_instance(receiver))
        data_for_double(double)
      elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(receiver))
        data_for_type_replacement(type_replacement)
      end

      if receiver_data
        FakeMethodExplanation.new(FakeMethodData.new(
          receiver: receiver,
          calls: receiver_data.calls,
          stubbings: receiver_data.stubbings
        ), describe_dry_method(receiver_data, method.name))
      end
    end

    def data_for_double(double)
      DoubleData.new(
        type: double.original_type,
        double: double.dry_instance,
        calls: Mocktail.cabinet.calls_for_double(double),
        stubbings: Mocktail.cabinet.stubbings_for_double(double)
      )
    end

    def double_explanation(double)
      double_data = data_for_double(double)

      DoubleExplanation.new(double_data, <<~MSG)
        This is a fake `#{double.original_type.name}' instance.

        It has these mocked methods:
        #{double.dry_methods.sort.map { |method| "  - #{method}" }.join("\n")}

        #{double.dry_methods.sort.map { |method| describe_dry_method(double_data, method) }.join("\n")}
      MSG
    end

    def data_for_type_replacement(type_replacement)
      TypeReplacementData.new(
        type: type_replacement.type,
        replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
        calls: Mocktail.cabinet.calls.select { |call|
          call.double == type_replacement.type
        },
        stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
          stubbing.recording.double == type_replacement.type
        }
      )
    end

    def replaced_type_explanation(type_replacement)
      type_replacement_data = data_for_type_replacement(type_replacement)

      ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
        `#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.

        It has these mocked methods:
        #{type_replacement_data.replaced_method_names.map { |method| "  - #{method}" }.join("\n")}

        #{type_replacement_data.replaced_method_names.map { |method| describe_dry_method(type_replacement_data, method) }.join("\n")}
      MSG
    end

    def describe_dry_method(double_data, method)
      method_name = @stringifies_method_name.stringify(Call.new(
        original_type: double_data.type,
        singleton: double_data.type == double_data.double,
        method: method
      ))

      [
        @stringifies_call.stringify_multiple(
          double_data.stubbings.map(&:recording).select { |call|
            call.method == method
          },
          nonzero_message: "`#{method_name}' stubbings",
          zero_message: "`#{method_name}' has no stubbings"
        ),
        @stringifies_call.stringify_multiple(
          double_data.calls.select { |call|
            call.method == method
          },
          nonzero_message: "`#{method_name}' calls",
          zero_message: "`#{method_name}' has no calls"
        )
      ].join("\n")
    end

    def no_explanation(thing)
      NoExplanation.new(thing,
        "Unfortunately, Mocktail doesn't know what this thing is: #{thing.inspect}")
    end
  end
end