# frozen_string_literal: true

# = StepsInspector
#
# This class takes a {Service::Base::Context} object and inspects it.
# It will output a list of steps and what is their known state.
class StepsInspector
  # @!visibility private
  class Step
    attr_reader :step, :result, :nesting_level

    delegate :name, to: :step
    delegate :failure?, :success?, :error, to: :step_result, allow_nil: true

    def self.for(step, result, nesting_level: 0)
      class_name =
        "#{module_parent_name}::#{step.class.name.split("::").last.sub(/^(\w+)Step$/, "\\1")}"
      class_name.constantize.new(step, result, nesting_level: nesting_level)
    end

    def initialize(step, result, nesting_level: 0)
      @step = step
      @result = result
      @nesting_level = nesting_level
    end

    def type
      self.class.name.split("::").last.downcase
    end

    def emoji
      "#{result_emoji}#{unexpected_result_emoji}"
    end

    def steps
      [self]
    end

    def inspect
      "#{"  " * nesting_level}[#{type}] '#{name}' #{emoji}".rstrip
    end

    private

    def step_result
      result["result.#{type}.#{name}"]
    end

    def result_emoji
      return "❌" if failure?
      return "✅" if success?
      ""
    end

    def unexpected_result_emoji
      " ⚠️#{unexpected_result_text}" if step_result.try(:[], "spec.unexpected_result")
    end

    def unexpected_result_text
      return "  <= expected to return true but got false instead" if failure?
      "  <= expected to return false but got true instead"
    end
  end

  # @!visibility private
  class Model < Step
    def error
      return result[name].errors.inspect if step_result.invalid
      step_result.exception.full_message
    end
  end

  # @!visibility private
  class Contract < Step
    def error
      step_result.errors.inspect
    end
  end

  # @!visibility private
  class Policy < Step
    def error
      step_result.reason
    end
  end

  # @!visibility private
  class Transaction < Step
    def steps
      [self, *step.steps.map { Step.for(_1, result, nesting_level: nesting_level + 1).steps }]
    end

    def inspect
      "#{"  " * nesting_level}[#{type}]"
    end

    def step_result
      nil
    end
  end

  attr_reader :steps, :result

  def initialize(result)
    @steps = result.__steps__.map { Step.for(_1, result).steps }.flatten
    @result = result
  end

  # Inspect the provided result object.
  # Example output:
  #   [1/4] [model] 'channel' ✅
  #   [2/4] [contract] 'default' ✅
  #   [3/4] [policy] 'check_channel_permission' ❌
  #   [4/4] [step] 'change_status'
  # @return [String] the steps of the result object with their state
  def inspect
    steps.map.with_index { |step, index| "[#{index + 1}/#{steps.size}] #{step.inspect}" }.join("\n")
  end

  # @return [String, nil] the first available error, if any.
  def error
    steps.detect(&:failure?)&.error
  end
end