# frozen_string_literal: true

require "spec_helper"

RSpec.describe Doorkeeper::AccessToken do
  subject(:access_token) { FactoryBot.build(:access_token) }

  it { expect(access_token).to be_valid }

  it_behaves_like "an accessible token"
  it_behaves_like "a revocable token"
  it_behaves_like "a unique token" do
    let(:factory_name) { :access_token }
  end

  module CustomGeneratorArgs
    def self.generate; end
  end

  describe "#generate_token" do
    it "generates a token using the default method" do
      FactoryBot.create :access_token

      token = FactoryBot.create :access_token
      expect(token.token).to be_a(String)
    end

    context "with hashing enabled" do
      let(:token) { FactoryBot.create :access_token }

      include_context "with token hashing enabled"

      it "holds a volatile plaintext token when created" do
        expect(token.plaintext_token).to be_a(String)
        expect(token.token)
          .to eq(hashed_or_plain_token_func.call(token.plaintext_token))

        # Finder method only finds the hashed token
        loaded = described_class.find_by(token: token.token)
        expect(loaded).to eq(token)
        expect(loaded.plaintext_token).to be_nil
        expect(loaded.token).to eq(token.token)
      end

      it "does not find_by plain text tokens" do
        expect(described_class.find_by(token: token.plaintext_token)).to be_nil
      end

      describe "with having a plain text token" do
        let(:plain_text_token) { "plain text token" }
        let(:access_token) { FactoryBot.create :access_token }

        before do
          # Assume we have a plain text token from before activating the option
          access_token.update_column(:token, plain_text_token)
        end

        context "without fallback lookup" do
          it "does not provide lookups with either through by_token" do
            expect(described_class.by_token(plain_text_token)).to eq(nil)
            expect(described_class.by_token(access_token.token)).to eq(nil)

            # And it does not touch the token
            access_token.reload
            expect(access_token.token).to eq(plain_text_token)
          end
        end

        context "with fallback lookup" do
          include_context "with token hashing and fallback lookup enabled"

          it "upgrades a plain token when falling back to it" do
            # Side-effect: This will automatically upgrade the token
            expect(described_class).to receive(:upgrade_fallback_value).and_call_original
            expect(described_class.by_token(plain_text_token))
              .to have_attributes(
                resource_owner_id: access_token.resource_owner_id,
                application_id: access_token.application_id,
                scopes: access_token.scopes,
              )

            # Will find subsequently by hashing the token
            expect(described_class.by_token(plain_text_token))
              .to have_attributes(
                resource_owner_id: access_token.resource_owner_id,
                application_id: access_token.application_id,
                scopes: access_token.scopes,
              )

            # Not all the ORM support :id PK
            if access_token.respond_to?(:id)
              expect(described_class.by_token(plain_text_token).id).to eq(access_token.id)
            end

            # And it modifies the token value
            access_token.reload
            expect(access_token.token).not_to eq(plain_text_token)
            expect(described_class.find_by(token: plain_text_token)).to eq(nil)
            expect(described_class.find_by(token: access_token.token)).not_to be_nil
          end
        end
      end
    end

    it "generates a token using a custom object" do
      eigenclass = class << CustomGeneratorArgs; self; end
      eigenclass.class_eval do
        remove_method :generate
      end
      module CustomGeneratorArgs
        def self.generate(opts = {})
          id = opts[:resource_owner_id] || opts[:resource_owner]&.id
          "custom_generator_token_#{id}"
        end
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "CustomGeneratorArgs"
      end

      owner = FactoryBot.create :resource_owner
      token = FactoryBot.create :access_token,
                                resource_owner_id: owner.id,
                                resource_owner_type: owner.class.name

      expect(token.token).to match(/custom_generator_token_\d+/)
    end

    it "allows the custom generator to access the application details" do
      eigenclass = class << CustomGeneratorArgs; self; end
      eigenclass.class_eval do
        remove_method :generate
      end

      module CustomGeneratorArgs
        def self.generate(opts = {})
          "custom_generator_token_#{opts[:application].name}"
        end
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "CustomGeneratorArgs"
      end

      token = FactoryBot.create :access_token
      expect(token.token).to match(/custom_generator_token_Application \d+/)
    end

    it "allows the custom generator to access the scopes" do
      eigenclass = class << CustomGeneratorArgs; self; end
      eigenclass.class_eval do
        remove_method :generate
      end
      module CustomGeneratorArgs
        def self.generate(opts = {})
          "custom_generator_token_#{opts[:scopes].count}_#{opts[:scopes]}"
        end
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "CustomGeneratorArgs"
      end

      token = FactoryBot.create :access_token, scopes: "public write"

      expect(token.token).to eq "custom_generator_token_2_public write"
    end

    it "allows the custom generator to access the expiry length" do
      eigenclass = class << CustomGeneratorArgs; self; end
      eigenclass.class_eval do
        remove_method :generate
      end
      module CustomGeneratorArgs
        def self.generate(opts = {})
          "custom_generator_token_#{opts[:expires_in]}"
        end
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "CustomGeneratorArgs"
      end

      token = FactoryBot.create :access_token
      expect(token.token).to eq "custom_generator_token_7200"
    end

    it "allows the custom generator to access the created time" do
      module CustomGeneratorArgs
        def self.generate(opts = {})
          "custom_generator_token_#{opts[:created_at].to_i}"
        end
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "CustomGeneratorArgs"
      end

      token = FactoryBot.create :access_token
      created_at = token.created_at
      expect(token.token).to eq "custom_generator_token_#{created_at.to_i}"
    end

    it "allows the custom generator to access the custom attributes" do
      module CustomGeneratorArgs
        def self.generate(opts = {})
          "custom_generator_token_#{opts[:tenant_name]}"
        end
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "CustomGeneratorArgs"
        custom_access_token_attributes [:tenant_name]
      end

      token = FactoryBot.create :access_token, tenant_name: "Tenant 1"
      expect(token.token).to eq "custom_generator_token_Tenant 1"
    end

    it "raises an error if the custom object does not support generate" do
      module NoGenerate
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "NoGenerate"
      end

      expect { FactoryBot.create :access_token }.to(
        raise_error(Doorkeeper::Errors::UnableToGenerateToken),
      )
    end

    it "raises original error if something went wrong in custom generator" do
      eigenclass = class << CustomGeneratorArgs; self; end
      eigenclass.class_eval do
        remove_method :generate
      end

      module CustomGeneratorArgs
        def self.generate(_opts = {})
          raise LoadError, "custom behaviour"
        end
      end

      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "CustomGeneratorArgs"
      end

      expect { FactoryBot.create :access_token }.to(
        raise_error(LoadError),
      )
    end

    it "raises an error if the custom object does not exist" do
      Doorkeeper.configure do
        orm DOORKEEPER_ORM
        access_token_generator "Doorkeeper::NotReal"
      end

      expect { FactoryBot.create :access_token }.to(
        raise_error(Doorkeeper::Errors::TokenGeneratorNotFound, /NotReal/),
      )
    end
  end

  describe "refresh_token" do
    it "has empty refresh token if it was not required" do
      token = FactoryBot.create :access_token
      expect(token.refresh_token).to be_nil
    end

    it "generates a refresh token if it was requested" do
      token = FactoryBot.create :access_token, use_refresh_token: true
      expect(token.refresh_token).not_to be_nil
    end

    it "is not valid if token exists" do
      token1 = FactoryBot.create :access_token, use_refresh_token: true
      token2 = FactoryBot.create :access_token, use_refresh_token: true
      token2.refresh_token = token1.refresh_token
      expect(token2).not_to be_valid
    end

    it "expects database to raise an error if refresh tokens are the same" do
      token1 = FactoryBot.create :access_token, use_refresh_token: true
      token2 = FactoryBot.create :access_token, use_refresh_token: true
      expect do
        token2.refresh_token = token1.refresh_token
        token2.save(validate: false)
      end.to raise_error(uniqueness_error)
    end

    context "with hashing enabled" do
      include_context "with token hashing enabled"
      let(:token) { FactoryBot.create :access_token, use_refresh_token: true }

      it "holds a volatile refresh token when created" do
        expect(token.plaintext_refresh_token).to be_a(String)
        expect(token.refresh_token)
          .to eq(hashed_or_plain_token_func.call(token.plaintext_refresh_token))

        # Finder method only finds the hashed token
        loaded = described_class.find_by(refresh_token: token.refresh_token)
        expect(loaded).to eq(token)
        expect(loaded.plaintext_refresh_token).to be_nil
        expect(loaded.refresh_token).to eq(token.refresh_token)
      end

      it "does not find_by plain text refresh tokens" do
        expect(described_class.find_by(refresh_token: token.plaintext_refresh_token)).to be_nil
      end

      describe "with having a plain text token" do
        let(:plain_refresh_token) { "plain refresh token" }
        let(:access_token) { FactoryBot.create :access_token }

        before do
          # Assume we have a plain text token from before activating the option
          access_token.update_column(:refresh_token, plain_refresh_token)
        end

        context "without fallback lookup" do
          it "does not provide lookups with either through by_token" do
            expect(described_class.by_refresh_token(plain_refresh_token)).to eq(nil)
            expect(described_class.by_refresh_token(access_token.refresh_token)).to eq(nil)

            # And it does not touch the token
            access_token.reload
            expect(access_token.refresh_token).to eq(plain_refresh_token)
          end
        end

        context "with fallback lookup" do
          include_context "with token hashing and fallback lookup enabled"

          it "upgrades a plain token when falling back to it" do
            # Side-effect: This will automatically upgrade the token
            expect(described_class).to receive(:upgrade_fallback_value).and_call_original
            expect(described_class.by_refresh_token(plain_refresh_token))
              .to have_attributes(
                token: access_token.token,
                resource_owner_id: access_token.resource_owner_id,
                application_id: access_token.application_id,
              )

            # Will find subsequently by hashing the token
            expect(described_class.by_refresh_token(plain_refresh_token))
              .to have_attributes(
                token: access_token.token,
                resource_owner_id: access_token.resource_owner_id,
                application_id: access_token.application_id,
              )

            # Not all the ORM support :id PK
            if access_token.respond_to?(:id)
              expect(described_class.by_refresh_token(plain_refresh_token).id).to eq(access_token.id)
            end

            # And it modifies the token value
            access_token.reload
            expect(access_token.refresh_token).not_to eq(plain_refresh_token)
            expect(described_class.find_by(refresh_token: plain_refresh_token)).to eq(nil)
            expect(described_class.find_by(refresh_token: access_token.refresh_token)).not_to be_nil
          end
        end
      end
    end
  end

  describe "validations" do
    it "is valid without resource_owner_id" do
      # For client credentials flow
      access_token.resource_owner_id = nil
      expect(access_token).to be_valid
    end

    it "is valid without application_id" do
      # For resource owner credentials flow
      access_token.application_id = nil
      expect(access_token).to be_valid
    end
  end

  describe "#same_credential?" do
    context "with default parameters" do
      let(:resource_owner) { FactoryBot.create(:resource_owner) }
      let(:resource_owner_id) { resource_owner.id }
      let(:application) { FactoryBot.create :application }
      let(:default_attributes) do
        {
          application: application,
          resource_owner_id: resource_owner_id,
          resource_owner_type: resource_owner.class.name,
        }
      end
      let(:access_token1) { FactoryBot.create :access_token, default_attributes }

      context "when the second token has the same owner and same app" do
        let(:access_token2) { FactoryBot.create :access_token, default_attributes }

        it "success" do
          expect(access_token1).to be_same_credential(access_token2)
        end
      end

      context "when the second token has same owner and different app" do
        let(:other_application) { FactoryBot.create :application }
        let(:access_token2) do
          FactoryBot.create :access_token,
                            application: other_application,
                            resource_owner_id: resource_owner_id,
                            resource_owner_type: resource_owner.class.name
        end

        it "fails" do
          expect(access_token1).not_to be_same_credential(access_token2)
        end
      end

      context "when the second token has different owner and different app" do
        let(:other_application) { FactoryBot.create :application }
        let(:access_token2) do
          FactoryBot.create :access_token,
                            application: other_application,
                            resource_owner_id: resource_owner.id + 1
        end

        it "fails" do
          expect(access_token1).not_to be_same_credential(access_token2)
        end
      end

      context "when the second token has different owner and same app" do
        let(:access_token2) do
          FactoryBot.create :access_token,
                            application: application,
                            resource_owner_id: resource_owner.id + 1
        end

        it "fails" do
          expect(access_token1).not_to be_same_credential(access_token2)
        end
      end
    end
  end

  describe "#acceptable?" do
    context "when token is not accessible" do
      let(:token) { FactoryBot.create(:access_token, created_at: 6.hours.ago) }

      it "returns false" do
        expect(token.acceptable?(nil)).to be false
      end
    end

    context "when token has the incorrect scopes" do
      let(:token) { FactoryBot.create(:access_token) }

      it "returns false" do
        expect(token.acceptable?(["public"])).to be false
      end
    end

    context "when token is acceptable with the correct scopes" do
      let(:token) do
        token = FactoryBot.create(:access_token)
        token[:scopes] = "public"
        token
      end

      it "returns true" do
        expect(token.acceptable?(["public"])).to be true
      end
    end
  end

  describe ".revoke_all_for" do
    let(:resource_owner) { FactoryBot.create :resource_owner }
    let(:application)    { FactoryBot.create :application }
    let(:default_attributes) do
      {
        application: application,
        resource_owner_id: resource_owner.id,
        resource_owner_type: resource_owner.class.name,
      }
    end

    it "revokes all tokens for given application and resource owner" do
      FactoryBot.create :access_token, default_attributes
      described_class.revoke_all_for(application.id, resource_owner)
      expect(described_class.all).to all(be_revoked)
    end

    it "matches application" do
      access_token_for_different_app = FactoryBot.create(
        :access_token,
        default_attributes.merge(application: FactoryBot.create(:application)),
      )

      described_class.revoke_all_for(application.id, resource_owner)

      expect(access_token_for_different_app.reload).not_to be_revoked
    end

    it "matches resource owner" do
      access_token_for_different_owner = FactoryBot.create(
        :access_token,
        default_attributes.merge(resource_owner_id: resource_owner.id + 1),
      )

      described_class.revoke_all_for(application.id, resource_owner)

      expect(access_token_for_different_owner.reload).not_to be_revoked
    end
  end

  describe ".matching_token_for" do
    let(:resource_owner)    { FactoryBot.create :resource_owner }
    let(:resource_owner_id) { resource_owner.id }
    let(:application)       { FactoryBot.create :application }
    let(:scopes) { Doorkeeper::OAuth::Scopes.from_string("public write") }
    let(:default_attributes) do
      {
        application: application,
        resource_owner_id: resource_owner_id,
        resource_owner_type: resource_owner.class.name,
        scopes: scopes.to_s,
      }
    end

    before do
      default_scopes_exist(*scopes.all)
    end

    it "returns only one token" do
      token = FactoryBot.create :access_token, default_attributes
      last_token = described_class.matching_token_for(application, resource_owner, scopes)
      expect(last_token).to eq(token)
    end

    it "accepts nil as resource owner" do
      token = FactoryBot.create :access_token,
                                default_attributes.merge(resource_owner_id: nil, resource_owner_type: nil)
      last_token = described_class.matching_token_for(application, nil, scopes)
      expect(last_token).to eq(token)
    end

    it "excludes revoked tokens" do
      FactoryBot.create :access_token, default_attributes.merge(revoked_at: 1.day.ago)
      last_token = described_class.matching_token_for(application, resource_owner_id, scopes)
      expect(last_token).to be_nil
    end

    it "excludes tokens with a different application" do
      FactoryBot.create :access_token, default_attributes.merge(application: FactoryBot.create(:application))
      last_token = described_class.matching_token_for(application, resource_owner_id, scopes)
      expect(last_token).to be_nil
    end

    it "excludes tokens with a different resource owner" do
      FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: resource_owner.id + 1)
      last_token = described_class.matching_token_for(application, resource_owner_id, scopes)
      expect(last_token).to be_nil
    end

    it "excludes tokens with fewer scopes" do
      FactoryBot.create :access_token, default_attributes.merge(scopes: "public")
      last_token = described_class.matching_token_for(application, resource_owner_id, scopes)
      expect(last_token).to be_nil
    end

    it "excludes tokens with different scopes" do
      FactoryBot.create :access_token, default_attributes.merge(scopes: "public email")
      last_token = described_class.matching_token_for(application, resource_owner, scopes)
      expect(last_token).to be_nil
    end

    it "excludes tokens with additional scopes" do
      FactoryBot.create :access_token, default_attributes.merge(scopes: "public write email")
      last_token = described_class.matching_token_for(application, resource_owner, scopes)
      expect(last_token).to be_nil
    end

    it "excludes tokens with scopes that are not present in server scopes" do
      FactoryBot.create :access_token, default_attributes.merge(
        application: application, scopes: "public read",
      )
      last_token = described_class.matching_token_for(application, resource_owner, scopes)
      expect(last_token).to be_nil
    end

    it "excludes tokens with scopes that are not present in application scopes" do
      application = FactoryBot.create :application, scopes: "private read"
      FactoryBot.create :access_token, default_attributes.merge(
        application: application,
      )
      last_token = described_class.matching_token_for(application, resource_owner, scopes)
      expect(last_token).to be_nil
    end

    it "does not match token if empty scope requested and token/app scopes present" do
      application = FactoryBot.create :application, scopes: "sample:scope"
      app_params = {
        application_id: application.id, scopes: "sample:scope",
        resource_owner_id: resource_owner.id,
        resource_owner_type: resource_owner.class.name,
      }
      FactoryBot.create :access_token, app_params
      empty_scopes = Doorkeeper::OAuth::Scopes.from_string("")
      last_token = described_class.matching_token_for(application, resource_owner.id, empty_scopes)
      expect(last_token).to be_nil
    end

    it "matches token if empty scope requested and no token scopes present" do
      empty_scopes = Doorkeeper::OAuth::Scopes.from_string("")
      token = FactoryBot.create :access_token, default_attributes.merge(scopes: empty_scopes)
      last_token = described_class.matching_token_for(application, resource_owner.id, empty_scopes)
      expect(last_token).to eq(token)
    end

    it "returns the last matching token" do
      FactoryBot.create :access_token, default_attributes.merge(created_at: 1.day.ago)
      matching_token = FactoryBot.create :access_token, default_attributes
      FactoryBot.create :access_token, default_attributes.merge(scopes: "public")

      last_token = described_class.matching_token_for(application, resource_owner_id, scopes)
      expect(last_token).to eq(matching_token)
    end

    context "when custom access token attributes are used" do
      before do
        Doorkeeper.configure do
          orm DOORKEEPER_ORM
          custom_access_token_attributes [:tenant_name]
        end
        default_scopes_exist(*scopes.all)
      end
      let(:custom_attributes) { { tenant_name: "Me" } }

      it "returns a token when attributes match" do
        token = FactoryBot.create :access_token, default_attributes.merge(custom_attributes)
        last_token = described_class.matching_token_for(
          application, resource_owner, scopes, custom_attributes: custom_attributes)
        expect(last_token).to eq(token)
      end

      it "does not return a token if attributes don't match" do
        token = FactoryBot.create :access_token, default_attributes.merge(custom_attributes)
        last_token = described_class.matching_token_for(application, resource_owner, scopes, custom_attributes: { tenant_id: 'different' })
        expect(last_token).to eq(nil)
      end

      it "ignores custom attributes if a nil value is passed" do
        token = FactoryBot.create :access_token, default_attributes.merge(custom_attributes)
        last_token = described_class.matching_token_for(application, resource_owner, scopes, custom_attributes: nil)
        expect(last_token).to eq(token)
      end
    end
  end

  describe "#as_json" do
    let(:token) { FactoryBot.create(:access_token) }
    let(:token_hash) do
      {
        resource_owner_id: token.resource_owner_id,
        scope: token.scopes,
        expires_in: token.expires_in_seconds,
        application: { uid: token.application.uid },
        created_at: token.created_at.to_i,
      }
    end

    it "returns as_json hash" do
      hash = token_hash

      if Doorkeeper.configuration.polymorphic_resource_owner?
        hash[:resource_owner_type] = token.resource_owner_type
      end

      expect(token.as_json).to match(hash)
    end

    describe "#not_expired" do
      let(:resource_owner) { FactoryBot.create(:doorkeeper_testing_user) }
      let(:application) { FactoryBot.create(:application) }

      let(:attrs) { { resource_owner_id: resource_owner.id, application_id: application.id } }

      let!(:active_token1) { FactoryBot.create(:access_token, attrs.merge(expires_in: 2000)) }
      let!(:active_token2) { FactoryBot.create(:access_token, attrs.merge(expires_in: 2)) }
      let!(:active_token3) { FactoryBot.create(:access_token, attrs.merge(expires_in: 10, created_at: Time.current - 5.seconds)) }
      let!(:active_token4) { FactoryBot.create(:access_token, attrs.merge(expires_in: nil)) }
      let!(:not_active_token1) { FactoryBot.create(:access_token, attrs.merge(expires_in: 2, created_at: Time.current - 2.seconds)) }
      let!(:not_active_token2) { FactoryBot.create(:access_token, attrs.merge(expires_in: 10, created_at: Time.current - 12.seconds)) }
      let!(:not_active_token3) { FactoryBot.create(:access_token, attrs.merge(expires_in: 10_000, revoked_at: Time.current)) }

      before do
        Timecop.freeze(Time.current)
      end

      after do
        Timecop.return
      end

      it "returns only non expired tokens" do
        expired_tokens = described_class.not_expired
        expect(expired_tokens.size).to be(4)
        expect(expired_tokens).to match_array([active_token1, active_token2, active_token3, active_token4])
      end
    end
  end
end
