require 'rails_helper'

RSpec.describe Idv::PersonalKeyController do
  include FlowPolicyHelper
  include SamlAuthHelper
  include PersonalKeyValidator

  def assert_personal_key_generated_for_profiles(*profile_pii_pairs)
    expect(idv_session.personal_key).to be_present

    normalized_personal_key = normalize_personal_key(idv_session.personal_key)

    # These keys are present in our applicant fixture but
    # are not actually supported in Pii::Attributes
    keys_to_ignore = %i[
      name_suffix
      sex
      height
      weight
      eye_color
      state_id_expiration
      state_id_issued
      state_id_number
      document_type_received
    ]

    profile_pii_pairs.each do |profile, pii|
      expected = Pii::Attributes.new(pii.except(*keys_to_ignore))
      actual = profile.reload.recover_pii(normalized_personal_key)
      expect(actual).to eql(expected)
    end
  end

  let(:applicant) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE }
  let(:password) { 'sekrit phrase' }
  let(:user) { create(:user, :fully_registered, password: password) }
  let(:is_enhanced_ipp) { false }

  # Most (but not all) of these tests assume that a profile has been minted
  # from the data in idv_session. Set this to false to prevent this behavior
  # and test the other way.
  # (idv_session.profile will be nil if the user is coming back to complete
  # the IdV flow out-of-band, like with GPO.)
  let(:mint_profile_from_idv_session) { true }

  let(:address_verification_mechanism) { 'phone' }

  let(:idv_session) { subject.idv_session }

  let(:threatmetrix_review_status) { nil }

  let(:pii_cacher) { Pii::Cacher.new(user, controller.user_session) }

  before do
    stub_analytics

    stub_sign_in(user)

    case address_verification_mechanism
    when 'phone'
      stub_up_to(:otp_verification, idv_session: idv_session)
    when 'gpo'
      stub_up_to(:request_letter, idv_session: idv_session)
      idv_session.gpo_code_verified = true
    when nil
      stub_up_to(:verify_info, idv_session: idv_session)
    else
      raise 'invalid address_verification_mechanism'
    end

    idv_session.applicant = applicant

    if mint_profile_from_idv_session
      idv_session.create_profile_from_applicant_with_password(
        password,
        is_enhanced_ipp:,
        proofing_components: {},
      )
    end
  end

  describe '#step_info' do
    let(:step_info) do
      controller.class.step_info
    end

    describe '#undo_step' do
      it 'clears personal_key_acknowledged' do
        idv_session.acknowledge_personal_key!
        step_info.undo_step.call(idv_session: idv_session, user: user)
        expect(idv_session.personal_key_acknowledged).to eql(nil)
      end

      it 'clears personal_key' do
        idv_session.personal_key = 'ABCD-1234'
        step_info.undo_step.call(idv_session: idv_session, user: user)
        expect(idv_session.personal_key).to be_nil
      end
    end

    describe '#preconditions' do
      let(:preconditions) do
        step_info.preconditions.call(idv_session: idv_session, user: user)
      end

      context 'when all conditions met' do
        it 'returns a truthy result' do
          expect(preconditions).to be_truthy
        end
      end

      context 'when user does not have a pending or active profile' do
        before do
          user.active_profile.deactivate(:password_reset)
          expect(user.active_profile).to eql(nil)
          user.reload
        end

        it 'returns something falsey' do
          expect(preconditions).to be_falsey
        end
      end

      context 'when address confirmed via GPO' do
        let(:address_verification_mechanism) { 'gpo' }
        it 'returns a truthy result' do
          expect(preconditions).to be_truthy
        end
      end

      context 'when address confirmed via phone' do
        let(:address_verification_mechanism) { 'phone' }
        it 'returns a truthy result' do
          expect(preconditions).to be_truthy
        end
      end

      context 'when address unconfirmed' do
        let(:address_verification_mechanism) { nil }
        it 'returns a falsey result' do
          expect(preconditions).to be_falsey
        end
      end

      context 'when personal_key_acknowledged is false' do
        before do
          idv_session.personal_key_acknowledged = false
        end
        it 'returns a truthy result' do
          expect(preconditions).to be_truthy
        end
      end

      context 'when personal_key_acknowledged is true' do
        before do
          idv_session.personal_key_acknowledged = true
        end
        it 'returns a falsey result' do
          expect(preconditions).to be_falsey
        end
      end

      context 'when personal_key_acknowledged is nil' do
        before do
          idv_session.personal_key_acknowledged = nil
        end
        it 'returns a truthy result' do
          expect(preconditions).to be_truthy
        end
      end
    end
  end

  describe 'before_actions' do
    it 'includes before_actions' do
      expect(subject).to have_actions(
        :before,
        :confirm_two_factor_authenticated,
        :confirm_step_allowed,
      )
    end

    it 'skips redundant or irrelevant before_actions' do
      expect(subject).not_to have_actions(
        :before,
        :confirm_idv_needed,
        :confirm_personal_key_acknowledged_if_needed,
        :confirm_no_pending_in_person_enrollment,
        :confirm_no_pending_profile,
      )
    end

    it 'includes before_actions from IdvSessionConcern' do
      expect(subject).to have_actions(
        :before,
        :redirect_unless_sp_requested_verification,
      )
    end
  end

  describe '#show' do
    context 'when we have no personal key or encrypted profiles in the session' do
      it 'redirects to get the users password and fetch the PII' do
        controller.idv_session.personal_key = nil
        controller.user_session[:encrypted_profiles] = nil

        response = get :show

        expect(controller.user_session[:stored_location]).to eq(idv_personal_key_path)
        expect(response).to redirect_to(capture_password_path)
      end
    end

    context 'profile has been created from idv_session' do
      it 'does not redirect' do
        get :show

        expect(response).to_not be_redirect
      end

      context 'profile is pending fraud review' do
        let(:threatmetrix_review_status) { 'reject' }
        it 'does not redirect' do
          get :show
          expect(response).to_not be_redirect
        end
      end
    end

    context 'profile has not been created from idv_session' do
      let(:mint_profile_from_idv_session) { false }

      it 'redirects to the enter password screen' do
        get :show
        expect(response).to redirect_to idv_enter_password_url
      end

      context 'but a profile is pending from a different session' do
        before { pii_cacher.save(password, pending_profile) }

        context 'due to fraud review' do
          let!(:pending_profile) do
            create(
              :profile,
              :fraud_review_pending,
              :with_pii,
              user: user,
            )
          end

          it 'does not redirect' do
            get :show
            expect(response).not_to be_redirect
          end
        end

        context 'due to in person proofing' do
          let!(:pending_profile) do
            create(
              :profile,
              :in_person_verification_pending,
              :with_pii,
              user: user,
            )
          end

          it 'does not redirect' do
            get :show
            expect(response).to_not be_redirect
          end
        end
      end
    end

    it 'sets code instance variable' do
      code = idv_session.personal_key
      expect(code).to be_present

      get :show

      expect(assigns(:code)).to eq(code)
    end

    it 'shows the same personal key when page is refreshed' do
      code = idv_session.personal_key
      expect(code).to be_present

      get :show
      get :show

      expect(assigns(:code)).to eq(code)
    end

    it 'can decrypt the profile with the code' do
      get :show

      code = assigns(:code)

      expect(PersonalKeyGenerator.new(user).verify(code)).to eq true
      expect(idv_session.profile.recover_pii(normalize_personal_key(code))).to eq(
        Pii::Attributes.new_from_hash(applicant),
      )
    end

    context 'user selected gpo verification' do
      let(:address_verification_mechanism) { 'gpo' }

      it 'redirects to letter enqueued url' do
        get :show

        expect(response).to redirect_to idv_letter_enqueued_url
      end
    end

    context 'no personal key generated yet' do
      before do
        idv_session.personal_key = nil
      end

      it 'generates a personal key that encrypts the idv_session profile data' do
        get :show
        assert_personal_key_generated_for_profiles([idv_session.profile, applicant])
      end

      context 'user has an existing profile in addition to the one attached to idv_session' do
        let(:existing_profile_pii) { idv_session.applicant.merge(first_name: 'Existing') }
        let!(:existing_profile) do
          create(
            :profile,
            :verify_by_mail_pending,
            user: user,
            pii: existing_profile_pii,
          )
        end

        before do
          Pii::Cacher.new(user, subject.user_session).save_decrypted_pii(
            existing_profile_pii,
            existing_profile.id,
          )
        end

        it 'generates a personal key that encrypts the idv_session and existing profile data' do
          expect(user.profiles).to include(existing_profile)
          expect(user.profiles).to include(idv_session.profile)
          get :show
          assert_personal_key_generated_for_profiles(
            [idv_session.profile, idv_session.applicant],
            [existing_profile, existing_profile_pii],
          )
        end
      end

      context 'no profile attached to idv_session' do
        let(:mint_profile_from_idv_session) { false }

        context 'user has a pending profile' do
          let!(:pending_profile_pii) { applicant.merge(first_name: 'Pending') }
          let!(:pending_profile) do
            create(
              :profile,
              :verify_by_mail_pending,
              user: user,
              pii: pending_profile_pii,
            )
          end

          before do
            Pii::Cacher.new(user, subject.user_session).save_decrypted_pii(
              pending_profile_pii,
              pending_profile.id,
            )
          end

          it 'generates a personal key that encrypts the pending profile data' do
            get :show
            assert_personal_key_generated_for_profiles([pending_profile, pending_profile_pii])
          end

          context 'and user has an active profile' do
            let(:active_profile_pii) { applicant.merge(first_name: 'Active') }
            let!(:active_profile) do
              create(
                :profile,
                :active,
                user: user,
                pii: active_profile_pii,
              )
            end

            before do
              Pii::Cacher.new(user, subject.user_session).save_decrypted_pii(
                active_profile_pii,
                active_profile.id,
              )
            end

            it 'generates a personal key that encrypts both profiles' do
              get :show
              assert_personal_key_generated_for_profiles(
                [active_profile, active_profile_pii],
                [pending_profile, pending_profile_pii],
              )
            end
          end
        end
      end
    end

    context 'personal key already acknowledged' do
      before do
        idv_session.acknowledge_personal_key!
      end
      it 'redirects away' do
        get :show
        expect(response).to be_redirect
      end
    end
  end

  describe '#update' do
    context 'user selected phone verification' do
      context 'with an sp' do
        let(:acr_values) { Saml::Idp::Constants::AAL1_AUTHN_CONTEXT_CLASSREF }
        let(:vtr) { nil }

        before do
          subject.session[:sp] = {
            issuer: create(:service_provider).issuer,
            acr_values:,
            vtr:,
          }
        end

        it 'redirects to sign up completed for the sp' do
          patch :update

          expect(response).to redirect_to sign_up_completed_url
        end

        context 'with vtr values' do
          let(:acr_values) { nil }
          let(:vtr) { ['C1'] }

          it 'redirects to sign up completed for the sp' do
            patch :update

            expect(response).to redirect_to sign_up_completed_url
          end
        end
      end

      it 'redirects to the account path when no sp present' do
        patch :update

        expect(response).to redirect_to account_path
      end

      it 'sets idv_session.personal_key_acknowledged' do
        expect { patch :update }.to change {
                                      idv_session.personal_key_acknowledged
                                    }.from(nil).to eql(true)
      end

      it 'logs key submitted event' do
        patch :update

        expect(@analytics).to have_logged_event(
          'IdV: personal key submitted',
          address_verification_method: 'phone',
          fraud_review_pending: false,
          fraud_rejection: false,
          in_person_verification_pending: false,
        )
      end
    end

    context 'user selected gpo verification' do
      let(:address_verification_mechanism) { 'gpo' }

      context 'when the user requested a letter this session' do
        it 'redirects to correct url' do
          patch :update
          expect(response).to redirect_to idv_letter_enqueued_url
        end

        it 'does not log any events' do
          expect(@analytics).not_to have_logged_event
          patch :update
        end
      end

      context 'when the user entered a GPO code' do
        before do
          pending_profile = user.pending_profile
          pending_profile.remove_gpo_deactivation_reason
          pending_profile.activate
        end

        it 'redirects to correct url' do
          patch :update
          expect(response).to redirect_to idv_sp_follow_up_url
        end

        it 'logs analytics' do
          patch :update

          expect(@analytics).to have_logged_event(
            'IdV: personal key submitted',
            address_verification_method: 'gpo',
            fraud_review_pending: false,
            fraud_rejection: false,
            in_person_verification_pending: false,
          )
        end
      end
    end

    context 'with in person profile' do
      let!(:profile) { create(:profile, :in_person_verification_pending, user: user) }

      before do
        user.reload_pending_in_person_enrollment
        allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
      end

      it 'creates a profile and returns completion url' do
        patch :update

        expect(response).to redirect_to idv_in_person_ready_to_verify_url
      end

      it 'logs key submitted event' do
        patch :update

        expect(@analytics).to have_logged_event(
          'IdV: personal key submitted',
          address_verification_method: 'phone',
          fraud_review_pending: false,
          fraud_rejection: false,
          in_person_verification_pending: false,
        )
      end
    end

    context 'with device profiling decisioning enabled' do
      before do
        allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled)
      end

      context 'fraud_review_pending_at is nil' do
        it 'redirects to account path' do
          patch :update

          expect(idv_session.profile.fraud_review_pending_at).to eq nil
          expect(response).to redirect_to account_path
        end

        it 'logs key submitted event' do
          patch :update

          expect(@analytics).to have_logged_event(
            'IdV: personal key submitted',
            address_verification_method: 'phone',
            fraud_review_pending: false,
            fraud_rejection: false,
            in_person_verification_pending: false,
          )
        end
      end

      context 'profile is in fraud_review' do
        before do
          idv_session.profile.fraud_pending_reason = 'threatmetrix_review'
          idv_session.profile.deactivate_for_fraud_review
        end

        it 'redirects to idv please call path' do
          patch :update
          expect(idv_session.profile.fraud_review_pending_at).to_not eq nil
          expect(response).to redirect_to idv_please_call_path
        end

        it 'logs key submitted event' do
          patch :update

          expect(@analytics).to have_logged_event(
            'IdV: personal key submitted',
            fraud_review_pending: true,
            fraud_rejection: false,
            address_verification_method: 'phone',
            in_person_verification_pending: false,
          )
        end
      end
    end

    context 'with one account enabled for service provider' do
      context 'with duplicate profile set found for user' do
        before do
          allow(controller)
            .to receive(:profile_creation_duplicate_profile_detected?)
            .and_return(true)
        end
        it 'redirects to duplicate profile detected page' do
          patch :update
          expect(response)
            .to redirect_to(duplicate_profiles_detected_url(source: :account_verified))
        end
      end

      context 'with duplicate profile set not found for user' do
        before do
          allow(controller)
            .to receive(:profile_creation_duplicate_profile_detected?).and_return(false)
        end
        it 'redirects to account path' do
          patch :update
          expect(response).to redirect_to(account_path)
        end
      end
    end
  end
end
