require_relative '../spec_helper'
require 'recursive_open_struct'

describe RecursiveOpenStruct do

  describe "recursive behavior" do
    let(:h) { { :blah => { :another => 'value' } } }
    subject(:ros) { RecursiveOpenStruct.new(h) }

    it "can convert the entire hash tree back into a hash" do
      blank_obj = Object.new
      h = {:asdf => 'John Smith', :foo => [{:bar => blank_obj}, {:baz => nil}]}
      ros = RecursiveOpenStruct.new(h)

      expect(ros.to_h).to eq h
      expect(ros.to_hash).to eq h
    end

    it "returns accessed hashes as RecursiveOpenStructs instead of hashes" do
      expect(subject.blah.another).to eq 'value'
    end

    it "handles subscript notation the same way as dotted notation" do
      expect(subject.blah.another).to eq subject[:blah].another
    end

    it "uses #key_as_a_hash to return key as a Hash" do
      expect(subject.blah_as_a_hash).to eq({ :another => 'value' })
    end

    it "handles sub-element replacement with dotted notation before member setup" do
      expect(ros[:blah][:another]).to eql 'value'
      expect(ros.methods).not_to include(:blah)

      ros.blah = { changed: 'backing' }

      expect(ros.blah.changed).to eql 'backing'
    end

    describe "handling loops in the original Hashes" do
      let(:h1) { { :a => 'a'} }
      let(:h2) { { :a => 'b', :h1 => h1 } }
      before(:each) { h1[:h2] = h2 }

      subject { RecursiveOpenStruct.new(h2) }

      it { expect(subject.h1.a).to eq 'a' }
      it { expect(subject.h1.h2.a).to eq 'b' }
      it { expect(subject.h1.h2.h1.a).to eq 'a' }
      it { expect(subject.h1.h2.h1.h2.a).to eq 'b' }
      it { expect(subject.h1).to eq subject.h1.h2.h1 }
      it { expect(subject.h1).to_not eq subject.h1.h2 }
    end # describe handling loops in the origin Hashes

    it "can modify a key of a sub-element" do
      h = {
        :blah => {
          :blargh => 'Brad'
        }
      }
      ros = RecursiveOpenStruct.new(h)
      ros.blah.blargh = "Janet"

      expect(ros.blah.blargh).to eq "Janet"
    end

    describe 'subscript mutation notation' do
      it 'handles the basic case' do
        subject[:blah] = 12345
        expect(subject.blah).to eql 12345
      end

      it 'recurses properly' do
        subject[:blah][:another] = 'abc'
        expect(subject.blah.another).to eql 'abc'
        expect(subject.blah_as_a_hash).to eql({ :another => 'abc' })
      end

      let(:diff){ { :different => 'thing' } }

      it 'can replace the entire hash' do
        expect(subject.to_h).to eql(h)
        subject[:blah] = diff
        expect(subject.to_h).to eql({ :blah => diff })
      end

      it 'updates sub-element cache' do
        expect(subject.blah.different).to be_nil
        subject[:blah] = diff
        expect(subject.blah.different).to eql 'thing'
        expect(subject.blah_as_a_hash).to eql(diff)
      end
    end

    context "after a sub-element has been modified" do
      let(:hash) do
        { :blah => { :blargh => "Brad" }, :some_array => [ 1, 2, 3] }
      end
      let(:updated_hash) do
        { :blah => { :blargh => "Janet" }, :some_array => [ 1, 2, 3] }
      end

      subject { RecursiveOpenStruct.new(hash) }

      before(:each) { subject.blah.blargh = "Janet" }

      describe ".to_h" do
        it "returns a hash tree that contains those modifications" do
          expect(subject.to_h).to eq updated_hash
        end

        specify "modifying the returned hash tree does not modify the ROS" do
          subject.to_h[:blah][:blargh] = "Dr Scott"

          expect(subject.blah.blargh).to eq "Janet"
        end
      end

      it "does not mutate the original hash tree passed to the constructor" do
        expect(hash[:blah][:blargh]).to eq 'Brad'
      end

      it "limits the deep-copy to the initial hash tree" do
        subject.some_array[0] = 4

        expect(hash[:some_array][0]).to eq 4
      end

      describe "#dup" do
        let(:duped_subject) { subject.dup }

        it "preserves sub-element modifications" do
          expect(duped_subject.blah.blargh).to eq subject.blah.blargh
        end

        it "allows the copy's sub-elements to be modified independently from the original's" do
          expect(subject.blah.blargh).to eq "Janet"

          duped_subject.blah.blargh = "Dr. Scott"

          expect(subject.blah.blargh).to eq "Janet"
          expect(duped_subject.blah.blargh).to eq "Dr. Scott"
        end
      end
    end

    context "when memoizing and then modifying entire recursive structures" do
      subject do
        RecursiveOpenStruct.new(
          { :blah => original_blah }, :recurse_over_arrays => true)
      end

      before(:each) { subject.blah } # enforce memoization

      context "when modifying an entire Hash" do
        let(:original_blah) { { :a => 'A', :b => 'B' } }
        let(:new_blah) { { :something_new => "C" } }

        before(:each) { subject.blah = new_blah }

        it "returns the modified value instead of the memoized one" do
          expect(subject.blah.something_new).to eq "C"
        end

        specify "the old value no longer exists" do
          expect(subject.blah.a).to be_nil
        end
      end

      context "when modifying an entire Array" do
        let(:original_blah) { [1, 2, 3] }

        it "returns the modified value instead of the memoized one" do
          new_blah = [4, 5, 6]
          subject.blah = new_blah
          expect(subject.blah).to eq new_blah
        end
      end
    end

    describe 'recursing over arrays' do
      let(:blah_list) { [ { :foo => '1' }, { :foo => '2' }, 'baz' ] }
      let(:h) { { :blah => blah_list } }

      context "when recursing over arrays is enabled" do
        subject { RecursiveOpenStruct.new(h, :recurse_over_arrays => true) }

        it { expect(subject.blah.length).to eq 3 }
        it { expect(subject.blah[0].foo).to eq '1' }
        it { expect(subject.blah[1].foo).to eq '2' }
        it { expect(subject.blah_as_a_hash).to eq blah_list }
        it { expect(subject.blah[2]).to eq 'baz' }

        context "when an inner value changes" do
          let(:updated_blah_list) { [ { :foo => '1' }, { :foo => 'Dr Scott' }, 'baz' ] }
          let(:updated_h) { { :blah => updated_blah_list } }

          before(:each) { subject.blah[1].foo = "Dr Scott" }

          it "Retains changes across Array lookups" do
            expect(subject.blah[1].foo).to eq "Dr Scott"
          end

          it "propagates the changes through to .to_h across Array lookups" do
            expect(subject.to_h).to eq({
              :blah => [ { :foo => '1' }, { :foo => "Dr Scott" }, 'baz' ]
            })
          end

          it "deep-copies hashes within Arrays" do
            subject.to_h[:blah][1][:foo] = "Rocky"

            expect(subject.blah[1].foo).to eq "Dr Scott"
          end

          it "does not mutate the input hash passed to the constructor" do
            expect(h[:blah][1][:foo]).to eq '2'
          end

          it "the deep copy recurses over Arrays as well" do
            expect(h[:blah][1][:foo]).to eq '2'
          end

          describe "#dup" do
            let(:duped_subject) { subject.dup }

            it "preserves sub-element modifications" do
              expect(duped_subject.blah[1].foo).to eq subject.blah[1].foo
            end

            it "allows the copy's sub-elements to be modified independently from the original's" do
              duped_subject.blah[1].foo = "Rocky"

              expect(duped_subject.blah[1].foo).to eq "Rocky"
              expect(subject.blah[1].foo).to eq "Dr Scott"
            end
          end
        end

        context "when array is nested deeper" do
          let(:deep_hash) { { :foo => { :blah => blah_list } } }
          subject { RecursiveOpenStruct.new(deep_hash, :recurse_over_arrays => true) }

          it { expect(subject.foo.blah.length).to eq 3 }
          it "Retains changes across Array lookups" do
            subject.foo.blah[1].foo = "Dr Scott"
            expect(subject.foo.blah[1].foo).to eq "Dr Scott"
          end

        end

        context "when array is in an array" do
          let(:haah) { { :blah => [ blah_list ] } }
          subject { RecursiveOpenStruct.new(haah, :recurse_over_arrays => true) }

          it { expect(subject.blah.length).to eq 1 }
          it { expect(subject.blah[0].length).to eq 3 }
          it "Retains changes across Array lookups" do
            subject.blah[0][1].foo = "Dr Scott"

            expect(subject.blah[0][1].foo).to eq "Dr Scott"
          end

        end

      end # when recursing over arrays is enabled

      context "when recursing over arrays is disabled" do
        subject { RecursiveOpenStruct.new(h) }

        it { expect(subject.blah.length).to eq 3 }
        it { expect(subject.blah[0]).to eq({ :foo => '1' }) }
        it { expect(subject.blah[0][:foo]).to eq '1' }
      end # when recursing over arrays is disabled

      describe 'modifying an array and recursing over it' do
        let(:h) { {} }
        subject { RecursiveOpenStruct.new(h, recurse_over_arrays: true) }

        context 'when adding an array with hashes into the tree' do
          before(:each) do
            subject.mystery = {}
            subject.mystery.science = [{ theatre: 9000 }]
          end

          it "ROS's it" do
            expect(subject.mystery.science[0].theatre).to eq 9000
          end
        end

        context 'when appending a hash to an array' do
          before(:each) do
            subject.mystery = {}
            subject.mystery.science = []
            subject.mystery.science << { theatre: 9000 }
          end

          it "ROS's it" do
            expect(subject.mystery.science[0].theatre).to eq 9000
          end

          specify "the changes show up in .to_h" do
            expect(subject.to_h).to eq({ mystery: { science: [{theatre: 9000}]}})
          end
        end

        context 'after appending a hash to an array' do
          before(:each) do
            subject.mystery = {}
            subject.mystery.science = []
            subject.mystery.science[0] = {}
          end

          it "can have new values be set" do
            expect do
              subject.mystery.science[0].theatre = 9000
            end.to_not raise_error

            expect(subject.mystery.science[0].theatre).to eq 9000
          end
        end
      end # modifying an array and then recursing
    end # recursing over arrays

    describe 'nested nil values' do
      let(:h) { { foo: { bar: nil }} }
      it 'returns nil' do
        expect(subject.foo.bar).to be_nil
      end

      it 'returns a hash with the key and a nil value' do
        expect(subject.to_hash).to eq({ foo: { bar: nil }})
      end
    end # nested nil values
  end # recursive behavior
end
