require "test_helper"

class OidcUserinfoControllerTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:alice)
    @application = applications(:kavita_app)

    # Add user to a group for groups claim testing
    @admin_group = groups(:admin_group)
    @user.groups << @admin_group unless @user.groups.include?(@admin_group)
  end

  def teardown
    # Clean up
    OidcAccessToken.where(user: @user, application: @application).destroy_all
  end

  # ============================================================================
  # HTTP Method Tests (GET and POST)
  # ============================================================================

  test "userinfo endpoint accepts GET requests" do
    access_token = create_access_token("openid email profile")

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)
    assert json["sub"].present?
  end

  test "userinfo endpoint accepts POST requests" do
    access_token = create_access_token("openid email profile")

    post "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)
    assert json["sub"].present?
  end

  test "userinfo endpoint accepts POST with access_token in body" do
    access_token = create_access_token("openid email profile")

    post "/oauth/userinfo", params: {
      access_token: access_token.plaintext_token
    }

    assert_response :success
    json = JSON.parse(response.body)
    assert json["sub"].present?
  end

  # ============================================================================
  # Scope-Based Claim Filtering Tests
  # ============================================================================

  test "userinfo with openid scope only returns minimal claims" do
    access_token = create_access_token("openid")

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)

    # Required claims
    assert json["sub"].present?, "Should include sub claim"

    # Scope-dependent claims should NOT be present
    assert_nil json["email"], "Should not include email without email scope"
    assert_nil json["email_verified"], "Should not include email_verified without email scope"
    assert_nil json["name"], "Should not include name without profile scope"
    assert_nil json["preferred_username"], "Should not include preferred_username without profile scope"
    assert_nil json["groups"], "Should not include groups without groups scope"
  end

  test "userinfo with email scope includes email claims" do
    access_token = create_access_token("openid email")

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)

    # Required claims
    assert json["sub"].present?

    # Email claims should be present
    assert_equal @user.email_address, json["email"], "Should include email with email scope"
    assert_equal true, json["email_verified"], "Should include email_verified with email scope"

    # Profile claims should NOT be present
    assert_nil json["name"], "Should not include name without profile scope"
    assert_nil json["preferred_username"], "Should not include preferred_username without profile scope"
  end

  test "userinfo with profile scope includes profile claims" do
    access_token = create_access_token("openid profile")

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)

    # Required claims
    assert json["sub"].present?

    # Profile claims we support should be present
    assert json["name"].present?, "Should include name with profile scope"
    assert json["preferred_username"].present?, "Should include preferred_username with profile scope"
    assert json["updated_at"].present?, "Should include updated_at with profile scope"

    # Email claims should NOT be present
    assert_nil json["email"], "Should not include email without email scope"
    assert_nil json["email_verified"], "Should not include email_verified without email scope"
  end

  test "userinfo with groups scope includes groups claim" do
    access_token = create_access_token("openid groups")

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)

    # Required claims
    assert json["sub"].present?

    # Groups claim should be present
    assert json["groups"].present?, "Should include groups with groups scope"
    assert_includes json["groups"], "Administrators", "Should include user's groups"

    # Email and profile claims should NOT be present
    assert_nil json["email"], "Should not include email without email scope"
    assert_nil json["name"], "Should not include name without profile scope"
  end

  test "userinfo with multiple scopes includes all requested claims" do
    access_token = create_access_token("openid email profile groups")

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)

    # All scope-based claims should be present
    assert json["sub"].present?
    assert json["email"].present?, "Should include email"
    assert json["email_verified"].present?, "Should include email_verified"
    assert json["name"].present?, "Should include name"
    assert json["preferred_username"].present?, "Should include preferred_username"
    assert json["groups"].present?, "Should include groups"
  end

  test "userinfo returns same filtered claims for GET and POST" do
    access_token = create_access_token("openid email")

    # GET request
    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }
    get_json = JSON.parse(response.body)

    # POST request
    post "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }
    post_json = JSON.parse(response.body)

    # Both should return the same claims
    assert_equal get_json, post_json, "GET and POST should return identical claims"
  end

  # ============================================================================
  # Authentication Tests
  # ============================================================================

  test "userinfo endpoint requires Bearer token" do
    get "/oauth/userinfo"

    assert_response :unauthorized
  end

  test "userinfo endpoint rejects invalid token" do
    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer invalid_token_12345"
    }

    assert_response :unauthorized
  end

  test "userinfo endpoint rejects expired token" do
    access_token = create_access_token("openid email profile")

    # Expire the token
    access_token.update!(expires_at: 1.hour.ago)

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :unauthorized
  end

  test "userinfo endpoint rejects revoked token" do
    access_token = create_access_token("openid email profile")

    # Revoke the token
    access_token.revoke!

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :unauthorized
  end

  # ============================================================================
  # Pairwise Subject Identifier Test
  # ============================================================================

  test "userinfo returns pairwise SID when consent exists" do
    access_token = create_access_token("openid")

    # Find existing consent or create new one (ensure it has a SID)
    consent = OidcUserConsent.find_or_initialize_by(
      user: @user,
      application: @application
    )
    consent.scopes_granted ||= "openid"
    consent.save!

    # Reload to get the auto-generated SID
    consent.reload

    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{access_token.plaintext_token}"
    }

    assert_response :success
    json = JSON.parse(response.body)
    assert_equal consent.sid, json["sub"], "Should use pairwise SID from consent"
    assert consent.sid.present?, "Consent should have a SID"
  end

  private

  def create_access_token(scope)
    OidcAccessToken.create!(
      application: @application,
      user: @user,
      scope: scope
    )
  end
end
