# frozen_string_literal: true

require "spec_helper"

describe Doorkeeper::OAuth::PasswordAccessTokenRequest do
  let(:server) do
    double(
      :server,
      default_scopes: Doorkeeper::OAuth::Scopes.new,
      access_token_expires_in: 2.hours,
      refresh_token_enabled?: false,
      custom_access_token_expires_in: lambda { |context|
        context.grant_type == Doorkeeper::OAuth::PASSWORD ? 1234 : nil
      },
    )
  end
  let(:client) { FactoryBot.create(:application) }
  let(:owner)  { double :owner, id: 99 }

  before do
    allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true)
  end

  subject do
    described_class.new(server, client, owner)
  end

  it "issues a new token for the client" do
    expect do
      subject.authorize
    end.to change { client.reload.access_tokens.count }.by(1)

    expect(client.reload.access_tokens.max_by(&:created_at).expires_in).to eq(1234)
  end

  it "issues a new token without a client" do
    expect do
      subject.client = nil
      subject.authorize
    end.to change { Doorkeeper::AccessToken.count }.by(1)
  end

  it "does not issue a new token with an invalid client" do
    expect do
      subject.client = nil
      subject.parameters = { client_id: "bad_id" }
      subject.authorize
    end.not_to(change { Doorkeeper::AccessToken.count })

    expect(subject.error).to eq(:invalid_client)
  end

  it "requires the owner" do
    subject.resource_owner = nil
    subject.validate
    expect(subject.error).to eq(:invalid_grant)
  end

  it "optionally accepts the client" do
    subject.client = nil
    expect(subject).to be_valid
  end

  it "creates token even when there is already one (default)" do
    FactoryBot.create(:access_token, application_id: client.id, resource_owner_id: owner.id)

    expect do
      subject.authorize
    end.to change { Doorkeeper::AccessToken.count }.by(1)
  end

  it "skips token creation if there is already one reusable" do
    allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
    FactoryBot.create(:access_token, application_id: client.id, resource_owner_id: owner.id)

    expect do
      subject.authorize
    end.not_to(change { Doorkeeper::AccessToken.count })
  end

  it "creates token when there is already one but non reusable" do
    allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
    FactoryBot.create(:access_token, application_id: client.id, resource_owner_id: owner.id)
    allow_any_instance_of(Doorkeeper::AccessToken).to receive(:reusable?).and_return(false)

    expect do
      subject.authorize
    end.to change { Doorkeeper::AccessToken.count }.by(1)
  end

  it "calls configured request callback methods" do
    expect(Doorkeeper.configuration.before_successful_strategy_response)
      .to receive(:call).with(subject).once

    expect(Doorkeeper.configuration.after_successful_strategy_response)
      .to receive(:call).with(subject, instance_of(Doorkeeper::OAuth::TokenResponse)).once

    subject.authorize
  end

  describe "with scopes" do
    subject do
      described_class.new(server, client, owner, scope: "public")
    end

    context "when scopes_by_grant_type is not configured for grant_type" do
      it "returns error when scopes are invalid" do
        allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("another"))
        subject.validate
        expect(subject.error).to eq(:invalid_scope)
      end

      it "creates the token with scopes if scopes are valid" do
        allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public"))
        expect do
          subject.authorize
        end.to change { Doorkeeper::AccessToken.count }.by(1)

        expect(Doorkeeper::AccessToken.last.scopes).to include("public")
      end
    end

    context "when scopes_by_grant_type is configured for grant_type" do
      it "returns error when scopes are valid but not permitted for grant_type" do
        allow(server)
          .to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public"))
        allow(Doorkeeper.configuration)
          .to receive(:scopes_by_grant_type).and_return(password: "another")
        subject.validate
        expect(subject.error).to eq(:invalid_scope)
      end

      it "creates the token with scopes if scopes are valid and permitted for grant_type" do
        allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public"))
        allow(Doorkeeper.configuration)
          .to receive(:scopes_by_grant_type).and_return(password: [:public])

        expect do
          subject.authorize
        end.to change { Doorkeeper::AccessToken.count }.by(1)

        expect(Doorkeeper::AccessToken.last.scopes).to include("public")
      end
    end
  end

  describe "with custom expiry" do
    let(:server) do
      double(
        :server,
        default_scopes: Doorkeeper::OAuth::Scopes.new,
        access_token_expires_in: 2.hours,
        refresh_token_enabled?: false,
        custom_access_token_expires_in: lambda { |context|
          if context.scopes.exists?("public")
            222
          elsif context.scopes.exists?("magic")
            Float::INFINITY
          end
        },
      )
    end

    before do
      allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true)
    end

    it "checks scopes" do
      subject = described_class.new(server, client, owner, scope: "public")
      allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public"))

      expect do
        subject.authorize
      end.to change { Doorkeeper::AccessToken.count }.by(1)

      expect(Doorkeeper::AccessToken.last.expires_in).to eq(222)
    end

    it "falls back to the default otherwise" do
      subject = described_class.new(server, client, owner, scope: "private")
      allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("private"))

      expect do
        subject.authorize
      end.to change { Doorkeeper::AccessToken.count }.by(1)

      expect(Doorkeeper::AccessToken.last.expires_in).to eq(2.hours)
    end
  end
end
