# frozen_string_literal: true

RSpec.describe EmailUpdater do
  let(:old_email) { "old.email@example.com" }
  let(:new_email) { "new.email@example.com" }

  it "provides better error message when a staged user has the same email" do
    Fabricate(:user, staged: true, email: new_email)

    user = Fabricate(:user, email: old_email)
    updater = EmailUpdater.new(guardian: user.guardian, user: user)
    updater.change_to(new_email)

    expect(updater.errors).to be_present
    expect(updater.errors.messages[:base].first).to be I18n.t("change_email.error_staged")
  end

  it "does not create multiple email change requests" do
    user = Fabricate(:user)

    EmailUpdater.new(guardian: Fabricate(:admin).guardian, user: user).change_to(new_email)
    EmailUpdater.new(guardian: Fabricate(:admin).guardian, user: user).change_to(new_email)

    expect(user.email_change_requests.count).to eq(1)
  end

  context "when an admin is changing the email of another user" do
    let(:admin) { Fabricate(:admin) }
    let(:updater) { EmailUpdater.new(guardian: admin.guardian, user: user) }

    def expect_old_email_job
      expect_enqueued_with(
        job: :critical_user_email,
        args: {
          to_address: old_email,
          type: :notify_old_email,
          user_id: user.id,
        },
      ) { yield }
    end

    context "for a regular user" do
      let(:user) { Fabricate(:user, email: old_email) }

      it "sends an email to the user for them to confirm the email change" do
        expect_enqueued_with(
          job: :critical_user_email,
          args: {
            type: :confirm_new_email,
            to_address: new_email,
          },
        ) { updater.change_to(new_email) }
      end

      it "sends an email to confirm old email first if require_change_email_confirmation is enabled" do
        SiteSetting.require_change_email_confirmation = true

        expect_enqueued_with(
          job: :critical_user_email,
          args: {
            type: :confirm_old_email,
            to_address: old_email,
          },
        ) { updater.change_to(new_email) }

        expect(updater.change_req).to be_present
        expect(updater.change_req.old_email).to eq(old_email)
        expect(updater.change_req.new_email).to eq(new_email)
        expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old])
        expect(updater.change_req.old_email_token.email).to eq(old_email)
        expect(updater.change_req.new_email_token).to be_blank
      end

      it "logs the admin user as the requester" do
        updater.change_to(new_email)
        expect(updater.change_req.requested_by).to eq(admin)
      end

      it "starts the new confirmation process" do
        updater.change_to(new_email)
        expect(updater.errors).to be_blank

        expect(updater.change_req).to be_present
        expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])

        expect(updater.change_req.old_email).to eq(old_email)
        expect(updater.change_req.new_email).to eq(new_email)
        expect(updater.change_req.old_email_token).to be_blank
        expect(updater.change_req.new_email_token.email).to eq(new_email)
      end
    end

    context "for a staff user" do
      let(:user) { Fabricate(:moderator, email: old_email) }

      before do
        expect_enqueued_with(
          job: :critical_user_email,
          args: {
            type: :confirm_old_email,
            to_address: old_email,
          },
        ) { updater.change_to(new_email) }
      end

      it "starts the old confirmation process" do
        expect(updater.errors).to be_blank

        expect(updater.change_req.old_email).to eq(old_email)
        expect(updater.change_req.new_email).to eq(new_email)
        expect(updater.change_req).to be_present
        expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old])

        expect(updater.change_req.old_email_token.email).to eq(old_email)
        expect(updater.change_req.new_email_token).to be_blank
      end

      it "does not immediately confirm the request" do
        expect(updater.change_req.change_state).not_to eq(EmailChangeRequest.states[:complete])
      end
    end

    context "when changing their own email" do
      let(:user) { admin }

      before do
        admin.update(email: old_email)

        expect_enqueued_with(
          job: :critical_user_email,
          args: {
            type: :confirm_old_email,
            to_address: old_email,
          },
        ) { updater.change_to(new_email) }
      end

      it "logs the user as the requester" do
        updater.change_to(new_email)
        expect(updater.change_req.requested_by).to eq(user)
      end

      it "starts the old confirmation process" do
        expect(updater.errors).to be_blank

        expect(updater.change_req.old_email).to eq(old_email)
        expect(updater.change_req.new_email).to eq(new_email)
        expect(updater.change_req).to be_present
        expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old])

        expect(updater.change_req.old_email_token.email).to eq(old_email)
        expect(updater.change_req.new_email_token).to be_blank
      end

      it "does not immediately confirm the request" do
        expect(updater.change_req.change_state).not_to eq(EmailChangeRequest.states[:complete])
      end
    end
  end

  context "as a regular user" do
    let(:user) { Fabricate(:user, email: old_email) }
    let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) }

    context "when changing primary email" do
      before do
        expect_enqueued_with(
          job: :critical_user_email,
          args: {
            type: :confirm_new_email,
            to_address: new_email,
          },
        ) { updater.change_to(new_email) }
      end

      it "starts the new confirmation process" do
        expect(updater.errors).to be_blank

        expect(updater.change_req).to be_present
        expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])

        expect(updater.change_req.old_email).to eq(old_email)
        expect(updater.change_req.new_email).to eq(new_email)
        expect(updater.change_req.old_email_token).to be_blank
        expect(updater.change_req.new_email_token.email).to eq(new_email)
      end

      context "when confirming an invalid token" do
        it "produces an error" do
          updater.confirm("random")
          expect(updater.errors).to be_present
          expect(user.reload.email).not_to eq(new_email)
        end
      end

      context "when confirming a valid token" do
        it "updates the user's email" do
          event =
            DiscourseEvent
              .track_events do
                expect_enqueued_with(
                  job: :critical_user_email,
                  args: {
                    type: :notify_old_email,
                    to_address: old_email,
                  },
                ) { updater.confirm(updater.change_req.new_email_token.token) }
              end
              .last

          expect(updater.errors).to be_blank
          expect(user.reload.email).to eq(new_email)

          expect(event[:event_name]).to eq(:user_updated)
          expect(event[:params].first).to eq(user)

          updater.change_req.reload
          expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:complete])
        end
      end
    end

    context "when adding an email" do
      before do
        expect_enqueued_with(
          job: :critical_user_email,
          args: {
            type: :confirm_new_email,
            to_address: new_email,
          },
        ) { updater.change_to(new_email, add: true) }
      end

      context "when confirming a valid token" do
        it "adds a user email" do
          expect(
            UserHistory.where(
              action: UserHistory.actions[:add_email],
              acting_user_id: user.id,
            ).last,
          ).to be_present

          event =
            DiscourseEvent
              .track_events do
                expect_enqueued_with(
                  job: :critical_user_email,
                  args: {
                    type: :notify_old_email_add,
                    to_address: old_email,
                  },
                ) { updater.confirm(updater.change_req.new_email_token.token) }
              end
              .last

          expect(updater.errors).to be_blank
          expect(UserEmail.where(user_id: user.id).pluck(:email)).to contain_exactly(
            user.email,
            new_email,
          )

          expect(event[:event_name]).to eq(:user_updated)
          expect(event[:params].first).to eq(user)

          updater.change_req.reload
          expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:complete])
        end
      end

      context "when it was deleted before" do
        it "works" do
          expect_enqueued_with(
            job: :critical_user_email,
            args: {
              type: :notify_old_email_add,
              to_address: old_email,
            },
          ) { updater.confirm(updater.change_req.new_email_token.token) }

          expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email)

          user.user_emails.where(email: new_email).delete_all
          expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email)

          expect_enqueued_with(
            job: :critical_user_email,
            args: {
              type: :confirm_new_email,
              to_address: new_email,
            },
          ) { updater.change_to(new_email, add: true) }

          expect_enqueued_with(
            job: :critical_user_email,
            args: {
              type: :notify_old_email_add,
              to_address: old_email,
            },
          ) { updater.confirm(updater.change_req.new_email_token.token) }

          expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email)
        end
      end
    end

    context "with max_allowed_secondary_emails" do
      let(:secondary_email_1) { "secondary_1@email.com" }
      let(:secondary_email_2) { "secondary_2@email.com" }

      before do
        SiteSetting.max_allowed_secondary_emails = 2
        Fabricate(:secondary_email, user: user, primary: false, email: secondary_email_1)
        Fabricate(:secondary_email, user: user, primary: false, email: secondary_email_2)
      end

      it "max secondary_emails limit reached" do
        updater.change_to(new_email, add: true)
        expect(updater.errors).to be_present
        expect(updater.errors.messages[:base].first).to be I18n.t(
             "change_email.max_secondary_emails_error",
           )
      end
    end
  end

  context "as a staff user" do
    let(:user) { Fabricate(:moderator, email: old_email) }
    let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) }

    before do
      expect_enqueued_with(
        job: :critical_user_email,
        args: {
          type: :confirm_old_email,
          to_address: old_email,
        },
      ) { updater.change_to(new_email) }
    end

    it "starts the old confirmation process" do
      expect(updater.errors).to be_blank

      expect(updater.change_req.old_email).to eq(old_email)
      expect(updater.change_req.new_email).to eq(new_email)
      expect(updater.change_req).to be_present
      expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_old])

      expect(updater.change_req.old_email_token.email).to eq(old_email)
      expect(updater.change_req.new_email_token).to be_blank
    end

    context "when confirming an invalid token" do
      it "produces an error" do
        updater.confirm("random")
        expect(updater.errors).to be_present
        expect(user.reload.email).not_to eq(new_email)
      end
    end

    context "when confirming a valid token" do
      before do
        expect_enqueued_with(
          job: :critical_user_email,
          args: {
            type: :confirm_new_email,
            to_address: new_email,
          },
        ) do
          @old_token = updater.change_req.old_email_token.token
          updater.confirm(@old_token)
        end
      end

      it "starts the new update process" do
        expect(updater.errors).to be_blank
        expect(user.reload.email).to eq(old_email)

        expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])
        expect(updater.change_req.new_email_token).to be_present
      end

      it "cannot be confirmed twice" do
        updater.confirm(@old_token)
        expect(updater.errors).to be_present
        expect(user.reload.email).to eq(old_email)

        updater.change_req.reload
        expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:authorizing_new])
        expect(updater.change_req.new_email_token.email).to eq(new_email)
      end

      context "when completing the new update process" do
        before do
          expect_not_enqueued_with(
            job: :critical_user_email,
            args: {
              type: :notify_old_email,
              to_address: old_email,
            },
          ) { updater.confirm(updater.change_req.new_email_token.token) }
        end

        it "updates the user's email" do
          expect(updater.errors).to be_blank
          expect(user.reload.email).to eq(new_email)

          updater.change_req.reload
          expect(updater.change_req.change_state).to eq(EmailChangeRequest.states[:complete])
        end
      end
    end
  end

  context "when hide_email_address_taken is enabled" do
    before { SiteSetting.hide_email_address_taken = true }

    let(:user) { Fabricate(:user, email: old_email) }
    let(:existing) { Fabricate(:user, email: new_email) }
    let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) }

    it "doesn't error if user exists with new email" do
      updater.change_to(existing.email)
      expect(updater.errors).to be_blank
      expect(user.email_change_requests).to be_empty
    end

    it "sends an email to the owner of the account with the new email" do
      expect_enqueued_with(
        job: :critical_user_email,
        args: {
          type: :account_exists,
          user_id: existing.id,
        },
      ) { updater.change_to(existing.email) }
    end
  end
end