RSpec.shared_examples 'ApplicationModel::CanLookup' do
  describe '.lookup_keys' do
    it 'returns a subset of: id, name, login, email, number' do
      expect(described_class.lookup_keys)
        .to all(be_in(%i[id name login email number]))
    end

    it 'only includes attributes present on the model' do
      expect(described_class.lookup_keys)
        .to all(be_in(described_class.attribute_names.map(&:to_sym)))
    end
  end

  describe '.lookup' do
    around do |example|
      Rails.cache.clear
      example.run
      Rails.cache.clear
    end

    let!(:instance)          { create(described_class.name.underscore) }
    let(:valid_lookup_key)   { (described_class.lookup_keys - [:id]).sample }
    let(:invalid_lookup_key) { (described_class.attribute_names.map(&:to_sym) - described_class.lookup_keys).sample }

    it 'accepts exactly one attribute-value pair' do
      expect { described_class.lookup(id: instance.id, valid_lookup_key => 'foo') }
        .to raise_error(ArgumentError)
    end

    it 'only accepts attributes from .lookup_keys' do
      expect { described_class.lookup(invalid_lookup_key => 'foo') }
        .to raise_error(ArgumentError)
    end

    shared_examples 'per-attribute examples' do |attribute|
      it "finds records by #{attribute} (like .find_by)" do
        expect(described_class.lookup(attribute => instance.send(attribute))).to eq(instance)
      end

      describe "cache storage by #{attribute}" do
        context 'inside a DB transaction' do  # provided by default RSpec config
          it 'leaves the cache untouched' do
            expect { described_class.lookup(attribute => instance.send(attribute)) }
              .not_to change { described_class.cache_get(instance.send(attribute)) }
          end
        end

        context 'outside a DB transaction' do
          before do
            allow(ActiveRecord::Base.connection)
              .to receive(:transaction_open?).and_return(false)
          end

          context 'when called for the first time' do
            it 'saves the value to the cache' do
              expect(Rails.cache)
                .to receive(:write)
                .with("#{described_class}::#{instance.send(attribute)}", instance, { expires_in: 7.days })
                .and_call_original

              expect { described_class.lookup(attribute => instance.send(attribute)) }
                .to change { described_class.cache_get(instance.send(attribute)) }
                .to(instance)
            end
          end

          context 'when called a second time' do
            before { described_class.lookup(attribute => instance.send(attribute)) }

            it 'retrieves results from cache' do
              expect(Rails.cache)
                .to receive(:read)
                .with("#{described_class}::#{instance.send(attribute)}")

              described_class.lookup(attribute => instance.send(attribute))
            end

            if attribute != :id
              context 'after it has been updated' do
                let!(:old_attribute_val) { instance.send(attribute) }
                let!(:new_attribute_val) { instance.send(attribute).next }

                it 'moves the record to a new cache key' do
                  expect { instance.update(attribute => new_attribute_val) }
                    .to change { described_class.cache_get(old_attribute_val) }.to(nil)

                  expect { described_class.lookup({ attribute => instance.send(attribute) }) }
                    .to change { described_class.cache_get(new_attribute_val) }.to(instance)
                end
              end
            end

            context 'after it has been destroyed' do
              it 'returns nil' do
                expect { instance.destroy }
                  .to change { described_class.cache_get(instance.send(attribute)) }
                  .to(nil)
              end
            end
          end
        end
      end
    end

    described_class.lookup_keys.each do |key|
      include_examples 'per-attribute examples', key
    end
  end
end
