require 'spec_helper'
require 'fpm/cookery/inheritable_attr'
require 'fpm/cookery/path'

def dsl_klass(name = nil)
  (klass = Class.new).send(:extend, FPM::Cookery::InheritableAttr)
  klass.class_eval(&Proc.new) if block_given?
  klass
end

shared_examples 'attribute registration' do |attr_method, registry_method|
  context "given an attribute created with .#{attr_method}" do
    it "registers the attribute in the .#{registry_method} list" do
      klass = Class.new do
        extend FPM::Cookery::InheritableAttr

        instance_eval %Q{
          #{attr_method} :example_attr
        }
      end

      expect(klass).to respond_to(registry_method)
      expect(klass.send(registry_method)).to include(:example_attr)
    end
  end
end

shared_context 'class inheritance' do
  let(:superklass) { dsl_klass }
  let(:subklass) { Class.new(superklass) }
end

shared_examples 'attribute inheritance' do |attr_method, default_value, attr_name = :example_attr|
  # A default implementation.  Useful for +.attr_rw+, but should probably be
  # overridden for the other DSL methods.
  let(:attr_setter) {
    Class.new do
      def self.call(k, m, v)
        k.send(m, v)
      end
    end
  }

  describe 'created attribute' do
    it 'is defined on class' do
      expect(superklass).to respond_to(attr_name)
    end

    it "is #{default_value.inspect} by default" do
      expect(superklass.send(attr_name)).to eq(default_value)
    end

    it 'sets the attribute' do
      attr_setter.(superklass, attr_name, value)
      expect(superklass.send(attr_name)).to eq value
    end
  end

  describe 'child class' do
    describe 'inherited attribute' do
      it 'inherits its value from the superclass' do
        attr_setter.(superklass, attr_name, value)
        expect(subklass.send(attr_name)).to eq value
      end

      context 'when altered' do
        it 'does not propagate to the superclass' do
          attr_setter.(superklass, attr_name, value)
          attr_setter.(subklass, attr_name, child_value)
          expect(superklass.send(attr_name)).to eq value
        end
      end
    end
  end
end

shared_context 'inheritable attributes' do |attr_method, default_value, attr_name = :example_attr|
  include_context 'class inheritance'
  include_examples 'attribute inheritance', attr_method, default_value

  before(:example) do
    missing = [:value, :child_value].reject { |m| respond_to?(m) }
    raise "Missing required methods: #{missing.join(', ')}" unless missing.empty?

    superklass.instance_eval do
      send(attr_method, attr_name)
    end
  end
end

describe FPM::Cookery::InheritableAttr do
  describe '.register_attrs' do
    describe '.attr_rw' do
      include_examples 'attribute registration', 'attr_rw', 'scalar_attrs'
    end

    describe '.attr_rw_list' do
      include_examples 'attribute registration', 'attr_rw_list', 'list_attrs'
    end

    describe '.attr_rw_hash' do
      include_examples 'attribute registration', 'attr_rw_hash', 'hash_attrs'
    end

    describe '.attr_rw_path' do
      include_examples 'attribute registration', 'attr_rw_path', 'path_attrs'
    end
  end

  describe '.attr_rw' do
    include_context 'inheritable attributes', 'attr_rw', nil do
      let(:value) { 'that' }
      let(:child_value) { 'the other' }
    end
  end

  describe '.attr_rw_list' do
    include_context 'inheritable attributes', 'attr_rw_list', [] do
      let(:attr_setter) {
        Class.new do
          def self.call(k, m, v)
            k.send(m, *v)
          end
        end
      }

      let(:value) { %w{so la ti do} }
      let(:child_value) { %w{fee fi fo fum} }
    end
  end

  describe '.attr_rw_hash' do
    include_context 'inheritable attributes', 'attr_rw_hash', {} do
      let(:value) { {:name => 'mike', :rank => 'colonel' } }
      let(:child_value) { {:name => 'mike', :favorite_color => 'puce' } }
    end

    describe 'created attribute' do
      before do
        superklass.instance_eval do
          attr_rw_hash :metadata
        end

        superklass.metadata({ :radius => 4.172, :weight => 4 })
      end

      it 'merges its argument into the existing attribute value' do
        superklass.metadata({ :weight => 7, :height => 12.3 })
        expect(superklass.metadata).to eq({
          :height => 12.3,
          :weight => 7,
          :radius => 4.172
        })
      end

      it 'supports []= assignment' do
        expect { superklass.metadata[:age] = 10000 }.not_to raise_error
        expect(superklass.metadata).to include(:age => 10000)
      end

      context 'from child class' do
        it 'does not propagate changes to the parent class' do
          superklass.metadata[:tags] = %w{a b c}
          subklass.metadata[:tags] << 'd'
          expect(superklass.metadata[:tags]).not_to include('d')
        end
      end
    end
  end

  describe '.attr_rw_path' do
    include_context 'inheritable attributes', 'attr_rw_path', nil do
      let(:attr_setter) {
        Class.new do
          def self.call(k, m, v)
            k.send(:"#{m}=", v)
          end
        end
      }

      let(:value) { FPM::Cookery::Path.new('/var/spool') }
      let(:child_value) { FPM::Cookery::Path.new('/proc/self') }
    end

    it 'returns an FPM::Cookery::Path object' do
      superklass.attr_rw_path :home
      superklass.home = "/where/the/heart/is"
      expect(superklass.home).to be_an(FPM::Cookery::Path)
    end
  end

  describe '.inherit_for' do
    include_context 'class inheritance'

    context 'given a method name that the superclass does not respond to' do
      it 'returns nil' do
        expect(superklass).not_to respond_to(:blech)
        expect(FPM::Cookery::InheritableAttr.inherit_for(subklass, :blech)).to be nil
      end
    end
  end
end
