RSpec.describe Dry::Logic::Rule do
  subject(:rule) { Rule.build(predicate, options) }

  let(:predicate) { -> { true } }
  let(:options) { {} }

  let(:schema) do
    Class.new do
      define_method(:class, Kernel.instance_method(:class))

      def method_missing(m, *)
        if m.to_s.end_with?('?')
          self.class.new
        else
          super
        end
      end

      def to_proc
        -> value { value }
      end

      def arity
        1
      end

      def parameters
        [[:req, :value]]
      end
    end.new
  end

  it_behaves_like Dry::Logic::Rule

  describe '.new' do
    it 'accepts an :id' do
      expect(Rule.build(predicate, id: :check_num).id).to be(:check_num)
    end
  end

  describe 'with a function returning truthy value' do
    it 'is successful for valid input' do
      expect(Rule.build(-> val { val }).('true')).to be_success
    end

    it 'is not successful for invalid input' do
      expect(Rule.build(-> val { val }).(nil)).to be_failure
    end
  end

  describe '#ast' do
    it 'returns predicate node with :id' do
      expect(Rule.build(-> value { true }).with(id: :email?).ast('oops')).to eql(
        [:predicate, [:email?, [[:value, 'oops']]]]
      )
    end

    it 'returns predicate node with undefined args' do
      expect(Rule.build(-> value { true }).with(id: :email?).ast).to eql(
        [:predicate, [:email?, [[:value, Undefined]]]]
      )
    end
  end

  describe '#type' do
    it 'returns rule type' do
      expect(rule.type).to be(:rule)
    end
  end

  describe '#bind' do
    let(:bound) { rule.with(id: :bound).bind(object) }

    context 'with an unbound method' do
      let(:predicate) { klass.instance_method(:test?) }
      let(:klass) { Class.new { def test?; true; end } }
      let(:object) { klass.new }

      it 'returns a new rule with its predicate bound to a specific object' do
        expect(bound.()).to be_success
      end

      it 'carries id' do
        expect(bound.id).to be(:bound)
      end
    end

    context 'with an arbitrary block' do
      let(:predicate) { -> value { value == expected } }
      let(:object) { Class.new { def expected; 'test'; end }.new }

      it 'returns a new with its predicate executed in the context of the provided object' do
        expect(bound.('test')).to be_success
        expect(bound.('oops')).to be_failure
      end

      it 'carries id' do
        expect(bound.id).to be(:bound)
      end

      it 'stores arity' do
        expect(bound.options[:arity]).to be(rule.arity)
      end

      it 'stores parameters' do
        expect(bound.options[:parameters]).to eql(rule.parameters)
      end
    end

    context 'with a schema instance' do
      let(:object) { schema }
      let(:predicate) { schema }

      it 'returns a new with its predicate executed in the context of the provided object' do
        expect(bound.(true)).to be_success
        expect(bound.(false)).to be_failure
      end
    end
  end

  describe '#eval_args' do
    context 'with an unbound method' do
      let(:options) { { args: [1, klass.instance_method(:num), :foo], arity: 3 } }
      let(:klass) { Class.new { def num; 7; end } }
      let(:object) { klass.new }

      it 'evaluates args in the context of the provided object' do
        expect(rule.eval_args(object).args).to eql([1, 7, :foo])
      end
    end

    context 'with a schema instance' do
      let(:options) { { args: [1, schema, :foo], arity: 3 } }
      let(:object) { Object.new }

      it 'returns a new with its predicate executed in the context of the provided object' do
        expect(rule.eval_args(object).args).to eql([1, schema, :foo])
      end
    end
  end

  describe 'arity specialization' do
    describe '0-arity rule' do
      let(:options) { { args: [1], arity: 1 } }
      let(:predicate) { :odd?.to_proc }

      it 'generates interface with the right arity' do
        expect(rule.method(:call).arity).to be_zero
        expect(rule.method(:[]).arity).to be_zero
        expect(rule[]).to be(true)
        expect(rule.()).to be_success
      end
    end

    describe '1-arity rule' do
      let(:options) { { args: [1], arity: 2 } }
      let(:predicate) { -> a, b { a + b } }

      it 'generates interface with the right arity' do
        expect(rule.method(:call).arity).to be(1)
        expect(rule.method(:[]).arity).to be(1)
        expect(rule[10]).to be(11)
        expect(rule.(1)).to be_success
      end
    end

    describe 'currying' do
      let(:options) { { args: [], arity: 2 } }
      let(:predicate) { -> a, b { a + b } }
      let(:rule) { super().curry(1) }

      it 'generates correct arity on currying' do
        expect(rule.method(:call).arity).to be(1)
        expect(rule.method(:[]).arity).to be(1)
        expect(rule[10]).to be(11)
        expect(rule.(1)).to be_success
      end
    end

    describe 'arbitrary arity' do
      let(:arity) { rand(20) }
      let(:curried) { arity.zero? ? 0 : rand(arity) }

      let(:options) { { args: [1] * curried, arity: arity } }
      let(:predicate) { double(:predicate) }

      it 'generates correct arity' do
        expect(rule.method(:call).arity).to be(arity - curried)
        expect(rule.method(:[]).arity).to be(arity - curried)
      end
    end

    describe '-1 arity' do
      let(:options) { { args: [], arity: -1 } }

      it 'accepts variable number of arguments' do
        expect(rule.method(:call).arity).to be(-1)
        expect(rule.method(:[]).arity).to be(-1)
      end
    end
  end
end