require 'spec_helper'

describe EnumerateIt::Base do
  it 'creates constants for each enumeration value' do
    constants = [TestEnumeration::VALUE_1, TestEnumeration::VALUE_2, TestEnumeration::VALUE_3,
                 TestEnumeration::VALUE_4]

    constants.each.with_index(1) do |constant, index|
      expect(constant).to eq(index.to_s)
    end
  end

  it 'creates constants for camel case values' do
    expect(TestEnumerationWithCamelCase::IPHONE).to eq('iPhone')
  end

  it 'creates constants replacing its dashes with underscores' do
    expect(TestEnumerationWithDash::PT_BR).to eq('pt-BR')
  end

  it 'creates constants for values with spaces' do
    expect(TestEnumerationWithSpaces::SPA_CES).to eq('spa ces')
  end

  describe '.list' do
    it "creates a method that returns the allowed values in the enumeration's class" do
      expect(TestEnumeration.list).to eq(%w[1 2 3 4])
    end

    context 'specifying a default sort mode' do
      subject { create_enumeration_class_with_sort_mode(sort_mode).list }

      context 'by value' do
        let(:sort_mode) { :value }

        it { is_expected.to eq(%w[0 1 2 3 4]) }
      end

      context 'by name' do
        let(:sort_mode) { :name }

        it { is_expected.to eq(%w[2 4 1 3 0]) }
      end

      context 'by translation' do
        let(:sort_mode) { :translation }

        it { is_expected.to eq(%w[3 2 0 1 4]) }
      end

      context 'by normalize translation' do
        let(:sort_mode) { :normalize_translation }

        it { is_expected.to eq(%w[3 4 2 0 1]) }
      end

      context 'by nothing' do
        let(:sort_mode) { :none }

        it { is_expected.to eq(%w[1 2 3 4 0]) }
      end
    end
  end

  it 'creates a method that returns the enumeration specification' do
    expect(TestEnumeration.enumeration).to eq(
      value_1: ['1', 'Hey, I am 1!'], value_2: ['2', 'Hey, I am 2!'],
      value_3: ['3', 'Hey, I am 3!'], value_4: ['4', 'Héy, I ãm 2!']
    )
  end

  describe '.length' do
    it 'returns the length of the enumeration' do
      expect(TestEnumeration.length).to eq(4)
    end
  end

  describe '.each_translation' do
    it "yields each enumeration's value translation" do
      translations = []
      TestEnumeration.each_translation do |translation|
        translations << translation
      end
      expect(translations).to eq(['Hey, I am 1!', 'Hey, I am 2!', 'Hey, I am 3!', 'Héy, I ãm 2!'])
    end
  end

  describe '.translations' do
    it 'returns all translations' do
      expect(TestEnumeration.translations).to eq(['Hey, I am 1!', 'Hey, I am 2!', 'Hey, I am 3!',
                                                  'Héy, I ãm 2!'])
    end
  end

  describe '.each_key' do
    it "yields each enumeration's key" do
      keys = []
      TestEnumeration.each_key do |key|
        keys << key
      end
      expect(keys).to eq(%i[value_1 value_2 value_3 value_4])
    end
  end

  describe '.each_value' do
    it "yields each enumeration's value" do
      values = []
      TestEnumeration.each_value do |value|
        values << value
      end
      expect(values).to eq(TestEnumeration.list)
    end
  end

  describe '.to_a' do
    it 'returns an array with the values and human representations' do
      expect(TestEnumeration.to_a)
        .to eq([['Hey, I am 1!', '1'], ['Hey, I am 2!', '2'], ['Hey, I am 3!', '3'],
                ['Héy, I ãm 2!', '4']])
    end

    it 'translates the available values' do
      I18n.locale = :en
      expect(TestEnumerationWithoutArray.to_a).to eq([['First Value', '1'], ['Value Two', '2']])
      I18n.locale = :pt
      expect(TestEnumerationWithoutArray.to_a).to eq([['Primeiro Valor', '1'], ['Value Two', '2']])
    end

    it 'can be extended from the enumeration class' do
      expect(TestEnumerationWithExtendedBehaviour.to_a).to eq([%w[Second 2], %w[First 1]])
    end
  end

  describe '.to_h' do
    it 'returns a hash' do
      expect(TestEnumerationWithoutArray.to_h).to eq(value_one: '1', value_two: '2')
    end
  end

  describe '.to_json' do
    it 'gives a valid json back' do
      I18n.locale = :inexsistent
      expect(TestEnumerationWithoutArray.to_json)
        .to eq('[{"value":"1","label":"Value One"},{"value":"2","label":"Value Two"}]')
    end

    it 'give translated values when available' do
      I18n.locale = :pt
      expect(TestEnumerationWithoutArray.to_json)
        .to eq('[{"value":"1","label":"Primeiro Valor"},{"value":"2","label":"Value Two"}]')
    end
  end

  describe '.t' do
    it 'translates a given value' do
      I18n.locale = :pt
      expect(TestEnumerationWithoutArray.t('1')).to eq('Primeiro Valor')
    end
  end

  describe '.to_range' do
    it "returns a Range object containing the enumeration's value interval" do
      expect(TestEnumeration.to_range).to eq('1'..'4')
    end
  end

  describe '.values_for' do
    it "returns an array representing some of the enumeration's values" do
      expect(TestEnumeration.values_for(%w[VALUE_1 VALUE_2]))
        .to eq([TestEnumeration::VALUE_1, TestEnumeration::VALUE_2])
    end

    it 'returns nil if the a constant named after one of the given strings cannot be found' do
      expect(TestEnumeration.values_for(%w[VALUE_1 THIS_IS_WRONG]))
        .to eq([TestEnumeration::VALUE_1, nil])
    end
  end

  describe '.value_for' do
    it "returns the enumeration's value" do
      expect(TestEnumeration.value_for('VALUE_1')).to eq(TestEnumeration::VALUE_1)
    end

    context 'when a constant named after the received value cannot be found' do
      it 'returns nil' do
        expect(TestEnumeration.value_for('THIS_IS_WRONG')).to be_nil
      end
    end

    context 'when a constant named after the received value exists as an ancestor' do
      it 'returns nil' do
        expect(TestEnumeration.value_for('Module')).to be_nil
      end
    end
  end

  describe '.value_from_key' do
    it 'returns the correct value when the key is a string' do
      expect(TestEnumeration.value_from_key('value_1')).to eq(TestEnumeration::VALUE_1)
    end

    it 'returns the correct value when the key is a symbol' do
      expect(TestEnumeration.value_from_key(:value_1)).to eq(TestEnumeration::VALUE_1)
    end

    it 'returns nil when the key does not exist in the enumeration' do
      expect(TestEnumeration.value_from_key('wrong')).to be_nil
    end

    it 'returns nil when the given value is nil' do
      expect(TestEnumeration.value_from_key(nil)).to be_nil
    end
  end

  describe '.keys' do
    it 'returns a list with the keys used to define the enumeration' do
      expect(TestEnumeration.keys).to eq(%i[value_1 value_2 value_3 value_4])
    end
  end

  describe '.key_for' do
    it 'returns the key for the given value inside the enumeration' do
      expect(TestEnumeration.key_for(TestEnumeration::VALUE_1)).to eq(:value_1)
    end

    it 'returns nil if the enumeration does not have the given value' do
      expect(TestEnumeration.key_for('foo')).to be_nil
    end
  end

  context 'associate values with a list' do
    it 'creates constants for each enumeration value' do
      expect(TestEnumerationWithList::FIRST).to  eq('first')
      expect(TestEnumerationWithList::SECOND).to eq('second')
    end

    it 'returns an array with the values and human representations' do
      expect(TestEnumerationWithList.to_a).to eq([%w[First first], %w[Second second]])
    end
  end

  context 'not specifying a sort mode' do
    subject(:enumeration) { create_enumeration_class_with_sort_mode(nil) }

    it 'does not sort' do
      expect(enumeration.to_a).to eq([%w[xyz 1], %w[fgh 2], %w[abc 3], %w[ábc 4], %w[jkl 0]])
    end
  end

  context 'specifying a sort mode' do
    subject(:enumeration) { create_enumeration_class_with_sort_mode(sort_mode) }

    context 'by value' do
      let(:sort_mode) { :value }

      it { expect(enumeration.to_a).to eq([%w[jkl 0], %w[xyz 1], %w[fgh 2], %w[abc 3], %w[ábc 4]]) }
    end

    context 'by name' do
      let(:sort_mode) { :name }

      it { expect(enumeration.to_a).to eq([%w[fgh 2], %w[ábc 4], %w[xyz 1], %w[abc 3], %w[jkl 0]]) }
    end

    context 'by translation' do
      let(:sort_mode) { :translation }

      it { expect(enumeration.to_a).to eq([%w[abc 3], %w[fgh 2], %w[jkl 0], %w[xyz 1], %w[ábc 4]]) }
    end

    context 'by normalize translation' do
      let(:sort_mode) { :normalize_translation }

      it { expect(enumeration.to_a).to eq([%w[abc 3], %w[ábc 4], %w[fgh 2], %w[jkl 0], %w[xyz 1]]) }
    end

    context 'by nothing' do
      let(:sort_mode) { :none }

      it { expect(enumeration.to_a).to eq([%w[xyz 1], %w[fgh 2], %w[abc 3], %w[ábc 4], %w[jkl 0]]) }
    end
  end

  context 'when included in ActiveRecord::Base' do
    let(:active_record_stub_class) do
      Class.new do
        extend EnumerateIt

        attr_accessor :bla

        class << self
          def validates_inclusion_of(_attribute, _options) # rubocop:disable Naming/PredicateMethod
            true
          end

          def validates_presence_of # rubocop:disable Naming/PredicateMethod
            true
          end
        end
      end
    end

    it 'creates a validation for inclusion' do
      expect(active_record_stub_class)
        .to receive(:validates_inclusion_of).with(:bla, in: TestEnumeration.list, allow_blank: true)

      active_record_stub_class.class_eval do
        has_enumeration_for :bla, with: TestEnumeration
      end
    end

    context 'using the :required option' do
      before do
        allow(active_record_stub_class).to receive(:validates_presence_of).and_return(true)
      end

      it 'creates a validation for presence' do
        expect(active_record_stub_class).to receive(:validates_presence_of)
        active_record_stub_class.class_eval do
          has_enumeration_for :bla, with: TestEnumeration, required: true
        end
      end

      it 'passes the given options to the validation method' do
        active_record_stub_class.class_eval do
          has_enumeration_for :bla, with: TestEnumeration, required: { if: :some_method }
        end

        expect(active_record_stub_class)
          .to have_received(:validates_presence_of).with(:bla, if: :some_method)
      end

      it 'does not require the attribute by default' do
        expect(active_record_stub_class).not_to receive(:validates_presence_of)
        active_record_stub_class.class_eval do
          has_enumeration_for :bla, with: TestEnumeration
        end
      end
    end

    context 'using :skip_validation option' do
      it "doesn't create a validation for inclusion" do
        expect(active_record_stub_class).not_to receive(:validates_inclusion_of)
        active_record_stub_class.class_eval do
          has_enumeration_for :bla, with: TestEnumeration, skip_validation: true
        end
      end

      it "doesn't create a validation for presence" do
        expect(active_record_stub_class).not_to receive(:validates_presence_of)
        active_record_stub_class.class_eval do
          has_enumeration_for :bla, with: TestEnumeration, require: true, skip_validation: true
        end
      end
    end
  end
end
