require "spec_helper"

RSpec.describe ActiveRemote::Association do
  let(:record) { double(:record) }
  let(:records) { [record] }

  describe ".belongs_to" do
    context "simple association" do
      let(:author_guid) { "AUT-123" }
      let(:user_guid) { "USR-123" }
      let(:default_category_guid) { "CAT-123" }
      subject { Post.new(author_guid: author_guid, user_guid: user_guid) }

      it { is_expected.to respond_to(:author) }
      it { is_expected.to respond_to(:author=) }

      it "searches the associated model for a single record" do
        expect(Author).to receive(:search).with({guid: subject.author_guid}).and_return(records)
        expect(subject.author).to eq record
      end

      it "memoizes the result record" do
        expect(Author).to receive(:search).once.with({guid: subject.author_guid}).and_return(records)
        3.times { expect(subject.author).to eq record }
      end

      context "when guid is nil" do
        subject { Post.new }

        it "returns nil" do
          expect(subject.author).to be_nil
        end
      end

      context "when the search is empty" do
        it "returns a nil" do
          expect(Author).to receive(:search).with({guid: subject.author_guid}).and_return([])
          expect(subject.author).to be_nil
        end
      end

      context "scoped field" do
        it { is_expected.to respond_to(:user) }

        it "searches the associated model for multiple records" do
          expect(Author).to receive(:search).with({guid: subject.author_guid, user_guid: subject.user_guid}).and_return(records)
          expect(subject.user).to eq(record)
        end

        context "when user_guid doesnt exist on model " do
          before { allow(subject.class).to receive_message_chain(:public_instance_methods, :include?).with(:user_guid).and_return(false) }

          it "raises an error" do
            expect { subject.user }.to raise_error(::RuntimeError, /Could not find attribute/)
          end
        end

        context "when user_guid doesnt exist on associated model " do
          before { allow(Author).to receive_message_chain(:public_instance_methods, :include?).with(:user_guid).and_return(false) }

          it "raises an error" do
            expect { subject.user }.to raise_error(::RuntimeError, /Could not find attribute/)
          end
        end
      end
    end

    context "specific association with class name" do
      let(:author_guid) { "AUT-456" }

      subject { Post.new(author_guid: author_guid) }
      it { is_expected.to respond_to(:coauthor) }

      it "searches the associated model for a single record" do
        expect(Author).to receive(:search).with({guid: subject.author_guid}).and_return(records)
        expect(subject.coauthor).to eq record
      end

      it "creates a setter method" do
        author = Author.new(guid: author_guid)
        subject.coauthor = author
        expect(subject.coauthor).to eq(author)
      end
    end

    context "specific association with class name and foreign_key" do
      let(:author_guid) { "AUT-456" }

      subject { Post.new(bestseller_guid: author_guid) }
      it { is_expected.to respond_to(:bestseller) }

      it "searches the associated model for a single record" do
        expect(Author).to receive(:search).with({guid: subject.bestseller_guid}).and_return(records)
        expect(subject.bestseller).to eq record
      end
    end
  end

  describe ".has_many" do
    let(:records) { [record, record, record] }
    let(:guid) { "AUT-123" }
    let(:user_guid) { "USR-123" }

    subject { Author.new(guid: guid, user_guid: user_guid) }

    it { is_expected.to respond_to(:posts) }
    it { is_expected.to respond_to(:posts=) }

    it "searches the associated model for all associated records" do
      expect(Post).to receive(:search).with({author_guid: subject.guid}).and_return(records)
      expect(subject.posts).to eq records
    end

    it "memoizes the result record" do
      expect(Post).to receive(:search).once.with({author_guid: subject.guid}).and_return(records)
      3.times { expect(subject.posts).to eq records }
    end

    context "when guid is nil" do
      subject { Author.new }

      it "returns []" do
        expect(subject.posts).to eq []
      end
    end

    context "when the search is empty" do
      it "returns the empty set" do
        expect(Post).to receive(:search).with({author_guid: subject.guid}).and_return([])
        expect(subject.posts).to be_empty
      end
    end

    context "specific association with class name" do
      it { is_expected.to respond_to(:flagged_posts) }

      it "searches the associated model for a single record" do
        expect(Post).to receive(:search).with({author_guid: subject.guid}).and_return([])
        expect(subject.flagged_posts).to be_empty
      end
    end

    context "specific association with class name and foreign_key" do
      it { is_expected.to respond_to(:bestseller_posts) }

      it "searches the associated model for multiple record" do
        expect(Post).to receive(:search).with({bestseller_guid: subject.guid}).and_return(records)
        expect(subject.bestseller_posts).to eq(records)
      end
    end

    context "scoped field" do
      it { is_expected.to respond_to(:user_posts) }

      it "searches the associated model for multiple records" do
        expect(Post).to receive(:search).with({author_guid: subject.guid, user_guid: subject.user_guid}).and_return(records)
        expect(subject.user_posts).to eq(records)
      end

      context "when user_guid doesnt exist on associated model " do
        before { allow(Post).to receive_message_chain(:public_instance_methods, :include?).with(:user_guid).and_return(false) }

        it "raises an error" do
          expect { subject.user_posts }.to raise_error(::RuntimeError, /Could not find attribute/)
        end
      end
    end

    context "writer method" do
      context "when new value is not an array" do
        it "raises an error" do
          expect { subject.posts = Post.new }.to raise_error(::RuntimeError, /New value must be an array/)
        end
      end
    end
  end

  describe ".has_one" do
    let(:guid) { "CAT-123" }
    let(:user_guid) { "USR-123" }
    let(:category_attributes) {
      {
        guid: guid,
        user_guid: user_guid
      }
    }

    subject { Category.new(category_attributes) }

    it { is_expected.to respond_to(:author) }
    it { is_expected.to respond_to(:author=) }

    it "searches the associated model for all associated records" do
      expect(Author).to receive(:search).with({category_guid: subject.guid}).and_return(records)
      expect(subject.author).to eq record
    end

    it "memoizes the result record" do
      expect(Author).to receive(:search).once.with({category_guid: subject.guid}).and_return(records)
      3.times { expect(subject.author).to eq record }
    end

    context "when guid is nil" do
      subject { Category.new }

      it "returns nil" do
        expect(subject.author).to be_nil
      end
    end

    context "when the search is empty" do
      it "returns a nil value" do
        expect(Author).to receive(:search).with({category_guid: subject.guid}).and_return([])
        expect(subject.author).to be_nil
      end
    end

    context "specific association with class name" do
      it { is_expected.to respond_to(:senior_author) }

      it "searches the associated model for a single record" do
        expect(Author).to receive(:search).with({category_guid: subject.guid}).and_return(records)
        expect(subject.senior_author).to eq record
      end

      it "creates a setter method" do
        expect(subject).to respond_to(:senior_author=)
      end
    end

    context "specific association with class name and foreign_key" do
      it { is_expected.to respond_to(:primary_editor) }

      it "searches the associated model for a single record" do
        expect(Author).to receive(:search).with({editor_guid: subject.guid}).and_return(records)
        expect(subject.primary_editor).to eq record
      end
    end

    context "scoped field" do
      it { is_expected.to respond_to(:chief_editor) }

      it "searches the associated model for multiple records" do
        expect(Author).to receive(:search).with({chief_editor_guid: subject.guid, user_guid: subject.user_guid}).and_return(records)
        expect(subject.chief_editor).to eq(record)
      end

      context "when user_guid doesnt exist on associated model " do
        before { allow(Author).to receive_message_chain(:public_instance_methods, :include?).with(:user_guid).and_return(false) }

        it "raises an error" do
          expect { subject.chief_editor }.to raise_error(::RuntimeError, /Could not find attribute/)
        end
      end
    end
  end
end
