require "test_helper"

class OidcAuthorizationCodeSecurityTest < ActionDispatch::IntegrationTest
  def setup
    @user = User.create!(email_address: "security_test@example.com", password: "password123")
    @application = Application.create!(
      name: "Security Test App",
      slug: "security-test-app",
      app_type: "oidc",
      redirect_uris: ["http://localhost:4000/callback"].to_json,
      active: true,
      require_pkce: false
    )

    # Store the plain text client secret for testing
    @client_secret = @application.client_secret_digest
    @application.generate_new_client_secret!
    @plain_client_secret = @application.client_secret
    @application.save!
    grant_everyone_access(@application)
  end

  def teardown
    # Delete in correct order to avoid foreign key constraints
    OidcRefreshToken.where(application: @application).delete_all
    OidcAccessToken.where(application: @application).delete_all
    OidcAuthorizationCode.where(application: @application).delete_all
    OidcUserConsent.where(application: @application).delete_all
    @user.destroy
    @application.destroy
  end

  # ====================
  # CRITICAL SECURITY TESTS
  # ====================

  test "consent endpoint rejects cross-site POST without a CSRF token" do
    sign_in_as(@user)

    # Forgery protection is disabled in the test env by default; enable it so the
    # before_action actually runs, mirroring production behaviour.
    original = ActionController::Base.allow_forgery_protection
    ActionController::Base.allow_forgery_protection = true
    begin
      # No authenticity_token param: a forged cross-site submission. Because
      # :consent is NOT in the verify_authenticity_token skip list, this must be
      # rejected before the action can grant any OAuth scopes.
      post "/oauth/authorize/consent", params: {approve: "true"}

      assert_response :unprocessable_entity
    ensure
      ActionController::Base.allow_forgery_protection = original
    end
  end

  test "prevents authorization code reuse - sequential attempts" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create a valid authorization code
    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    # First request should succeed
    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :success
    first_response = JSON.parse(@response.body)
    assert first_response.key?("access_token")
    assert first_response.key?("id_token")

    # Second request with same code should fail
    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_grant", error["error"]
    assert_match(/already been used/, error["error_description"])
  end

  test "revokes existing tokens when authorization code is reused" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create a valid authorization code
    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    # First request - get access token
    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :success
    first_response = JSON.parse(@response.body)
    first_access_token = first_response["access_token"]

    # Verify the token works
    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{first_access_token}"
    }
    assert_response :success

    # Second request with same code - should fail AND revoke first token
    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request

    # Verify the first token is now revoked (expired)
    get "/oauth/userinfo", headers: {
      "Authorization" => "Bearer #{first_access_token}"
    }
    assert_response :unauthorized, "First access token should be revoked after code reuse"
  end

  test "rejects already used authorization code" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create and mark code as used
    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      used: true,
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_grant", error["error"]
    assert_match(/already been used/, error["error_description"])
  end

  test "rejects expired authorization code" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create expired code
    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 5.minutes.ago
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_grant", error["error"]
    assert_match(/expired/, error["error_description"])
  end

  test "rejects authorization code with mismatched redirect_uri" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://evil.com/callback" # Wrong redirect URI
    }

    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_grant", error["error"]
    assert_match(/Redirect URI mismatch/, error["error_description"])
  end

  test "rejects non-existent authorization code" do
    token_params = {
      grant_type: "authorization_code",
      code: "nonexistent_code_12345",
      redirect_uri: "http://localhost:4000/callback"
    }

    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_grant", error["error"]
  end

  test "rejects authorization code for different application" do
    # Create consent for the first application
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create another application
    other_app = Application.create!(
      name: "Other App",
      slug: "other-app",
      app_type: "oidc",
      redirect_uris: ["http://localhost:5000/callback"].to_json,
      active: true,
      require_pkce: false
    )
    other_secret = other_app.client_secret

    # Create auth code for first application
    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    # Try to use it with different application credentials
    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{other_app.client_id}:#{other_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_grant", error["error"]

    other_app.destroy
  end

  # ====================
  # CLIENT AUTHENTICATION TESTS
  # ====================

  test "rejects invalid client_id in Basic auth" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("invalid_client_id:#{@plain_client_secret}")
    }

    assert_response :unauthorized
    error = JSON.parse(@response.body)
    assert_equal "invalid_client", error["error"]
  end

  test "rejects invalid client_secret in Basic auth" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    post "/oauth/token", params: token_params, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:wrong_secret")
    }

    assert_response :unauthorized
    error = JSON.parse(@response.body)
    assert_equal "invalid_client", error["error"]
  end

  test "accepts client credentials in POST body" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback",
      client_id: @application.client_id,
      client_secret: @plain_client_secret
    }

    post "/oauth/token", params: token_params

    assert_response :success
    response_body = JSON.parse(@response.body)
    assert response_body.key?("access_token")
    assert response_body.key?("id_token")
  end

  test "rejects request with no client authentication" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    post "/oauth/token", params: token_params

    assert_response :unauthorized
    error = JSON.parse(@response.body)
    assert_equal "invalid_client", error["error"]
  end

  # ====================
  # GRANT TYPE VALIDATION
  # ====================

  test "rejects unsupported grant_type" do
    post "/oauth/token", params: {
      grant_type: "password",
      username: "user",
      password: "pass"
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "unsupported_grant_type", error["error"]
  end

  test "rejects missing grant_type" do
    post "/oauth/token", params: {
      code: "some_code",
      redirect_uri: "http://localhost:4000/callback"
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "unsupported_grant_type", error["error"]
  end

  # ====================
  # TIMING ATTACK PROTECTION
  # ====================

  test "client authentication uses constant-time comparison" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    token_params = {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }

    # Test with completely wrong secret
    times_wrong = []
    5.times do
      start_time = Time.now.to_f
      post "/oauth/token", params: token_params, headers: {
        "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:wrong_secret_xxx")
      }
      times_wrong << (Time.now.to_f - start_time)
      assert_response :unauthorized
    end

    # Test with almost correct secret (differs by one character)
    correct_secret = @plain_client_secret
    almost_correct = correct_secret[0..-2] + "X"

    times_almost = []
    5.times do
      start_time = Time.now.to_f
      post "/oauth/token", params: token_params, headers: {
        "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{almost_correct}")
      }
      times_almost << (Time.now.to_f - start_time)
      assert_response :unauthorized
    end

    # The timing difference should be minimal (within 50ms) if using constant-time comparison
    avg_wrong = times_wrong.sum / times_wrong.size
    avg_almost = times_almost.sum / times_almost.size
    timing_difference = (avg_wrong - avg_almost).abs

    # This is a best-effort check - in practice, constant-time comparison is handled by bcrypt
    assert timing_difference < 0.05,
      "Timing difference #{timing_difference}s suggests potential timing attack vulnerability"
  end

  # ====================
  # STATE PARAMETER BINDING (CSRF PREVENTION FOR OAUTH)
  # ====================

  test "state parameter is required and validated in authorization flow" do
    # Create consent to skip consent page
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Sign in first
    post signin_path, params: {email_address: "security_test@example.com", password: "password123"}

    # Test authorization with state parameter
    get "/oauth/authorize", params: {
      client_id: @application.client_id,
      redirect_uri: "http://localhost:4000/callback",
      response_type: "code",
      scope: "openid profile",
      state: "random_state_123"
    }

    # Should include state in redirect
    assert_response :redirect
    assert_match(/state=random_state_123/, response.location)
  end

  test "authorization without state parameter still works but is less secure" do
    # Create consent to skip consent page
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Sign in first
    post signin_path, params: {email_address: "security_test@example.com", password: "password123"}

    # Test authorization without state parameter
    get "/oauth/authorize", params: {
      client_id: @application.client_id,
      redirect_uri: "http://localhost:4000/callback",
      response_type: "code",
      scope: "openid profile"
    }

    # Should work but state is recommended for CSRF protection
    assert_response :redirect
  end

  # ====================
  # NONCE PARAMETER VALIDATION (FOR ID TOKENS)
  # ====================

  test "nonce parameter is included in ID token" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create authorization code with nonce
    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      nonce: "test_nonce_123",
      expires_at: 10.minutes.from_now
    )

    # Exchange code for tokens
    post "/oauth/token", params: {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :success
    response_body = JSON.parse(@response.body)
    id_token = response_body["id_token"]

    # Decode ID token (without verification for this test)
    decoded_token = JWT.decode(id_token, nil, false)

    # Verify nonce is included in ID token
    assert_equal "test_nonce_123", decoded_token[0]["nonce"]
  end

  # ====================
  # TOKEN LEAKAGE VIA REFERER HEADER TESTS
  # ====================

  test "access tokens are not exposed in referer header" do
    # Create consent and authorization code
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    # Exchange code for tokens
    post "/oauth/token", params: {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :success
    response_body = JSON.parse(@response.body)
    response_body["access_token"]

    # Verify token is not in response headers (especially Referer)
    assert_nil response.headers["Referer"], "Access token should not leak in Referer header"
    assert_nil response.headers["Location"], "Access token should not leak in Location header"
  end

  # ====================
  # PKCE ENFORCEMENT FOR PUBLIC CLIENTS TESTS
  # ====================

  test "PKCE code_verifier is required when code_challenge was provided" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create authorization code with PKCE challenge
    code_verifier = SecureRandom.urlsafe_base64(32)
    code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      code_challenge: code_challenge,
      code_challenge_method: "S256",
      expires_at: 10.minutes.from_now
    )

    # Try to exchange code without code_verifier
    post "/oauth/token", params: {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_request", error["error"]
    assert_match(/code_verifier is required/, error["error_description"])
  end

  test "PKCE with S256 method validates correctly" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create authorization code with PKCE S256
    code_verifier = SecureRandom.urlsafe_base64(32)
    code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      code_challenge: code_challenge,
      code_challenge_method: "S256",
      expires_at: 10.minutes.from_now
    )

    # Exchange code with correct code_verifier
    post "/oauth/token", params: {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback",
      code_verifier: code_verifier
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :success
    response_body = JSON.parse(@response.body)
    assert response_body.key?("access_token")
  end

  test "PKCE rejects invalid code_verifier" do
    # Create consent
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create authorization code with PKCE
    code_verifier = SecureRandom.urlsafe_base64(32)
    code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      code_challenge: code_challenge,
      code_challenge_method: "S256",
      expires_at: 10.minutes.from_now
    )

    # Try with wrong code_verifier
    post "/oauth/token", params: {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback",
      code_verifier: "wrong_code_verifier_12345678901234567890"
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :bad_request
    error = JSON.parse(@response.body)
    assert_equal "invalid_request", error["error"]
  end

  # ====================
  # REFRESH TOKEN ROTATION TESTS
  # ====================

  test "refresh token rotation is enforced" do
    # Create consent for the refresh token endpoint
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-123"
    )

    # Create initial access and refresh tokens
    access_token = OidcAccessToken.create!(
      application: @application,
      user: @user,
      scope: "openid profile"
    )

    refresh_token = OidcRefreshToken.create!(
      application: @application,
      user: @user,
      oidc_access_token: access_token,
      scope: "openid profile"
    )

    original_token_family_id = refresh_token.token_family_id
    old_refresh_token = refresh_token.token

    # Refresh the token
    post "/oauth/token", params: {
      grant_type: "refresh_token",
      refresh_token: old_refresh_token
    }, headers: {
      "Authorization" => "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")
    }

    assert_response :success
    response_body = JSON.parse(@response.body)
    new_refresh_token = response_body["refresh_token"]

    # Verify new refresh token is different
    assert_not_equal old_refresh_token, new_refresh_token

    # Verify token family is preserved
    new_token_record = OidcRefreshToken.find_by_token(new_refresh_token)
    assert_equal original_token_family_id, new_token_record.token_family_id

    # Old refresh token should be revoked
    old_token_record = OidcRefreshToken.find(refresh_token.id)
    assert old_token_record.revoked?
  end

  test "code replay revokes the full token chain including rotated descendants" do
    OidcUserConsent.create!(
      user: @user,
      application: @application,
      scopes_granted: "openid profile",
      granted_at: Time.current,
      sid: "test-sid-chain"
    )

    auth_code = OidcAuthorizationCode.create!(
      application: @application,
      user: @user,
      redirect_uri: "http://localhost:4000/callback",
      scope: "openid profile",
      expires_at: 10.minutes.from_now
    )

    basic = "Basic " + Base64.strict_encode64("#{@application.client_id}:#{@plain_client_secret}")

    # Initial exchange -> A1 + R1
    post "/oauth/token", params: {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }, headers: {"Authorization" => basic}
    assert_response :success
    first_refresh = JSON.parse(@response.body)["refresh_token"]

    # Rotate once -> A2 + R2 (same auth_code FK carried forward)
    post "/oauth/token", params: {
      grant_type: "refresh_token",
      refresh_token: first_refresh
    }, headers: {"Authorization" => basic}
    assert_response :success

    # Sanity: the full chain is now linked to the auth_code
    assert_equal 2, auth_code.oidc_access_tokens.count
    assert_equal 2, auth_code.oidc_refresh_tokens.count

    # Replay the original code
    post "/oauth/token", params: {
      grant_type: "authorization_code",
      code: auth_code.plaintext_code,
      redirect_uri: "http://localhost:4000/callback"
    }, headers: {"Authorization" => basic}
    assert_response :bad_request

    # Every descendant token must now have revoked_at set
    auth_code.oidc_access_tokens.each do |token|
      assert_not_nil token.reload.revoked_at,
        "access token #{token.id} should have revoked_at set after replay"
    end
    auth_code.oidc_refresh_tokens.each do |token|
      assert_not_nil token.reload.revoked_at,
        "refresh token #{token.id} should have revoked_at set after replay"
    end
  end
end
