# frozen_string_literal: true

RSpec.describe Faraday::Options do
  SubOptions = Class.new(Faraday::Options.new(:sub_a, :sub_b))
  class ParentOptions < Faraday::Options.new(:a, :b, :c)
    options c: SubOptions
  end

  describe '#merge' do
    it 'merges options with hashes' do
      options = ParentOptions.new(1)
      expect(options.a).to eq(1)
      expect(options.b).to be_nil

      dup = options.merge a: 2, b: 3
      expect(dup.a).to eq(2)
      expect(dup.b).to eq(3)
      expect(options.a).to eq(1)
      expect(options.b).to be_nil
    end

    it 'deeply merges two options' do
      sub_opts1 = SubOptions.from(sub_a: 3)
      sub_opts2 = SubOptions.from(sub_b: 4)
      opt1      = ParentOptions.from(a: 1, c: sub_opts1)
      opt2      = ParentOptions.from(b: 2, c: sub_opts2)

      merged = opt1.merge(opt2)

      expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4)
      expected          = ParentOptions.from(a: 1, b: 2, c: expected_sub_opts)
      expect(merged).to eq(expected)
    end

    it 'deeply merges options with hashes' do
      sub_opts1 = SubOptions.from(sub_a: 3)
      sub_opts2 = { sub_b: 4 }
      opt1      = ParentOptions.from(a: 1, c: sub_opts1)
      opt2      = { b: 2, c: sub_opts2 }

      merged = opt1.merge(opt2)

      expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4)
      expected          = ParentOptions.from(a: 1, b: 2, c: expected_sub_opts)
      expect(merged).to eq(expected)
    end

    it 'deeply merges options with nil' do
      sub_opts = SubOptions.new(3, 4)
      options  = ParentOptions.new(1, 2, sub_opts)
      expect(options.a).to eq(1)
      expect(options.b).to eq(2)
      expect(options.c.sub_a).to eq(3)
      expect(options.c.sub_b).to eq(4)

      options2 = ParentOptions.from(b: 5, c: nil)

      merged = options.merge(options2)

      expect(merged.b).to eq(5)
      expect(merged.c).to eq(sub_opts)
    end

    it 'deeply merges options with options having nil sub-options' do
      options = ParentOptions.from(a: 1)

      sub_opts = SubOptions.new(3, 4)
      options2 = ParentOptions.from(b: 2, c: sub_opts)

      expect(options.a).to eq(1)
      expect(options2.b).to eq(2)
      expect(options2.c.sub_a).to eq(3)
      expect(options2.c.sub_b).to eq(4)

      merged = options.merge(options2)

      expect(merged.c).to eq(sub_opts)
    end

    describe '#dup' do
      it 'duplicate options but not sub-options' do
        sub_opts = SubOptions.from(sub_a: 3)
        opts     = ParentOptions.from(b: 1, c: sub_opts)

        duped         = opts.dup
        duped.b       = 2
        duped.c.sub_a = 4

        expect(opts.b).to eq(1)
        expect(opts.c.sub_a).to eq(4)
      end
    end

    describe '#deep_dup' do
      it 'duplicate options and also suboptions' do
        sub_opts = SubOptions.from(sub_a: 3)
        opts     = ParentOptions.from(b: 1, c: sub_opts)

        duped         = opts.deep_dup
        duped.b       = 2
        duped.c.sub_a = 4

        expect(opts.b).to eq(1)
        expect(opts.c.sub_a).to eq(3)
      end
    end

    describe '#clear' do
      it 'clears the options' do
        options = SubOptions.new(1)
        expect(options.empty?).not_to be_truthy
        options.clear
        expect(options.empty?).to be_truthy
      end
    end

    describe '#empty?' do
      it 'returns true only if all options are nil' do
        options = SubOptions.new
        expect(options.empty?).to be_truthy
        options.sub_a = 1
        expect(options.empty?).not_to be_truthy
        options.delete(:sub_a)
        expect(options.empty?).to be_truthy
      end
    end

    describe '#each_key' do
      it 'allows to iterate through keys' do
        options = ParentOptions.new(1, 2, 3)
        enum    = options.each_key
        expect(enum.next.to_sym).to eq(:a)
        expect(enum.next.to_sym).to eq(:b)
        expect(enum.next.to_sym).to eq(:c)
      end
    end

    describe '#key?' do
      it 'returns true if the key exists and is not nil' do
        options = SubOptions.new
        expect(options.key?(:sub_a)).not_to be_truthy
        options.sub_a = 1
        expect(options.key?(:sub_a)).to be_truthy
      end
    end

    describe '#each_value' do
      it 'allows to iterate through values' do
        options = ParentOptions.new(1, 2, 3)
        enum    = options.each_value
        expect(enum.next).to eq(1)
        expect(enum.next).to eq(2)
        expect(enum.next).to eq(3)
      end
    end

    describe '#value?' do
      it 'returns true if any key has that value' do
        options = SubOptions.new
        expect(options.value?(1)).not_to be_truthy
        options.sub_a = 1
        expect(options.value?(1)).to be_truthy
      end
    end

    describe '#update' do
      it 'updates options from hashes' do
        options = ParentOptions.new(1)
        expect(options.a).to eq(1)
        expect(options.b).to be_nil

        updated = options.update a: 2, b: 3
        expect(options.a).to eq(2)
        expect(options.b).to eq(3)
        expect(updated).to eq(options)
      end
    end

    describe '#delete' do
      it 'allows to remove value for key' do
        options = ParentOptions.new(1)
        expect(options.a).to eq(1)
        expect(options.delete(:a)).to eq(1)
        expect(options.a).to be_nil
      end
    end

    describe '#from' do
      it { expect { ParentOptions.from invalid: 1 }.to raise_error(NoMethodError) }

      it 'works with options' do
        options = ParentOptions.new(1)

        value = ParentOptions.from(options)
        expect(value.a).to eq(1)
        expect(value.b).to be_nil
      end

      it 'works with options with sub object' do
        sub     = SubOptions.new(1)
        options = ParentOptions.from a: 1, c: sub
        expect(options).to be_a_kind_of(ParentOptions)
        expect(options.a).to eq(1)
        expect(options.b).to be_nil
        expect(options.c).to be_a_kind_of(SubOptions)
        expect(options.c.sub_a).to eq(1)
      end

      it 'works with hash' do
        options = ParentOptions.from a: 1
        expect(options).to be_a_kind_of(ParentOptions)
        expect(options.a).to eq(1)
        expect(options.b).to be_nil
      end

      it 'works with hash with sub object' do
        options = ParentOptions.from a: 1, c: { sub_a: 1 }
        expect(options).to be_a_kind_of(ParentOptions)
        expect(options.a).to eq(1)
        expect(options.b).to be_nil
        expect(options.c).to be_a_kind_of(SubOptions)
        expect(options.c.sub_a).to eq(1)
      end

      it 'works with deep hash' do
        hash    = { b: 1 }
        options = ParentOptions.from a: hash
        expect(options.a[:b]).to eq(1)

        hash[:b] = 2
        expect(options.a[:b]).to eq(1)

        options.a[:b] = 3
        expect(hash[:b]).to eq(2)
        expect(options.a[:b]).to eq(3)
      end

      it 'works with nil' do
        options = ParentOptions.from(nil)
        expect(options).to be_a_kind_of(ParentOptions)
        expect(options.a).to be_nil
        expect(options.b).to be_nil
      end

      it 'respects inheritance' do
        subclass = Class.new(ParentOptions)
        options  = subclass.from(c: { sub_a: 'hello' })
        expect(options.c).to be_a_kind_of(SubOptions)
        expect(options.c.sub_a).to eq('hello')
      end
    end

    describe '#memoized' do
      subject(:options_class) { Class.new(ParentOptions) }
      it 'requires block' do
        expect { options_class.memoized(:a) }.to raise_error(ArgumentError)
      end

      it 'accepts block' do
        options_class.memoized(:a) { :foo }
        expect(options_class.new.a).to eql(:foo)
      end
    end

    describe '#fetch' do
      subject { SubOptions.new }

      context 'when the fetched key has no value' do
        it 'uses falsey default' do
          expect(subject.fetch(:sub_a, false) { |_| :blah }).to be_falsey
        end

        it 'accepts block' do
          expect(subject.fetch(:sub_a) { |k| "yo #{k.inspect}" }).to eq('yo :sub_a')
        end

        it 'needs a default if key is missing' do
          expect { subject.fetch(:sub_a) }.to raise_error(Faraday::Options.fetch_error_class)
        end
      end

      context 'when the fetched key has a value' do
        before do
          subject.sub_a = 1
        end

        it 'grabs value' do
          expect(subject.fetch(:sub_a, false) { |_| :blah }).to eq(1)
        end

        it 'works with key' do
          expect(subject.fetch(:sub_a)).to eq(1)
        end
      end
    end
  end
end
