require 'active_model'
require 'devise'
require 'devise_zxcvbn'
require 'devise_zxcvbn/model'

describe Devise::Models::Zxcvbnable do
  ValidDummyClass = Struct.new(:password, :skip_password_complexity, keyword_init: true) do
    include ActiveModel::Validations
    include Devise::Models::Zxcvbnable

    def skip_password_complexity?
      skip_password_complexity
    end
  end

  let(:skip_password_complexity) { false }

  describe '#password_score' do
    context 'when password is strong' do
      let(:user) { ValidDummyClass.new(password: 'Jm1C4C3aaDzC1aRW', skip_password_complexity: skip_password_complexity) }

      it 'returns the score equal 4' do
        password_score = user.password_score

        expect(password_score.score).to eq(4)
        expect(password_score.crack_times_display['offline_fast_hashing_1e10_per_second']).to eq('12 days')
      end
    end

    context 'when password is weak' do
      let(:user) { ValidDummyClass.new(password: '12345678', skip_password_complexity: skip_password_complexity) }

      it 'returns the weak score' do
        password_score = user.password_score

        expect(password_score.score).to eq(0)
        expect(password_score.crack_times_display['offline_fast_hashing_1e10_per_second']).to eq('less than a second')
      end
    end
  end

  describe '#password_weak?' do
    let(:user) { ValidDummyClass.new(password: 'Jm1C4C3aaDzC1aRW', skip_password_complexity: skip_password_complexity) }

    it 'returns false for the call of the method' do
      expect(user.password_weak?).to be_falsey
    end
  end

  describe 'validations' do
    subject { resource.validate; resource }

    let(:resource) { ValidDummyClass.new(password: password, skip_password_complexity: skip_password_complexity) }

    context 'when password complexity check is required' do
      context 'when password is strong' do
        let(:password) { 'Jm1C4C3aaDzC1aRW' }

        it 'expects the model to be valid' do
          expect(subject).to be_valid
        end

        it 'returns empty validation messages' do
          expect(subject.errors[:password]).to be_empty
        end
      end

      context 'when password is weak' do
        let(:password) { '12345678' }

        it 'expects the model to be invalid' do
          expect(subject).to be_invalid
        end

        it 'returns validation message' do
          expect(subject.errors[:password])
            .to eq(['not strong enough. It scored 0. It must score at least 4.'])
        end
      end
    end

    context 'when password complexity check is not required' do
      let(:skip_password_complexity) { true }

      context 'when password score is strong' do
        let(:password) { 'Jm1C4C3aaDzC1aRW' }

        it 'expects the model to be valid' do
          expect(subject).to be_valid
        end
      end

      context 'when password score is weak' do
        let(:password) { '12345678' }

        it 'expects the model to be valid' do
          expect(subject).to be_valid
        end
      end
    end
  end

  describe 'exceptions raises' do
    context 'when password method is not given for instance' do
      class InvalidPasswordDummyClass < ValidDummyClass
        undef_method :password
      end

      let(:user) { InvalidPasswordDummyClass.new }

      it 'raises exception regarding absence password method' do
        expect { user.password_score }.to raise_error(DeviseZxcvbnError, 'the object must respond to password')
      end
    end

    context 'when weak_words method returns not Array' do
      class InvalidWeakWordsDummyClass < ValidDummyClass
        def weak_words
          String.new()
        end
      end

      let(:user) { InvalidWeakWordsDummyClass.new }

      it 'raises exception regarding type of weak_words method' do
        expect { user.password_score }.to raise_error(DeviseZxcvbnError, 'weak_words must return an Array')
      end
    end
  end
end
