require "test_helper"

# Tokens must not be minted for a user who has lost access (deactivated, or removed
# from the application's allowed group) between authorization and the token request.
# Every grant re-checks Application#user_allowed? at mint time.
class OidcMintAuthorizationTest < ActionDispatch::IntegrationTest
  DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code".freeze

  def setup
    @group = Group.create!(name: "mint-authz-testers", description: "test")
    @user = User.create!(email_address: "mint_authz@example.com", password: "password123")
    @user.groups << @group

    @secret = "mint-authz-secret-value-abcdefghij"
    @application = Application.create!(name: "Mint Authz App", slug: "mint-authz-app", app_type: "oidc",
      client_secret: @secret, active: true, require_pkce: false,
      redirect_uris: ["https://app.example.com/cb"].to_json)
    @application.allowed_groups << @group

    OidcUserConsent.create!(user: @user, application: @application, scopes_granted: "openid", granted_at: Time.current)
  end

  def teardown
    OidcRefreshToken.where(application: @application).delete_all
    OidcAccessToken.where(application: @application).delete_all
    OidcDeviceCode.where(application: @application).delete_all
    OidcAuthorizationCode.where(application: @application).delete_all
    OidcUserConsent.where(application: @application).delete_all
  end

  # --- Device grant ----------------------------------------------------------

  test "device grant issues tokens for a still-allowed user" do
    poll(approved_device_code)
    assert_response :success
    assert JSON.parse(@response.body)["access_token"].present?
  end

  test "device grant refuses a user removed from the allowed group after approval" do
    dc = approved_device_code
    revoke_group!
    poll(dc)
    assert_access_denied
  end

  test "device grant refuses a deactivated user after approval" do
    dc = approved_device_code
    @user.disabled!
    poll(dc)
    assert_access_denied
  end

  # --- Authorization code grant ---------------------------------------------

  test "authorization_code grant refuses a user removed from the allowed group" do
    code = OidcAuthorizationCode.create!(application: @application, user: @user,
      redirect_uri: "https://app.example.com/cb", scope: "openid", auth_time: Time.current.to_i, acr: "1")
    revoke_group!
    post "/oauth/token", params: {grant_type: "authorization_code", code: code.plaintext_code,
      redirect_uri: "https://app.example.com/cb", client_id: @application.client_id, client_secret: @secret}
    assert_access_denied
  end

  # --- Refresh grant ---------------------------------------------------------

  test "refresh_token grant refuses a deactivated user" do
    refresh = issue_refresh_token
    @user.disabled!
    refresh_with(refresh)
    assert_access_denied
  end

  test "refresh_token grant refuses a removed user and leaves the token intact" do
    refresh = issue_refresh_token
    revoke_group!
    refresh_with(refresh)
    assert_access_denied
    assert_not refresh.reload.revoked?, "a denied refresh must not rotate/revoke the token"
  end

  private

  def approved_device_code
    dc = OidcDeviceCode.create!(application: @application, scope: "openid")
    dc.approve!(user: @user, acr: "1", auth_time: Time.current.to_i)
    dc
  end

  def issue_refresh_token
    access = OidcAccessToken.create!(application: @application, user: @user, scope: "openid")
    OidcRefreshToken.create!(application: @application, user: @user, oidc_access_token: access,
      scope: "openid", auth_time: Time.current.to_i, acr: "1")
  end

  def revoke_group!
    UserGroup.where(user: @user, group: @group).delete_all
    @user.reload
  end

  def poll(dc)
    post "/oauth/token", params: {grant_type: DEVICE_GRANT, device_code: dc.plaintext_device_code,
      client_id: @application.client_id, client_secret: @secret}
  end

  def refresh_with(refresh)
    post "/oauth/token", params: {grant_type: "refresh_token", refresh_token: refresh.token,
      client_id: @application.client_id, client_secret: @secret}
  end

  def assert_access_denied
    assert_response :bad_request
    assert_equal "access_denied", JSON.parse(@response.body)["error"]
  end
end
