# frozen_string_literal: true

RSpec.describe SecondFactorManager do
  fab!(:user)
  fab!(:user_second_factor_totp) { Fabricate(:user_second_factor_totp, user: user) }
  fab!(:user_security_key) do
    Fabricate(
      :user_security_key,
      user: user,
      public_key: valid_security_key_data[:public_key],
      credential_id: valid_security_key_data[:credential_id],
    )
  end
  fab!(:another_user) { Fabricate(:user) }

  fab!(:user_second_factor_backup)
  let(:user_backup) { user_second_factor_backup.user }

  describe "#totp" do
    it "should return the right data" do
      totp = nil

      expect do totp = another_user.create_totp(enabled: true) end.to change {
        UserSecondFactor.count
      }.by(1)

      expect(totp.totp_object.issuer).to eq(SiteSetting.title)
      expect(totp.totp_object.secret).to eq(
        another_user.reload.user_second_factors.totps.first.data,
      )
    end
  end

  describe "#create_totp" do
    it "should create the right record" do
      second_factor = another_user.create_totp(enabled: true)

      expect(second_factor.method).to eq(UserSecondFactor.methods[:totp])
      expect(second_factor.data).to be_present
      expect(second_factor.enabled).to eq(true)
    end
  end

  describe "#totp_provisioning_uri" do
    it "should return the right uri" do
      expect(user.user_second_factors.totps.first.totp_provisioning_uri).to eq(
        "otpauth://totp/#{SiteSetting.title}:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=#{SiteSetting.title}",
      )
    end
    it "should handle a colon in the site title" do
      SiteSetting.title = "Spaceballs: The Discourse"
      expect(user.user_second_factors.totps.first.totp_provisioning_uri).to eq(
        "otpauth://totp/Spaceballs%20The%20Discourse:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=Spaceballs%20The%20Discourse",
      )
    end
    it "should handle a two words before a colon in the title" do
      SiteSetting.title = "Our Spaceballs: The Discourse"
      expect(user.user_second_factors.totps.first.totp_provisioning_uri).to eq(
        "otpauth://totp/Our%20Spaceballs%20The%20Discourse:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=Our%20Spaceballs%20The%20Discourse",
      )
    end
  end

  describe "#authenticate_totp" do
    it "should be able to authenticate a token" do
      freeze_time do
        expect(user.user_second_factors.totps.first.last_used).to eq(nil)

        token = user.user_second_factors.totps.first.totp_object.now

        expect(user.authenticate_totp(token)).to eq(true)
        expect(user.user_second_factors.totps.first.last_used).to eq_time(Time.zone.now)
        expect(user.authenticate_totp(token)).to eq(false)
      end
    end

    describe "when token is blank" do
      it "should be false" do
        expect(user.authenticate_totp(nil)).to eq(false)
        expect(user.user_second_factors.totps.first.last_used).to eq(nil)
      end
    end

    describe "when token is invalid" do
      it "should be false" do
        expect(user.authenticate_totp("111111")).to eq(false)
        expect(user.user_second_factors.totps.first.last_used).to eq(nil)
      end
    end
  end

  describe "#totp_enabled?" do
    describe "when user does not have a second factor record" do
      it "should return false" do
        expect(another_user.totp_enabled?).to eq(false)
      end
    end

    describe "when user's second factor record is disabled" do
      it "should return false" do
        disable_totp
        expect(user.totp_enabled?).to eq(false)
      end
    end

    describe "when user's second factor record is enabled" do
      it "should return true" do
        expect(user.totp_enabled?).to eq(true)
      end
    end

    describe "when SSO is enabled" do
      it "should return false" do
        SiteSetting.discourse_connect_url = "http://someurl.com"
        SiteSetting.enable_discourse_connect = true

        expect(user.totp_enabled?).to eq(false)
      end
    end

    describe "when local login is disabled" do
      it "should return false" do
        SiteSetting.enable_local_logins = false

        expect(user.totp_enabled?).to eq(false)
      end
    end
  end

  describe "#has_multiple_second_factor_methods?" do
    context "when security keys and totp are enabled" do
      it "returns true" do
        expect(user.has_multiple_second_factor_methods?).to eq(true)
      end
    end

    context "if the totp gets disabled" do
      it "returns false" do
        disable_totp
        expect(user.has_multiple_second_factor_methods?).to eq(false)
      end
    end

    context "if the security key gets disabled" do
      it "returns false" do
        disable_security_key
        expect(user.has_multiple_second_factor_methods?).to eq(false)
      end
    end
  end

  describe "#only_security_keys_enabled?" do
    it "returns true if totp disabled and security key enabled" do
      disable_totp
      expect(user.only_security_keys_enabled?).to eq(true)
    end
  end

  describe "#only_totp_or_backup_codes_enabled?" do
    it "returns true if totp enabled and security key disabled" do
      disable_security_key
      expect(user.only_totp_or_backup_codes_enabled?).to eq(true)
    end
  end

  describe "#authenticate_second_factor" do
    let(:params) { {} }
    let(:secure_session) { {} }

    context "when neither security keys nor totp/backup codes are enabled" do
      before { disable_security_key && disable_totp }
      it "returns OK, because it doesn't need to authenticate" do
        expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
      end

      it "keeps used_2fa_method nil because no authentication is done" do
        expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(nil)
      end
    end

    context "when only security key is enabled" do
      before do
        disable_totp
        simulate_localhost_webauthn_challenge
        DiscourseWebauthn.stage_challenge(user, secure_session)
        DiscourseWebauthn.stubs(:origin).returns("http://localhost:3000")
      end

      context "when security key params are valid" do
        let(:params) do
          {
            second_factor_token: valid_security_key_auth_post_data,
            second_factor_method: UserSecondFactor.methods[:security_key],
          }
        end
        it "returns OK" do
          expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
        end

        it "sets used_2fa_method to security keys" do
          expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(
            UserSecondFactor.methods[:security_key],
          )
        end
      end

      context "when security key params are invalid" do
        let(:params) do
          {
            second_factor_token: {
              signature: "bad",
              clientData: "bad",
              authenticatorData: "bad",
              credentialId: "bad",
            },
            second_factor_method: UserSecondFactor.methods[:security_key],
          }
        end
        it "returns not OK" do
          result = user.authenticate_second_factor(params, secure_session)
          expect(result.ok).to eq(false)
          expect(result.error).to eq(I18n.t("webauthn.validation.not_found_error"))
          expect(result.used_2fa_method).to eq(nil)
        end
      end
    end

    context "when only totp is enabled" do
      before { disable_security_key }

      context "when totp is valid" do
        let(:params) do
          {
            second_factor_token: user.user_second_factors.totps.first.totp_object.now,
            second_factor_method: UserSecondFactor.methods[:totp],
          }
        end
        it "returns OK" do
          expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
        end

        it "sets used_2fa_method to totp" do
          expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(
            UserSecondFactor.methods[:totp],
          )
        end
      end

      context "when totp is invalid" do
        let(:params) do
          { second_factor_token: "blah", second_factor_method: UserSecondFactor.methods[:totp] }
        end
        it "returns not OK" do
          result = user.authenticate_second_factor(params, secure_session)
          expect(result.ok).to eq(false)
          expect(result.error).to eq(I18n.t("login.invalid_second_factor_code"))
          expect(result.used_2fa_method).to eq(nil)
        end
      end
    end

    context "when both security keys and totp are enabled" do
      let(:invalid_method) { 4 }
      let(:method) { invalid_method }

      before do
        simulate_localhost_webauthn_challenge
        DiscourseWebauthn.stage_challenge(user, secure_session)
        DiscourseWebauthn.stubs(:origin).returns("http://localhost:3000")
      end

      context "when method selected is invalid" do
        it "returns an error" do
          result = user.authenticate_second_factor(params, secure_session)
          expect(result.ok).to eq(false)
          expect(result.error).to eq(I18n.t("login.invalid_second_factor_method"))
          expect(result.used_2fa_method).to eq(nil)
        end
      end

      context "when method selected is TOTP" do
        let(:method) { UserSecondFactor.methods[:totp] }
        let(:token) { user.user_second_factors.totps.first.totp_object.now }

        context "when totp params are provided" do
          let(:params) { { second_factor_token: token, second_factor_method: method } }

          it "validates totp OK" do
            expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
          end

          it "sets used_2fa_method to totp" do
            expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(
              UserSecondFactor.methods[:totp],
            )
          end

          context "when the user does not have TOTP enabled" do
            let(:token) { "test" }
            before { user.totps.destroy_all }

            it "returns an error" do
              result = user.authenticate_second_factor(params, secure_session)
              expect(result.ok).to eq(false)
              expect(result.error).to eq(I18n.t("login.not_enabled_second_factor_method"))
              expect(result.used_2fa_method).to eq(nil)
            end
          end
        end
      end

      context "when method selected is Security Keys" do
        let(:method) { UserSecondFactor.methods[:security_key] }

        before do
          simulate_localhost_webauthn_challenge
          DiscourseWebauthn.stage_challenge(user, secure_session)
        end

        context "when security key params are valid" do
          let(:params) do
            { second_factor_token: valid_security_key_auth_post_data, second_factor_method: method }
          end
          it "returns OK" do
            expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
          end

          it "sets used_2fa_method to security keys" do
            expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(
              UserSecondFactor.methods[:security_key],
            )
          end

          context "when the user does not have security keys enabled" do
            before { user.security_keys.destroy_all }

            it "returns an error" do
              result = user.authenticate_second_factor(params, secure_session)
              expect(result.ok).to eq(false)
              expect(result.error).to eq(I18n.t("login.not_enabled_second_factor_method"))
              expect(result.used_2fa_method).to eq(nil)
            end
          end
        end
      end

      context "when method selected is Backup Codes" do
        let(:method) { UserSecondFactor.methods[:backup_codes] }
        let!(:backup_code) { Fabricate(:user_second_factor_backup, user: user) }

        context "when backup code params are provided" do
          let(:params) do
            { second_factor_token: "iAmValidBackupCode", second_factor_method: method }
          end

          context "when backup codes enabled" do
            it "validates codes OK" do
              expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
            end

            it "sets used_2fa_method to backup codes" do
              expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(
                UserSecondFactor.methods[:backup_codes],
              )
            end
          end

          context "when backup codes disabled" do
            before { user.user_second_factors.backup_codes.destroy_all }

            it "returns an error" do
              result = user.authenticate_second_factor(params, secure_session)
              expect(result.ok).to eq(false)
              expect(result.error).to eq(I18n.t("login.not_enabled_second_factor_method"))
              expect(result.used_2fa_method).to eq(nil)
            end
          end
        end
      end

      context "when no totp params are provided" do
        let(:params) do
          {
            second_factor_token: valid_security_key_auth_post_data,
            second_factor_method: UserSecondFactor.methods[:security_key],
          }
        end

        it "validates the security key OK" do
          expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
        end

        it "sets used_2fa_method to security keys" do
          expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(
            UserSecondFactor.methods[:security_key],
          )
        end
      end

      context "when totp params are provided" do
        let(:params) do
          {
            second_factor_token: user.user_second_factors.totps.first.totp_object.now,
            second_factor_method: UserSecondFactor.methods[:totp],
          }
        end

        it "validates totp OK" do
          expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true)
        end

        it "sets used_2fa_method to totp" do
          expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(
            UserSecondFactor.methods[:totp],
          )
        end
      end
    end
  end

  describe "backup codes" do
    describe "#generate_backup_codes" do
      it "should generate and store 10 backup codes" do
        backup_codes = user.generate_backup_codes

        expect(backup_codes.length).to be 10
        expect(user_backup.user_second_factors.backup_codes).to be_present
        expect(user_backup.user_second_factors.backup_codes.pluck(:method).uniq[0]).to eq(
          UserSecondFactor.methods[:backup_codes],
        )
        expect(user_backup.user_second_factors.backup_codes.pluck(:enabled).uniq[0]).to eq(true)
      end
    end

    describe "#create_backup_codes" do
      it "should create 10 backup code records" do
        raw_codes = Array.new(10) { SecureRandom.hex(8) }
        backup_codes = another_user.create_backup_codes(raw_codes)

        expect(another_user.user_second_factors.backup_codes.length).to be 10
      end
    end

    describe "#authenticate_backup_code" do
      it "should be able to authenticate a backup code" do
        backup_code = "iAmValidBackupCode"

        expect(user_backup.authenticate_backup_code(backup_code)).to eq(true)
        expect(user_backup.authenticate_backup_code(backup_code)).to eq(false)
      end

      describe "when code is blank" do
        it "should be false" do
          expect(user_backup.authenticate_backup_code(nil)).to eq(false)
        end
      end

      describe "when code is invalid" do
        it "should be false" do
          expect(user_backup.authenticate_backup_code("notValidBackupCode")).to eq(false)
        end
      end
    end

    describe "#backup_codes_enabled?" do
      describe "when user does not have a second factor backup enabled" do
        it "should return false" do
          expect(another_user.backup_codes_enabled?).to eq(false)
        end
      end

      describe "when user's second factor backup codes have been used" do
        it "should return false" do
          user_backup.user_second_factors.backup_codes.update_all(enabled: false)
          expect(user_backup.backup_codes_enabled?).to eq(false)
        end
      end

      describe "when user's second factor code is available" do
        it "should return true" do
          expect(user_backup.backup_codes_enabled?).to eq(true)
        end
      end

      describe "when SSO is enabled" do
        it "should return false" do
          SiteSetting.discourse_connect_url = "http://someurl.com"
          SiteSetting.enable_discourse_connect = true

          expect(user_backup.backup_codes_enabled?).to eq(false)
        end
      end

      describe "when local login is disabled" do
        it "should return false" do
          SiteSetting.enable_local_logins = false

          expect(user_backup.backup_codes_enabled?).to eq(false)
        end
      end
    end
  end

  def disable_totp
    user.user_second_factors.totps.first.update!(enabled: false)
  end

  def disable_security_key
    user.security_keys.first.destroy!
  end
end