class TestTrack::VaryDSL
  include TestTrack::RequiredOptions

  attr_reader :defaulted, :default_variant
  alias defaulted? defaulted

  def initialize(opts = {})
    @assignment = require_option!(opts, :assignment)
    @context = require_option!(opts, :context)
    @split_registry = require_option!(opts, :split_registry, allow_nil: true)
    raise ArgumentError, "unknown opts: #{opts.keys.to_sentence}" if opts.present?
    raise ArgumentError, "unknown split: #{split_name}" if @split_registry && !split
  end

  def when(*variants, &block)
    raise ArgumentError, "must provide at least one variant" unless variants.present?
    variants.each do |variant|
      assign_behavior_to_variant(variant, block)
    end
  end

  def default(variant, &block)
    raise ArgumentError, "cannot provide more than one `default`" unless default_variant.nil?
    @default_variant = assign_behavior_to_variant(variant, block)
  end

  private

  attr_reader :split_registry, :assignment, :context
  delegate :split_name, to: :assignment

  def split
    split_registry && split_registry[split_name]
  end

  def split_variants
    @split_variants ||= split.keys if split_registry
  end

  def notify_because_vary(msg)
    misconfiguration_notifier.notify("vary for \"#{split_name}\" #{msg}")
  end

  def misconfiguration_notifier
    @misconfiguration_notifier ||= TestTrack::MisconfigurationNotifier.new
  end

  def variant_behaviors
    @variant_behaviors ||= {}
  end

  def assign_behavior_to_variant(variant, behavior_proc)
    variant = variant.to_s

    raise ArgumentError, "must provide block for #{variant}" unless behavior_proc
    notify_because_vary "configures unknown variant \"#{variant}\"" unless variant_acceptable?(variant)

    variant_behaviors[variant] = behavior_proc
    variant
  end

  def variant_acceptable?(variant)
    split_variants ? split_variants.include?(variant) : true # If we're flying blind (with no split registry), assume the dev is correct
  end

  def default_proc
    variant_behaviors[default_variant]
  end

  def run # rubocop:disable Metrics/AbcSize
    validate!

    if variant_behaviors[assignment.variant]
      chosen_proc = variant_behaviors[assignment.variant]
    else
      chosen_proc = default_proc
      assignment.variant = default_variant
      @defaulted = true
    end
    assignment.context = context
    chosen_proc.call
  end

  def validate!
    raise ArgumentError, "must provide exactly one `default`" unless default_variant
    raise ArgumentError, "must provide at least one `when`" unless variant_behaviors.size >= 2
    return true unless split_variants
    missing_variants = split_variants - variant_behaviors.keys
    notify_because_vary("does not configure variants #{missing_variants.to_sentence}") && false unless missing_variants.empty?
  end
end