Commit b9ab88e333 for woocommerce

commit b9ab88e333c0c91970432204f3dc3867ed7059fd
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Tue Nov 4 13:53:17 2025 +0200

    [Payments NOX] Fix payment providers tests using fill in classes (#61789)

    * Add guards against invalid payment providers

    * test: Add tests for getting payments providers instances

    * refac: No need for call_user_func_array

    * Inject the LegacyProxy into providers for proper test mocking

    * Use proxy calls in the WooPayments provider for easy mocking in tests

    * Remove WooPayments onboarding router experiment

    * test: Improve and expand WooPayments provider unit tests

    * test: Fix WCCore providers unit tests

    * test: Fix generic PaymentGateway provider unit tests

    * test: Improve generic PaymentGateway unit test coverage

    * Add changelog

    * Use unresolved string constants

    * test: Remove duplicate assertions

    * docs: Fix types

    * Better guard against failure to get WPCOM Connection Manager instance

    * No need to bail on missing query params

    * Guard against magic __call methods

    * test: Update proxy mocks to include method_exists

    * Do not use resolved class or function names

    * test: Do not use resolved class or function names

    * Lint fixes

    * test: Have mocks fallback on PHP native functions when not mocked

diff --git a/plugins/woocommerce/changelog/fix-providers-tests-using-fill-in-classes b/plugins/woocommerce/changelog/fix-providers-tests-using-fill-in-classes
new file mode 100644
index 0000000000..c2e7f54f4a
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-providers-tests-using-fill-in-classes
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: Update payments providers classes to use the LegacyProxy when making externals calls.
+
+
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
index 976ab401d3..47ad39c9b1 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
@@ -366,7 +366,7 @@ class PaymentsProviders {
 		/**
 		 * The provider class for the gateway.
 		 *
-		 * @var PaymentGateway|null $provider_class
+		 * @var class-string<PaymentGateway>|null $provider_class
 		 */
 		$provider_class = null;
 		if ( isset( $this->payment_gateways_providers_class_map[ $gateway_id ] ) ) {
@@ -386,16 +386,31 @@ class PaymentsProviders {
 			}
 		}

+		// Check that the provider class extends the PaymentGateway class.
+		if ( ! is_null( $provider_class ) && ! is_subclass_of( $provider_class, PaymentGateway::class ) ) {
+			wc_doing_it_wrong(
+				__METHOD__,
+				sprintf(
+					/* translators: %s: Gateway ID. */
+					esc_html__( 'The provider class for gateway ID "%s" must extend the PaymentGateway class.', 'woocommerce' ),
+					$gateway_id
+				),
+				'10.4.0'
+			);
+			// Return the generic provider as a fallback.
+			$provider_class = null;
+		}
+
 		// If the gateway ID is not mapped to a provider class, return the generic provider.
 		if ( is_null( $provider_class ) ) {
 			if ( ! isset( $this->instances['generic'] ) ) {
-				$this->instances['generic'] = new PaymentGateway();
+				$this->instances['generic'] = new PaymentGateway( $this->proxy );
 			}

 			return $this->instances['generic'];
 		}

-		$this->instances[ $gateway_id ] = new $provider_class();
+		$this->instances[ $gateway_id ] = new $provider_class( $this->proxy );

 		return $this->instances[ $gateway_id ];
 	}
@@ -416,23 +431,36 @@ class PaymentsProviders {
 		/**
 		 * The provider class for the payment extension suggestion (PES).
 		 *
-		 * @var PaymentGateway|null $provider_class
+		 * @var class-string<PaymentGateway>|null $provider_class
 		 */
 		$provider_class = null;
 		if ( isset( $this->payment_extension_suggestions_providers_class_map[ $pes_id ] ) ) {
-			$provider_class = $this->payment_extension_suggestions_providers_class_map[ $pes_id ];
+			if ( ! is_subclass_of( $this->payment_extension_suggestions_providers_class_map[ $pes_id ], PaymentGateway::class ) ) {
+				wc_doing_it_wrong(
+					__METHOD__,
+					sprintf(
+						/* translators: %s: Payment extension suggestion ID. */
+						esc_html__( 'The provider class for payment extension suggestion ID "%s" must extend the PaymentGateway class.', 'woocommerce' ),
+						$pes_id
+					),
+					'10.4.0'
+				);
+				// Return the generic provider as a fallback.
+			} else {
+				$provider_class = $this->payment_extension_suggestions_providers_class_map[ $pes_id ];
+			}
 		}

 		// If the gateway ID is not mapped to a provider class, return the generic provider.
 		if ( is_null( $provider_class ) ) {
 			if ( ! isset( $this->instances['generic'] ) ) {
-				$this->instances['generic'] = new PaymentGateway();
+				$this->instances['generic'] = new PaymentGateway( $this->proxy );
 			}

 			return $this->instances['generic'];
 		}

-		$this->instances[ $pes_id ] = new $provider_class();
+		$this->instances[ $pes_id ] = new $provider_class( $this->proxy );

 		return $this->instances[ $pes_id ];
 	}
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php
index 868590d4fb..b5f20aa38e 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php
@@ -8,6 +8,7 @@ use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
 use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
 use Automattic\WooCommerce\Internal\Admin\Settings\Utils;
 use Automattic\WooCommerce\Internal\Logging\SafeGlobalFunctionProxy;
+use Automattic\WooCommerce\Proxies\LegacyProxy;
 use Throwable;
 use WC_HTTPS;
 use WC_Payment_Gateway;
@@ -36,6 +37,22 @@ class PaymentGateway {
 	const PAYMENT_METHOD_CATEGORY_PRIMARY   = 'primary';
 	const PAYMENT_METHOD_CATEGORY_SECONDARY = 'secondary';

+	/**
+	 * The LegacyProxy instance.
+	 *
+	 * @var LegacyProxy
+	 */
+	protected LegacyProxy $proxy;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param LegacyProxy $proxy The LegacyProxy instance.
+	 */
+	public function __construct( LegacyProxy $proxy ) {
+		$this->proxy = $proxy;
+	}
+
 	/**
 	 * Extract the payment gateway provider details from the object.
 	 *
@@ -602,10 +619,7 @@ class PaymentGateway {
 			if ( method_exists( $payment_gateway, 'get_onboarding_not_supported_message' ) &&
 				is_callable( array( $payment_gateway, 'get_onboarding_not_supported_message' ) ) ) {

-				$message = call_user_func_array(
-					array( $payment_gateway, 'get_onboarding_not_supported_message' ),
-					array( $country_code ),
-				);
+				$message = call_user_func( array( $payment_gateway, 'get_onboarding_not_supported_message' ), $country_code, );
 				if ( is_string( $message ) && ! empty( $message ) ) {
 					return sanitize_textarea_field( trim( $message ) );
 				}
@@ -964,10 +978,7 @@ class PaymentGateway {

 		try {
 			// Get the "raw" recommended payment methods from the payment gateway.
-			$recommended_pms = call_user_func_array(
-				array( $payment_gateway, 'get_recommended_payment_methods' ),
-				array( $country_code ),
-			);
+			$recommended_pms = call_user_func( array( $payment_gateway, 'get_recommended_payment_methods' ), $country_code );
 			if ( ! is_array( $recommended_pms ) ) {
 				// Bail if the recommended payment methods are not an array.
 				return array();
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/WooPayments.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/WooPayments.php
index 8c301ab31d..c5a2620fc1 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/WooPayments.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/WooPayments.php
@@ -18,7 +18,6 @@ use Automattic\WooCommerce\Internal\Logging\SafeGlobalFunctionProxy;
 use Throwable;
 use WC_Abstract_Order;
 use WC_Payment_Gateway;
-use WooCommerce\Admin\Experimental_Abtest;

 defined( 'ABSPATH' ) || exit;

@@ -189,7 +188,7 @@ class WooPayments extends PaymentGateway {

 			// Switch to the native in-context onboarding type if the WooPayments extension its version is compatible.
 			// We need to put back the '.php' extension to construct the plugin filename.
-			$plugin_data = PluginsHelper::get_plugin_data( $extension_suggestion['plugin']['file'] . '.php' );
+			$plugin_data = $this->proxy->call_static( PluginsHelper::class, 'get_plugin_data', $extension_suggestion['plugin']['file'] . '.php' );
 			if ( $plugin_data && ! empty( $plugin_data['Version'] ) &&
 				version_compare( $plugin_data['Version'], PaymentsProviders\WooPayments\WooPaymentsService::EXTENSION_MINIMUM_VERSION, '>=' ) ) {

@@ -246,24 +245,6 @@ class WooPayments extends PaymentGateway {
 		return $extension_suggestion;
 	}

-	/**
-	 * Get the current state of the store's WPCOM/Jetpack connection.
-	 *
-	 * @return array The store's WPCOM/Jetpack connection state.
-	 */
-	private function get_wpcom_connection_state(): array {
-		$wpcom_connection_manager = new WPCOM_Connection_Manager( 'woocommerce' );
-		$is_connected             = $wpcom_connection_manager->is_connected();
-		$has_connected_owner      = $wpcom_connection_manager->has_connected_owner();
-
-		return array(
-			'wpcom_has_working_connection' => $is_connected && $has_connected_owner,
-			'wpcom_is_store_connected'     => $is_connected,
-			'wpcom_has_connected_owner'    => $has_connected_owner,
-			'wpcom_is_connection_owner'    => $has_connected_owner && $wpcom_connection_manager->is_connection_owner(),
-		);
-	}
-
 	/**
 	 * Check if the payment gateway needs setup.
 	 *
@@ -296,11 +277,13 @@ class WooPayments extends PaymentGateway {
 	 * @return bool True if the payment gateway is in test mode, false otherwise.
 	 */
 	public function is_in_test_mode( WC_Payment_Gateway $payment_gateway ): bool {
-		if ( class_exists( '\WC_Payments' ) &&
-			is_callable( '\WC_Payments::mode' ) ) {
+		if ( $this->proxy->call_function( 'class_exists', 'WC_Payments' ) &&
+			$this->proxy->call_function( 'is_callable', 'WC_Payments::mode' ) ) {
+
+			$woopayments_mode = $this->proxy->call_static( 'WC_Payments', 'mode' );
+			if ( $this->proxy->call_function( 'method_exists', $woopayments_mode, 'is_test' ) &&
+				$this->proxy->call_function( 'is_callable', array( $woopayments_mode, 'is_test' ) ) ) {

-			$woopayments_mode = \WC_Payments::mode();
-			if ( is_callable( array( $woopayments_mode, 'is_test' ) ) ) {
 				return $woopayments_mode->is_test();
 			}
 		}
@@ -319,11 +302,13 @@ class WooPayments extends PaymentGateway {
 	 * @return bool True if the payment gateway is in dev mode, false otherwise.
 	 */
 	public function is_in_dev_mode( WC_Payment_Gateway $payment_gateway ): bool {
-		if ( class_exists( '\WC_Payments' ) &&
-			is_callable( '\WC_Payments::mode' ) ) {
+		if ( $this->proxy->call_function( 'class_exists', 'WC_Payments' ) &&
+			$this->proxy->call_function( 'is_callable', 'WC_Payments::mode' ) ) {
+
+			$woopayments_mode = $this->proxy->call_static( 'WC_Payments', 'mode' );
+			if ( $this->proxy->call_function( 'method_exists', $woopayments_mode, 'is_dev' ) &&
+				$this->proxy->call_function( 'is_callable', array( $woopayments_mode, 'is_dev' ) ) ) {

-			$woopayments_mode = \WC_Payments::mode();
-			if ( is_callable( array( $woopayments_mode, 'is_dev' ) ) ) {
 				return $woopayments_mode->is_dev();
 			}
 		}
@@ -405,11 +390,13 @@ class WooPayments extends PaymentGateway {
 	 * @return bool True if the payment gateway is in test mode onboarding, false otherwise.
 	 */
 	public function is_in_test_mode_onboarding( WC_Payment_Gateway $payment_gateway ): bool {
-		if ( class_exists( '\WC_Payments' ) &&
-			is_callable( '\WC_Payments::mode' ) ) {
+		if ( $this->proxy->call_function( 'class_exists', 'WC_Payments' ) &&
+			$this->proxy->call_function( 'is_callable', 'WC_Payments::mode' ) ) {
+
+			$woopayments_mode = $this->proxy->call_static( 'WC_Payments', 'mode' );
+			if ( $this->proxy->call_function( 'method_exists', $woopayments_mode, 'is_test_mode_onboarding' ) &&
+				$this->proxy->call_function( 'is_callable', array( $woopayments_mode, 'is_test_mode_onboarding' ) ) ) {

-			$woopayments_mode = \WC_Payments::mode();
-			if ( is_callable( array( $woopayments_mode, 'is_test_mode_onboarding' ) ) ) {
 				return $woopayments_mode->is_test_mode_onboarding();
 			}
 		}
@@ -429,22 +416,18 @@ class WooPayments extends PaymentGateway {
 	 * @return string The onboarding URL for the payment gateway.
 	 */
 	public function get_onboarding_url( WC_Payment_Gateway $payment_gateway, string $return_url = '' ): string {
-		if ( class_exists( '\WC_Payments_Account' ) && is_callable( '\WC_Payments_Account::get_connect_url' ) ) {
-			$connect_url = \WC_Payments_Account::get_connect_url();
+		if ( $this->proxy->call_function( 'class_exists', 'WC_Payments_Account' ) &&
+			$this->proxy->call_function( 'is_callable', 'WC_Payments_Account::get_connect_url' ) ) {
+
+			$connect_url = $this->proxy->call_static( 'WC_Payments_Account', 'get_connect_url' );
 		} else {
 			$connect_url = parent::get_onboarding_url( $payment_gateway, $return_url );
 		}

-		$query = wp_parse_url( $connect_url, PHP_URL_QUERY );
-		// We expect the URL to have a query string. Bail if it doesn't.
-		if ( empty( $query ) ) {
-			return $connect_url;
-		}
-
 		// Default URL params to set, regardless if they exist.
 		$params = array(
-			'from'                      => defined( '\WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_SETTINGS' ) ? \WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_SETTINGS : 'WCADMIN_PAYMENT_SETTINGS',
-			'source'                    => defined( '\WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE' ) ? \WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE : 'wcadmin-settings-page',
+			'from'                      => Constants::is_defined( 'WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_SETTINGS' ) ? (string) Constants::get_constant( 'WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_SETTINGS' ) : 'WCADMIN_PAYMENT_SETTINGS',
+			'source'                    => Constants::is_defined( 'WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE' ) ? (string) Constants::get_constant( 'WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE' ) : 'wcadmin-settings-page',
 			'redirect_to_settings_page' => 'true',
 		);

@@ -495,30 +478,6 @@ class WooPayments extends PaymentGateway {
 			$live_onboarding = true;
 		}

-		// We run an experiment to determine the efficiency of test-account-first onboarding vs straight-to-live onboarding.
-		// If the experiment is active and the store is in the treatment group, we will force live onboarding.
-		// Otherwise, we will do test-account-first onboarding (control group).
-		// Stores that are determined by our routing logic that they should do straight-to-live onboarding
-		// will not be affected by the experiment.
-		if ( ! $live_onboarding ) {
-			$transient_key = 'wc_experiment_failure_woocommerce_payment_settings_onboarding_2025_v1';
-
-			// Try to get cached result first.
-			$cached_result = get_transient( $transient_key );
-
-			// If we have a cache entry that indicates an error, don't enforce anything. Just let the routing logic decide.
-			if ( 'error' !== $cached_result ) {
-				try {
-					if ( Experimental_Abtest::in_treatment( 'woocommerce_payment_settings_onboarding_2025_v1' ) ) {
-						$live_onboarding = true;
-					}
-				} catch ( \Exception $e ) {
-					// If the experiment fails, set a transient to avoid repeated failures.
-					set_transient( $transient_key, 'error', HOUR_IN_SECONDS );
-				}
-			}
-		}
-
 		// If we are doing live onboarding, we don't need to add more to the URL.
 		// But for test-drive/sandbox mode, we have work to do.
 		if ( ! $live_onboarding ) {
@@ -617,9 +576,15 @@ class WooPayments extends PaymentGateway {
 	 * @return bool True if the account is a test account, false otherwise.
 	 */
 	private function has_test_account(): bool {
-		if ( function_exists( '\wcpay_get_container' ) && class_exists( '\WC_Payments_Account' ) ) {
-			$account_service = \wcpay_get_container()->get( \WC_Payments_Account::class );
-			if ( ! empty( $account_service ) && is_callable( array( $account_service, 'get_account_status_data' ) ) ) {
+		if ( $this->proxy->call_function( 'function_exists', 'wcpay_get_container' ) &&
+			$this->proxy->call_function( 'class_exists', 'WC_Payments_Account' ) ) {
+
+			$woopayments_container = $this->proxy->call_function( 'wcpay_get_container' );
+			$account_service       = $woopayments_container->get( 'WC_Payments_Account' );
+			if ( ! empty( $account_service ) &&
+				$this->proxy->call_function( 'method_exists', $account_service, 'get_account_status_data' ) &&
+				$this->proxy->call_function( 'is_callable', array( $account_service, 'get_account_status_data' ) ) ) {
+
 				$account_status = $account_service->get_account_status_data();

 				return ! empty( $account_status['testDrive'] );
@@ -640,9 +605,15 @@ class WooPayments extends PaymentGateway {
 	 * @return bool True if the account is a sandbox account, false otherwise.
 	 */
 	private function has_sandbox_account(): bool {
-		if ( function_exists( '\wcpay_get_container' ) && class_exists( '\WC_Payments_Account' ) ) {
-			$account_service = \wcpay_get_container()->get( \WC_Payments_Account::class );
-			if ( ! empty( $account_service ) && is_callable( array( $account_service, 'get_account_status_data' ) ) ) {
+		if ( $this->proxy->call_function( 'function_exists', 'wcpay_get_container' ) &&
+			$this->proxy->call_function( 'class_exists', 'WC_Payments_Account' ) ) {
+
+			$woopayments_container = $this->proxy->call_function( 'wcpay_get_container' );
+			$account_service       = $woopayments_container->get( 'WC_Payments_Account' );
+			if ( ! empty( $account_service ) &&
+				$this->proxy->call_function( 'method_exists', $account_service, 'get_account_status_data' ) &&
+				$this->proxy->call_function( 'is_callable', array( $account_service, 'get_account_status_data' ) ) ) {
+
 				$account_status = $account_service->get_account_status_data();

 				return empty( $account_status['isLive'] ) && empty( $account_status['testDrive'] );
@@ -661,10 +632,10 @@ class WooPayments extends PaymentGateway {
 	 */
 	private function get_supported_country_codes(): ?array {
 		try {
-			if ( class_exists( '\WC_Payments_Utils' ) &&
-				is_callable( '\WC_Payments_Utils::supported_countries' ) ) {
+			if ( $this->proxy->call_function( 'class_exists', 'WC_Payments_Utils' ) &&
+				$this->proxy->call_function( 'is_callable', 'WC_Payments_Utils::supported_countries' ) ) {

-				$supported_country_codes = \WC_Payments_Utils::supported_countries();
+				$supported_country_codes = $this->proxy->call_static( 'WC_Payments_Utils', 'supported_countries' );
 				if ( is_array( $supported_country_codes ) ) {
 					return array_unique( array_map( 'strtoupper', array_keys( $supported_country_codes ) ) );
 				}
@@ -682,4 +653,41 @@ class WooPayments extends PaymentGateway {

 		return null;
 	}
+
+	/**
+	 * Get the current state of the store's WPCOM/Jetpack connection.
+	 *
+	 * @return array The store's WPCOM/Jetpack connection state.
+	 */
+	private function get_wpcom_connection_state(): array {
+		try {
+			$wpcom_connection_manager = $this->proxy->get_instance_of( WPCOM_Connection_Manager::class, 'woocommerce' );
+		} catch ( \Throwable $e ) {
+			// Log so we can investigate.
+			SafeGlobalFunctionProxy::wc_get_logger()->error(
+				'Failed to get the WPCOM/Jetpack Connection Manager instance: ' . $e->getMessage(),
+				array(
+					'source' => 'settings-payments',
+				)
+			);
+
+			// Assume no connection.
+			return array(
+				'wpcom_has_working_connection' => false,
+				'wpcom_is_store_connected'     => false,
+				'wpcom_has_connected_owner'    => false,
+				'wpcom_is_connection_owner'    => false,
+			);
+		}
+
+		$is_connected        = $wpcom_connection_manager->is_connected();
+		$has_connected_owner = $wpcom_connection_manager->has_connected_owner();
+
+		return array(
+			'wpcom_has_working_connection' => $is_connected && $has_connected_owner,
+			'wpcom_is_store_connected'     => $is_connected,
+			'wpcom_has_connected_owner'    => $has_connected_owner,
+			'wpcom_is_connection_owner'    => $has_connected_owner && $wpcom_connection_manager->is_connection_owner(),
+		);
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/Mocks/WCPaymentsUtils.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/Mocks/WCPaymentsUtils.php
deleted file mode 100644
index 917d6d0e1c..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/Mocks/WCPaymentsUtils.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-/**
- * Mock WC_Payments_Utils class for testing.
- *
- * This mock is used in tests when the real WC_Payments_Utils class is not available.
- *
- * @package WooCommerce\Tests\Internal\Admin\Settings\Mocks
- */
-
-declare( strict_types=1 );
-
-if ( ! class_exists( 'WC_Payments_Utils' ) ) {
-	/**
-	 * Mock WC_Payments_Utils class.
-	 *
-	 * phpcs:disable Squiz.Classes.ClassFileName.NoMatch
-	 * phpcs:disable Suin.Classes.PSR4.IncorrectClassName
-	 * phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
-	 */
-	class WC_Payments_Utils {
-		/**
-		 * Get the list of supported countries for WooPayments.
-		 *
-		 * @return array Array of country codes and names.
-		 */
-		public static function supported_countries() {
-			// This is just a subset of countries that WooPayments supports.
-			// But it should cover our testing needs.
-			return array(
-				'us' => 'United States',
-				'gb' => 'United Kingdom',
-				'de' => 'Germany',
-			);
-		}
-	}
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php
index 663fcfa15c..e48392c686 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php
@@ -7,7 +7,11 @@ use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\PaymentGateway;
 use Automattic\WooCommerce\Internal\Admin\Suggestions\PaymentsExtensionSuggestions;
+use Automattic\WooCommerce\Proxies\LegacyProxy;
+use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
+use Automattic\WooCommerce\Testing\Tools\TestingContainer;
 use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
+use PHPUnit\Framework\MockObject\MockObject;
 use stdClass;
 use WC_Unit_Test_Case;

@@ -18,6 +22,11 @@ use WC_Unit_Test_Case;
  */
 class PaymentGatewayTest extends WC_Unit_Test_Case {

+	/**
+	 * @var MockableLegacyProxy|MockObject
+	 */
+	protected $mockable_proxy;
+
 	/**
 	 * @var PaymentGateway
 	 */
@@ -36,7 +45,16 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

-		$this->sut = new PaymentGateway();
+		/**
+		 * TestingContainer instance.
+		 *
+		 * @var TestingContainer $container
+		 */
+		$container = wc_get_container();
+
+		$this->mockable_proxy = $container->get( LegacyProxy::class );
+
+		$this->sut = new PaymentGateway( $this->mockable_proxy );
 	}

 	/**
@@ -524,6 +542,66 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
 		$this->assertFalse( $this->sut->needs_setup( $fake_gateway ) );
 	}

+	/**
+	 * Test needs_setup fallback logic when method returns false but account is not connected.
+	 */
+	public function test_needs_setup_fallback_when_method_returns_false_and_not_connected() {
+		// Arrange - Create a mock gateway with needs_setup method returning false.
+		$gateway = $this->getMockBuilder( 'WC_Payment_Gateway' )
+			->disableOriginalConstructor()
+			->onlyMethods( array( 'needs_setup' ) )
+			->addMethods( array( 'is_account_connected' ) )
+			->getMock();
+
+		$gateway->id = 'test_gateway';
+
+		// Expect needs_setup method to return false.
+		$gateway->expects( $this->once() )
+			->method( 'needs_setup' )
+			->willReturn( false );
+
+		// Expect is_account_connected to be called and return false.
+		$gateway->expects( $this->once() )
+			->method( 'is_account_connected' )
+			->willReturn( false );
+
+		// Act.
+		$result = $this->sut->needs_setup( $gateway );
+
+		// Assert - Should return true because account is not connected.
+		$this->assertTrue( $result );
+	}
+
+	/**
+	 * Test needs_setup fallback logic when method returns false and account is connected.
+	 */
+	public function test_needs_setup_fallback_when_method_returns_false_and_is_connected() {
+		// Arrange - Create a mock gateway with needs_setup method returning false.
+		$gateway = $this->getMockBuilder( 'WC_Payment_Gateway' )
+			->disableOriginalConstructor()
+			->onlyMethods( array( 'needs_setup' ) )
+			->addMethods( array( 'is_account_connected' ) )
+			->getMock();
+
+		$gateway->id = 'test_gateway';
+
+		// Expect needs_setup method to return false.
+		$gateway->expects( $this->once() )
+			->method( 'needs_setup' )
+			->willReturn( false );
+
+		// Expect is_account_connected to be called and return true.
+		$gateway->expects( $this->once() )
+			->method( 'is_account_connected' )
+			->willReturn( true );
+
+		// Act.
+		$result = $this->sut->needs_setup( $gateway );
+
+		// Assert - Should return false because account is connected.
+		$this->assertFalse( $result );
+	}
+
 	/**
 	 * Test is_in_test_mode.
 	 */
@@ -539,6 +617,129 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
 		$this->assertFalse( $this->sut->is_in_test_mode( $fake_gateway ) );
 	}

+	/**
+	 * Test is_in_test_mode with testmode property.
+	 * Use a mock gateway without is_test_mode() method to test property fallback.
+	 */
+	public function test_is_in_test_mode_with_testmode_property() {
+		// Arrange - Create a mock gateway without is_test_mode() or is_in_test_mode() methods.
+		$gateway = $this->getMockBuilder( 'WC_Payment_Gateway' )
+			->disableOriginalConstructor()
+			->getMock();
+
+		$gateway->id = 'test_gateway';
+
+		// Test with testmode property set to true.
+		$gateway->testmode = true;
+		$this->assertTrue( $this->sut->is_in_test_mode( $gateway ) );
+
+		// Test with testmode property set to false.
+		$gateway->testmode = false;
+		$this->assertFalse( $this->sut->is_in_test_mode( $gateway ) );
+
+		// Test with string values.
+		$gateway->testmode = 'yes';
+		$this->assertTrue( $this->sut->is_in_test_mode( $gateway ) );
+
+		$gateway->testmode = 'no';
+		$this->assertFalse( $this->sut->is_in_test_mode( $gateway ) );
+	}
+
+	/**
+	 * Test is_in_test_mode with mode option fallback.
+	 * Use a mock gateway without methods or properties to test get_option fallback.
+	 */
+	public function test_is_in_test_mode_with_mode_option() {
+		// Test mode option with 'test' value.
+		$gateway = $this->getMockBuilder( 'WC_Payment_Gateway' )
+			->disableOriginalConstructor()
+			->onlyMethods( array( 'get_option' ) )
+			->getMock();
+
+		$gateway->id = 'test_gateway';
+
+		// Expect get_option to be called for 'test_mode', 'testmode', and 'mode'.
+		$gateway->expects( $this->exactly( 3 ) )
+			->method( 'get_option' )
+			->willReturnCallback(
+				function ( $key, $default_value ) {
+					unset( $default_value ); // Avoid parameter not used PHPCS errors.
+					if ( 'mode' === $key ) {
+						return 'test';
+					}
+					return 'not_found';
+				}
+			);
+
+		$this->assertTrue( $this->sut->is_in_test_mode( $gateway ) );
+
+		// Test with 'sandbox' value.
+		$gateway = $this->getMockBuilder( 'WC_Payment_Gateway' )
+			->disableOriginalConstructor()
+			->onlyMethods( array( 'get_option' ) )
+			->getMock();
+
+		$gateway->id = 'test_gateway';
+
+		$gateway->expects( $this->exactly( 3 ) )
+			->method( 'get_option' )
+			->willReturnCallback(
+				function ( $key, $default_value ) {
+					unset( $default_value ); // Avoid parameter not used PHPCS errors.
+					if ( 'mode' === $key ) {
+						return 'sandbox';
+					}
+					return 'not_found';
+				}
+			);
+
+		$this->assertTrue( $this->sut->is_in_test_mode( $gateway ) );
+
+		// Test with 'live' value.
+		$gateway = $this->getMockBuilder( 'WC_Payment_Gateway' )
+			->disableOriginalConstructor()
+			->onlyMethods( array( 'get_option' ) )
+			->getMock();
+
+		$gateway->id = 'test_gateway';
+
+		$gateway->expects( $this->exactly( 3 ) )
+			->method( 'get_option' )
+			->willReturnCallback(
+				function ( $key, $default_value ) {
+					unset( $default_value ); // Avoid parameter not used PHPCS errors.
+					if ( 'mode' === $key ) {
+						return 'live';
+					}
+					return 'not_found';
+				}
+			);
+
+		$this->assertFalse( $this->sut->is_in_test_mode( $gateway ) );
+
+		// Test with 'production' value.
+		$gateway = $this->getMockBuilder( 'WC_Payment_Gateway' )
+			->disableOriginalConstructor()
+			->onlyMethods( array( 'get_option' ) )
+			->getMock();
+
+		$gateway->id = 'test_gateway';
+
+		$gateway->expects( $this->exactly( 3 ) )
+			->method( 'get_option' )
+			->willReturnCallback(
+				function ( $key, $default_value ) {
+					unset( $default_value ); // Avoid parameter not used PHPCS errors.
+					if ( 'mode' === $key ) {
+						return 'production';
+					}
+					return 'not_found';
+				}
+			);
+
+		$this->assertFalse( $this->sut->is_in_test_mode( $gateway ) );
+	}
+
 	/**
 	 * Test is_in_dev_mode.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WCCoreTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WCCoreTest.php
index b3b52527be..1cc5c914a6 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WCCoreTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WCCoreTest.php
@@ -4,7 +4,11 @@ declare( strict_types=1 );
 namespace Automattic\WooCommerce\Tests\Internal\Admin\Settings\PaymentsProviders;

 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WCCore;
+use Automattic\WooCommerce\Proxies\LegacyProxy;
+use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
+use Automattic\WooCommerce\Testing\Tools\TestingContainer;
 use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
+use PHPUnit\Framework\MockObject\MockObject;
 use WC_Unit_Test_Case;
 use WC_Gateway_BACS;
 use WC_Gateway_Cheque;
@@ -18,6 +22,11 @@ use WC_Gateway_Paypal;
  */
 class WCCoreTest extends WC_Unit_Test_Case {

+	/**
+	 * @var MockableLegacyProxy|MockObject
+	 */
+	protected $mockable_proxy;
+
 	/**
 	 * @var WCCore
 	 */
@@ -29,7 +38,16 @@ class WCCoreTest extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

-		$this->sut = new WCCore();
+		/**
+		 * TestingContainer instance.
+		 *
+		 * @var TestingContainer $container
+		 */
+		$container = wc_get_container();
+
+		$this->mockable_proxy = $container->get( LegacyProxy::class );
+
+		$this->sut = new WCCore( $this->mockable_proxy );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPaymentsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPaymentsTest.php
index c1546cae48..e661d032cf 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPaymentsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPaymentsTest.php
@@ -3,16 +3,19 @@ declare( strict_types=1 );

 namespace Automattic\WooCommerce\Tests\Internal\Admin\Settings\PaymentsProviders;

+use Automattic\Jetpack\Connection\Manager as WPCOM_Connection_Manager;
 use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Admin\PluginsHelper;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\PaymentGateway;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsRestController;
 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsService;
-use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
-use Automattic\WooCommerce\Internal\Admin\Settings\Utils;
+use Automattic\WooCommerce\Proxies\LegacyProxy;
+use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
 use Automattic\WooCommerce\Testing\Tools\TestingContainer;
 use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
+use PHPUnit\Framework\MockObject\MockObject;
 use WC_Unit_Test_Case;

 /**
@@ -23,14 +26,39 @@ use WC_Unit_Test_Case;
 class WooPaymentsTest extends WC_Unit_Test_Case {

 	/**
-	 * @var WooPaymentsRestController
+	 * @var WooPaymentsRestController|MockObject
 	 */
 	protected $mock_rest_controller;

+	/**
+	 * @var MockableLegacyProxy|MockObject
+	 */
+	protected $mockable_proxy;
+
+	/**
+	 * @var WPCOM_Connection_Manager|MockObject
+	 */
+	protected $mock_wpcom_connection_manager;
+
+	/**
+	 * @var object&MockObject
+	 */
+	protected $mock_woopayments_container;
+
+	/**
+	 * @var object&MockObject
+	 */
+	protected $mock_woopayments_account_service;
+
+	/**
+	 * @var object&MockObject
+	 */
+	protected $mock_woopayments_mode;
+
 	/**
 	 * @var WooPayments
 	 */
-	protected $sut;
+	protected WooPayments $sut;

 	/**
 	 * Set up test.
@@ -38,7 +66,33 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

-		$this->sut = new WooPayments();
+		$this->setup_woopayments_container_mock();
+		$this->setup_woopayments_mode_mock();
+		$this->setup_wpcom_connection_mock();
+		$this->setup_woopayments_account_service_mock();
+		// Mock the WC WooPayments REST controller.
+		$this->setup_woopayments_reset_controller_mock();
+		// Finally, set up the mockable proxy.
+		$this->setup_legacy_proxy_mocks();
+
+		$this->sut = new WooPayments( $this->mockable_proxy );
+	}
+
+	/**
+	 * Tear down.
+	 */
+	public function tearDown(): void {
+		$this->mockable_proxy->reset();
+
+		/**
+		 * TestingContainer instance.
+		 *
+		 * @var TestingContainer $container
+		 */
+		$container = wc_get_container();
+		$container->reset_all_resolved();
+
+		parent::tearDown();
 	}

 	/**
@@ -111,135 +165,126 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			),
 		);

+		// Arrange the WooPayments mode.
+		$this->mock_woopayments_mode
+			->method( 'is_test' )
+			->willReturn( true );
+		$this->mock_woopayments_mode
+			->method( 'is_test_mode_onboarding' )
+			->willReturn( true );
+		$this->mock_woopayments_mode
+			->method( 'is_dev' )
+			->willReturn( true );
+
+		// Arrange the WPCOM connection as fully working.
+		$this->mock_wpcom_connection();
+
 		// Arrange the version constant to meet the minimum requirements for the native in-context onboarding.
 		Constants::set_constant( 'WCPAY_VERSION_NUMBER', WooPaymentsService::EXTENSION_MINIMUM_VERSION );

-		// Mock the WooPaymentsRestController to provide REST URL paths.
-		$mock_rest_controller = $this->createMock( WooPaymentsRestController::class );
-		$mock_rest_controller
-			->method( 'get_rest_url_path' )
-			->willReturnCallback(
-				function ( $relative_path = '' ) {
-					$path = '/some/rest/for/woopayments';
-					if ( ! empty( $relative_path ) ) {
-						$path .= '/' . ltrim( $relative_path, '/' );
-					}
-					return $path;
-				}
-			);
-
 		/**
 		 * TestingContainer instance.
 		 *
 		 * @var TestingContainer $container
 		 */
 		$container = wc_get_container();
-		$container->replace( WooPaymentsRestController::class, $mock_rest_controller );
+		$container->replace( WooPaymentsRestController::class, $this->mock_rest_controller );

 		try {
 			// Act.
 			$gateway_details = $this->sut->get_details( $fake_gateway, 999 );

-			// Assert that we have all the details.
-			$this->assertEquals(
-				array(
-					'id'          => 'woocommerce_payments',
-					'_order'      => 999,
-					'title'       => 'WooPayments has a very long title that should be truncated after some length',
-					'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim…',
-					'icon'        => 'https://example.com/icon.png',
-					'supports'    => array( 'products', 'something', 'bogus' ),
-					'links'       => array(),
-					'state'       => array(
-						'enabled'           => true,
-						'account_connected' => true,
-						'needs_setup'       => true,
-						'test_mode'         => true,
-						'dev_mode'          => true,
-					),
-					'management'  => array(
-						'_links' => array(
-							'settings' => array(
-								'href' => 'https://example.com/wp-admin/admin.php?page=wc-settings&tab=checkout&section=bogus_settings&from=' . Payments::FROM_PAYMENTS_SETTINGS,
-							),
-						),
-					),
-					'plugin'      => array(
-						'_type'  => PaymentsProviders::EXTENSION_TYPE_WPORG,
-						'slug'   => 'woocommerce-payments',
-						'file'   => 'woocommerce-payments/woocommerce-payments',
-						'status' => PaymentsProviders::EXTENSION_ACTIVE,
-					),
-					'onboarding'  => array(
-						'type'                        => PaymentGateway::ONBOARDING_TYPE_NATIVE_IN_CONTEXT,
-						'state'                       => array(
-							'supported'                    => true,
-							'started'                      => true,
-							'completed'                    => true,
-							'test_mode'                    => true,
-							'test_drive_account'           => false,
-							'wpcom_has_working_connection' => false,
-							'wpcom_is_store_connected'     => false,
-							'wpcom_has_connected_owner'    => false,
-							'wpcom_is_connection_owner'    => false,
-						),
-						'messages'                    => array(
-							'not_supported' => null,
-						),
-						'_links'                      => array(
-							'onboard' => array(
-								'href' => Utils::wc_payments_settings_url( '/woopayments/onboarding', array( 'from' => Payments::FROM_PAYMENTS_SETTINGS ) ),
-							),
-							'reset'   => array(
-								'href' => rest_url( '/some/rest/for/woopayments/onboarding/reset' ),
-							),
-						),
-						'recommended_payment_methods' => array(
-							array(
-								'id'          => 'woopay',
-								'_order'      => 0,
-								'enabled'     => false,
-								'required'    => false,
-								'title'       => 'WooPay',
-								'description' => 'WooPay express checkout',
-								'icon'        => '', // The icon with an invalid URL is ignored.
-								'category'    => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
-							),
-							array(
-								'id'          => 'card',
-								'_order'      => 1,
-								'enabled'     => true,
-								'required'    => true,
-								'title'       => 'Credit/debit card (required)',
-								'description' => '<strong>Accepts</strong> <b>all major</b><em>credit</em> and <a href="#" target="_blank">debit cards</a>.',
-								'icon'        => 'https://example.com/card-icon.png',
-								'category'    => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
-							),
-							array(
-								'id'          => 'basic2',
-								'_order'      => 2,
-								'enabled'     => false,
-								'required'    => false,
-								'title'       => 'Title',
-								'description' => '',
-								'icon'        => '',
-								'category'    => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
-							),
-							array(
-								'id'          => 'basic',
-								'_order'      => 3,
-								'enabled'     => true,
-								'required'    => false,
-								'title'       => 'Title',
-								'description' => '',
-								'icon'        => '',
-								'category'    => PaymentGateway::PAYMENT_METHOD_CATEGORY_SECONDARY,
-							),
-						),
-					),
-				),
-				$gateway_details
-			);
+			// Assert - Use targeted assertions for resilient testing.
+			// Basic gateway details.
+			$this->assertSame( 'woocommerce_payments', $gateway_details['id'] );
+			$this->assertSame( 999, $gateway_details['_order'] );
+			$this->assertSame( 'WooPayments has a very long title that should be truncated after some length', $gateway_details['title'] );
+			$this->assertStringStartsWith( 'Lorem ipsum dolor sit amet', $gateway_details['description'] );
+			$this->assertSame( 'https://example.com/icon.png', $gateway_details['icon'] );
+			$this->assertSame( array( 'products', 'something', 'bogus' ), $gateway_details['supports'] );
+			$this->assertSame( array(), $gateway_details['links'] );
+
+			// State.
+			$this->assertArrayHasKey( 'state', $gateway_details );
+			$this->assertTrue( $gateway_details['state']['enabled'] );
+			$this->assertTrue( $gateway_details['state']['account_connected'] );
+			$this->assertTrue( $gateway_details['state']['needs_setup'] );
+			$this->assertTrue( $gateway_details['state']['test_mode'] );
+			$this->assertTrue( $gateway_details['state']['dev_mode'] );
+
+			// Management.
+			$this->assertArrayHasKey( 'management', $gateway_details );
+			$this->assertArrayHasKey( '_links', $gateway_details['management'] );
+			$this->assertArrayHasKey( 'settings', $gateway_details['management']['_links'] );
+			$this->assertStringContainsString( 'admin.php?page=wc-settings', $gateway_details['management']['_links']['settings']['href'] );
+
+			// Plugin.
+			$this->assertArrayHasKey( 'plugin', $gateway_details );
+			$this->assertSame( PaymentsProviders::EXTENSION_TYPE_WPORG, $gateway_details['plugin']['_type'] );
+			$this->assertSame( 'woocommerce-payments', $gateway_details['plugin']['slug'] );
+			$this->assertSame( 'woocommerce-payments/woocommerce-payments', $gateway_details['plugin']['file'] );
+			$this->assertSame( PaymentsProviders::EXTENSION_ACTIVE, $gateway_details['plugin']['status'] );
+
+			// Onboarding - Type.
+			$this->assertArrayHasKey( 'onboarding', $gateway_details );
+			$this->assertSame( PaymentGateway::ONBOARDING_TYPE_NATIVE_IN_CONTEXT, $gateway_details['onboarding']['type'] );
+
+			// Onboarding - State.
+			$this->assertArrayHasKey( 'state', $gateway_details['onboarding'] );
+			$this->assertTrue( $gateway_details['onboarding']['state']['supported'] );
+			$this->assertTrue( $gateway_details['onboarding']['state']['started'] );
+			$this->assertTrue( $gateway_details['onboarding']['state']['completed'] );
+			$this->assertTrue( $gateway_details['onboarding']['state']['test_mode'] );
+			$this->assertFalse( $gateway_details['onboarding']['state']['test_drive_account'] );
+			$this->assertTrue( $gateway_details['onboarding']['state']['wpcom_has_working_connection'] );
+
+			// Onboarding - Messages.
+			$this->assertArrayHasKey( 'messages', $gateway_details['onboarding'] );
+			$this->assertArrayHasKey( 'not_supported', $gateway_details['onboarding']['messages'] );
+			$this->assertNull( $gateway_details['onboarding']['messages']['not_supported'] );
+
+			// Onboarding - Links.
+			$this->assertArrayHasKey( '_links', $gateway_details['onboarding'] );
+			$this->assertArrayHasKey( 'onboard', $gateway_details['onboarding']['_links'] );
+			$this->assertStringContainsString( '/woopayments/onboarding', $gateway_details['onboarding']['_links']['onboard']['href'] );
+			$this->assertArrayHasKey( 'reset', $gateway_details['onboarding']['_links'] );
+			$this->assertStringContainsString( '/onboarding/reset', $gateway_details['onboarding']['_links']['reset']['href'] );
+
+			// Onboarding - Recommended payment methods.
+			$this->assertArrayHasKey( 'recommended_payment_methods', $gateway_details['onboarding'] );
+			$recommended_pms = $gateway_details['onboarding']['recommended_payment_methods'];
+			$this->assertCount( 4, $recommended_pms );
+
+			// Check first payment method (woopay) - ordered first, disabled, invalid icon removed.
+			$this->assertSame( 'woopay', $recommended_pms[0]['id'] );
+			$this->assertSame( 0, $recommended_pms[0]['_order'] );
+			$this->assertFalse( $recommended_pms[0]['enabled'] );
+			$this->assertFalse( $recommended_pms[0]['required'] );
+			$this->assertSame( 'WooPay', $recommended_pms[0]['title'] );
+			$this->assertSame( '', $recommended_pms[0]['icon'] ); // Invalid URL removed.
+			$this->assertSame( PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY, $recommended_pms[0]['category'] );
+
+			// Check second payment method (card) - required, HTML tags stripped but content preserved.
+			$this->assertSame( 'card', $recommended_pms[1]['id'] );
+			$this->assertSame( 1, $recommended_pms[1]['_order'] );
+			$this->assertTrue( $recommended_pms[1]['enabled'] );
+			$this->assertTrue( $recommended_pms[1]['required'] );
+			$this->assertSame( 'Credit/debit card (required)', $recommended_pms[1]['title'] );
+			$this->assertStringContainsString( 'Accepts', $recommended_pms[1]['description'] );
+			$this->assertStringContainsString( 'debit cards', $recommended_pms[1]['description'] );
+			$this->assertSame( 'https://example.com/card-icon.png', $recommended_pms[1]['icon'] );
+
+			// Check third payment method (basic2) - uses priority, category normalized.
+			$this->assertSame( 'basic2', $recommended_pms[2]['id'] );
+			$this->assertSame( 2, $recommended_pms[2]['_order'] );
+			$this->assertFalse( $recommended_pms[2]['enabled'] );
+			$this->assertSame( PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY, $recommended_pms[2]['category'] ); // 'unknown' normalized to primary.
+
+			// Check fourth payment method (basic) - no order, placed last, secondary category.
+			$this->assertSame( 'basic', $recommended_pms[3]['id'] );
+			$this->assertSame( 3, $recommended_pms[3]['_order'] );
+			$this->assertTrue( $recommended_pms[3]['enabled'] );
+			$this->assertSame( PaymentGateway::PAYMENT_METHOD_CATEGORY_SECONDARY, $recommended_pms[3]['category'] );
 		} finally {
 			// Clean up.
 			Constants::clear_constants();
@@ -265,23 +310,23 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			),
 		);

+		// Arrange the WooPayments mode.
+		$this->mock_woopayments_mode
+			->method( 'is_test' )
+			->willReturn( true );
+		$this->mock_woopayments_mode
+			->method( 'is_test_mode_onboarding' )
+			->willReturn( false );
+		$this->mock_woopayments_mode
+			->method( 'is_dev' )
+			->willReturn( true );
+
+		// Arrange the WPCOM connection as fully working.
+		$this->mock_wpcom_connection();
+
 		// Arrange the version constant to meet the minimum requirements.
 		Constants::set_constant( 'WCPAY_VERSION_NUMBER', WooPaymentsService::EXTENSION_MINIMUM_VERSION );

-		// Mock the WooPaymentsRestController to provide REST URL paths.
-		$mock_rest_controller = $this->createMock( WooPaymentsRestController::class );
-		$mock_rest_controller
-			->method( 'get_rest_url_path' )
-			->willReturnCallback(
-				function ( $relative_path = '' ) {
-					$path = '/some/rest/for/woopayments';
-					if ( ! empty( $relative_path ) ) {
-						$path .= '/' . ltrim( $relative_path, '/' );
-					}
-					return $path;
-				}
-			);
-
 		// Mock the WooPaymentsService to return onboarding details.
 		$mock_service = $this->createMock( WooPaymentsService::class );
 		$mock_service
@@ -317,7 +362,7 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 		 * @var TestingContainer $container
 		 */
 		$container = wc_get_container();
-		$container->replace( WooPaymentsRestController::class, $mock_rest_controller );
+		$container->replace( WooPaymentsRestController::class, $this->mock_rest_controller );
 		$container->replace( WooPaymentsService::class, $mock_service );

 		try {
@@ -363,23 +408,23 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			),
 		);

+		// Arrange the WooPayments mode.
+		$this->mock_woopayments_mode
+			->method( 'is_test' )
+			->willReturn( false );
+		$this->mock_woopayments_mode
+			->method( 'is_test_mode_onboarding' )
+			->willReturn( false );
+		$this->mock_woopayments_mode
+			->method( 'is_dev' )
+			->willReturn( false );
+
+		// Arrange the WPCOM connection as fully working.
+		$this->mock_wpcom_connection();
+
 		// Arrange the version constant.
 		Constants::set_constant( 'WCPAY_VERSION_NUMBER', WooPaymentsService::EXTENSION_MINIMUM_VERSION );

-		// Mock the WooPaymentsRestController to provide REST URL paths.
-		$mock_rest_controller = $this->createMock( WooPaymentsRestController::class );
-		$mock_rest_controller
-			->method( 'get_rest_url_path' )
-			->willReturnCallback(
-				function ( $relative_path = '' ) {
-					$path = '/some/rest/for/woopayments';
-					if ( ! empty( $relative_path ) ) {
-						$path .= '/' . ltrim( $relative_path, '/' );
-					}
-					return $path;
-				}
-			);
-
 		// Mock the service to throw an exception.
 		$mock_service = $this->createMock( WooPaymentsService::class );
 		$mock_service
@@ -392,7 +437,7 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 		 * @var TestingContainer $container
 		 */
 		$container = wc_get_container();
-		$container->replace( WooPaymentsRestController::class, $mock_rest_controller );
+		$container->replace( WooPaymentsRestController::class, $this->mock_rest_controller );
 		$container->replace( WooPaymentsService::class, $mock_service );

 		try {
@@ -418,11 +463,6 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 		// Arrange.
 		$fake_gateway = new FakePaymentGateway( 'woocommerce_payments', array() );

-		// Load the mock WC_Payments_Utils if the real class doesn't exist.
-		if ( ! class_exists( '\WC_Payments_Utils' ) ) {
-			require_once __DIR__ . '/../Mocks/WCPaymentsUtils.php';
-		}
-
 		// Act.
 		$is_supported = $this->sut->is_onboarding_supported( $fake_gateway, 'US' );

@@ -446,11 +486,6 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			)
 		);

-		// Load the mock WC_Payments_Utils if the real class doesn't exist.
-		if ( ! class_exists( '\WC_Payments_Utils' ) ) {
-			require_once __DIR__ . '/../Mocks/WCPaymentsUtils.php';
-		}
-
 		// Act - testing with a country definitely not in the supported list.
 		$is_supported = $this->sut->is_onboarding_supported( $fake_gateway, 'XX' );

@@ -541,6 +576,17 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			),
 		);

+		// Arrange the WooPayments mode.
+		$this->mock_woopayments_mode
+			->method( 'is_test' )
+			->willReturn( false );
+		$this->mock_woopayments_mode
+			->method( 'is_test_mode_onboarding' )
+			->willReturn( false );
+		$this->mock_woopayments_mode
+			->method( 'is_dev' )
+			->willReturn( false );
+
 		// Arrange the version constant.
 		Constants::set_constant( 'WCPAY_VERSION_NUMBER', WooPaymentsService::EXTENSION_MINIMUM_VERSION );

@@ -569,9 +615,7 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			'woocommerce_payments',
 			array( 'onboarding_supported' => null )
 		);
-		if ( ! class_exists( '\WC_Payments_Utils' ) ) {
-			require_once __DIR__ . '/../Mocks/WCPaymentsUtils.php';
-		}
+
 		$this->assertTrue( $this->sut->is_onboarding_supported( $fake_gateway, 'GB' ) );
 	}

@@ -592,6 +636,17 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			)
 		);

+		// Arrange the WooPayments mode.
+		$this->mock_woopayments_mode
+			->method( 'is_test' )
+			->willReturn( false );
+		$this->mock_woopayments_mode
+			->method( 'is_test_mode_onboarding' )
+			->willReturn( false );
+		$this->mock_woopayments_mode
+			->method( 'is_dev' )
+			->willReturn( false );
+
 		// Arrange the version constant.
 		Constants::set_constant( 'WCPAY_VERSION_NUMBER', WooPaymentsService::EXTENSION_MINIMUM_VERSION );

@@ -613,4 +668,881 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
 			Constants::clear_constants();
 		}
 	}
+
+	/**
+	 * Test enhance_extension_suggestion with compatible installed version.
+	 */
+	public function test_enhance_extension_suggestion_with_compatible_installed_version() {
+		// Arrange - Mock PluginsHelper to return compatible version (>= 9.3.0).
+		$this->mockable_proxy->register_static_mocks(
+			array(
+				PluginsHelper::class => array(
+					'get_plugin_data' => function ( $plugin_file ) {
+						if ( 'woocommerce-payments/woocommerce-payments.php' === $plugin_file ) {
+							return array(
+								'Version' => '10.0.0', // Compatible version >= 9.3.0.
+							);
+						}
+						return false;
+					},
+				),
+			)
+		);
+
+		// Arrange - Extension suggestion for installed WooPayments with compatible version.
+		$extension_suggestion = array(
+			'id'         => 'woocommerce-payments',
+			'plugin'     => array(
+				'file'   => 'woocommerce-payments/woocommerce-payments',
+				'status' => PaymentsProviders::EXTENSION_ACTIVE,
+			),
+			'onboarding' => array(),
+		);
+
+		// Arrange the WPCOM connection as fully working.
+		$this->mock_wpcom_connection();
+
+		// Act.
+		$enhanced = $this->sut->enhance_extension_suggestion( $extension_suggestion );
+
+		// Assert - Should set native in-context onboarding type.
+		$this->assertArrayHasKey( 'onboarding', $enhanced );
+		$this->assertArrayHasKey( 'type', $enhanced['onboarding'] );
+		$this->assertSame( PaymentGateway::ONBOARDING_TYPE_NATIVE_IN_CONTEXT, $enhanced['onboarding']['type'] );
+
+		// Assert - Should include WPCOM connection state.
+		$this->assertArrayHasKey( 'state', $enhanced['onboarding'] );
+		$this->assertArrayHasKey( 'wpcom_has_working_connection', $enhanced['onboarding']['state'] );
+		$this->assertArrayHasKey( 'wpcom_is_store_connected', $enhanced['onboarding']['state'] );
+
+		// Assert - Should not include preload link when WPCOM is connected.
+		$this->assertArrayNotHasKey( 'preload', $enhanced['onboarding']['_links'] ?? array() );
+	}
+
+	/**
+	 * Test enhance_extension_suggestion with incompatible installed version.
+	 */
+	public function test_enhance_extension_suggestion_with_incompatible_installed_version() {
+		// Arrange - Mock PluginsHelper to return old version.
+		$this->mockable_proxy->register_static_mocks(
+			array(
+				PluginsHelper::class => array(
+					'get_plugin_data' => function ( $plugin_file ) {
+						if ( 'woocommerce-payments/woocommerce-payments.php' === $plugin_file ) {
+							return array(
+								'Version' => '3.0.0', // Below minimum.
+							);
+						}
+						return false;
+					},
+				),
+			)
+		);
+
+		$extension_suggestion = array(
+			'id'         => 'woocommerce-payments',
+			'plugin'     => array(
+				'file'   => 'woocommerce-payments/woocommerce-payments',
+				'status' => PaymentsProviders::EXTENSION_ACTIVE,
+			),
+			'onboarding' => array(),
+		);
+
+		// Act.
+		$enhanced = $this->sut->enhance_extension_suggestion( $extension_suggestion );
+
+		// Assert - Should fall back to external onboarding type (from parent).
+		$this->assertArrayHasKey( 'onboarding', $enhanced );
+		$this->assertArrayHasKey( 'type', $enhanced['onboarding'] );
+		$this->assertSame( PaymentGateway::ONBOARDING_TYPE_EXTERNAL, $enhanced['onboarding']['type'] );
+	}
+
+	/**
+	 * Test enhance_extension_suggestion when extension not installed.
+	 */
+	public function test_enhance_extension_suggestion_when_not_installed() {
+		// Arrange - Extension suggestion for not-yet-installed WooPayments.
+		$extension_suggestion = array(
+			'id'         => 'woocommerce-payments',
+			'plugin'     => array(
+				'file'   => '',
+				'status' => PaymentsProviders::EXTENSION_NOT_INSTALLED,
+			),
+			'onboarding' => array(),
+		);
+
+		// Act.
+		$enhanced = $this->sut->enhance_extension_suggestion( $extension_suggestion );
+
+		// Assert - Should assume latest version and set native in-context.
+		$this->assertArrayHasKey( 'onboarding', $enhanced );
+		$this->assertArrayHasKey( 'type', $enhanced['onboarding'] );
+		$this->assertSame( PaymentGateway::ONBOARDING_TYPE_NATIVE_IN_CONTEXT, $enhanced['onboarding']['type'] );
+	}
+
+	/**
+	 * Test enhance_extension_suggestion includes preload link without WPCOM connection.
+	 */
+	public function test_enhance_extension_suggestion_includes_preload_link_without_wpcom() {
+		// Arrange - Mock WPCOM connection as not working.
+		$this->mock_wpcom_connection_manager
+			->method( 'is_connected' )
+			->willReturn( false );
+		$this->mock_wpcom_connection_manager
+			->method( 'has_connected_owner' )
+			->willReturn( false );
+
+		$extension_suggestion = array(
+			'id'         => 'woocommerce-payments',
+			'plugin'     => array(
+				'file'   => '',
+				'status' => PaymentsProviders::EXTENSION_NOT_INSTALLED,
+			),
+			'onboarding' => array(),
+		);
+
+		// Arrange the WPCOM connection as not working.
+		$this->mock_wpcom_connection( false, false, false );
+
+		/**
+		 * TestingContainer instance.
+		 *
+		 * @var TestingContainer $container
+		 */
+		$container = wc_get_container();
+		$container->replace( WooPaymentsRestController::class, $this->mock_rest_controller );
+
+		try {
+			// Act.
+			$enhanced = $this->sut->enhance_extension_suggestion( $extension_suggestion );
+
+			// Assert - Should include preload link when WPCOM not connected.
+			$this->assertArrayHasKey( 'onboarding', $enhanced );
+			$this->assertArrayHasKey( '_links', $enhanced['onboarding'] );
+			$this->assertArrayHasKey( 'preload', $enhanced['onboarding']['_links'] );
+			$this->assertStringContainsString( '/onboarding/preload', $enhanced['onboarding']['_links']['preload']['href'] );
+
+			// Assert - WPCOM connection state should show not connected.
+			$this->assertArrayHasKey( 'state', $enhanced['onboarding'] );
+			$this->assertFalse( $enhanced['onboarding']['state']['wpcom_has_working_connection'] );
+		} finally {
+			$container->reset_replacement( WooPaymentsRestController::class );
+		}
+	}
+
+	/**
+	 * Test needs_setup returns true when account is not connected.
+	 */
+	public function test_needs_setup_when_account_not_connected() {
+		// Arrange - Gateway without account connection.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => false,
+			)
+		);
+
+		// Act.
+		$needs_setup = $this->sut->needs_setup( $fake_gateway );
+
+		// Assert - Should need setup when no account connected.
+		$this->assertTrue( $needs_setup );
+	}
+
+	/**
+	 * Test needs_setup returns false when test-drive account exists.
+	 */
+	public function test_needs_setup_with_test_drive_account() {
+		// Arrange - Gateway with connected account.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => true,
+			)
+		);
+
+		// Arrange - Mock test-drive account status.
+		$this->mock_woopayments_account_service
+			->method( 'get_account_status_data' )
+			->willReturn(
+				array(
+					'testDrive' => true,
+					'isLive'    => false,
+				)
+			);
+
+		// Act.
+		$needs_setup = $this->sut->needs_setup( $fake_gateway );
+
+		// Assert - Test-drive accounts don't need setup.
+		$this->assertFalse( $needs_setup );
+	}
+
+	/**
+	 * Test needs_setup delegates to parent when account connected and not test-drive.
+	 */
+	public function test_needs_setup_delegates_to_parent_when_normal_account() {
+		// Arrange - Gateway with connected account that needs setup (per parent logic).
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => true,
+				'needs_setup'       => true,
+			)
+		);
+
+		// Arrange - Mock normal account (not test-drive).
+		$this->mock_woopayments_account_service
+			->method( 'get_account_status_data' )
+			->willReturn(
+				array(
+					'testDrive' => false,
+					'isLive'    => true,
+				)
+			);
+
+		// Act.
+		$needs_setup = $this->sut->needs_setup( $fake_gateway );
+
+		// Assert - Should delegate to parent and return its value.
+		$this->assertTrue( $needs_setup );
+	}
+
+	/**
+	 * Test needs_setup delegates to parent and returns false when setup complete.
+	 */
+	public function test_needs_setup_returns_false_when_setup_complete() {
+		// Arrange - Gateway fully configured.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => true,
+				'needs_setup'       => false,
+			)
+		);
+
+		// Arrange - Mock normal account.
+		$this->mock_woopayments_account_service
+			->method( 'get_account_status_data' )
+			->willReturn(
+				array(
+					'testDrive' => false,
+					'isLive'    => true,
+				)
+			);
+
+		// Act.
+		$needs_setup = $this->sut->needs_setup( $fake_gateway );
+
+		// Assert - Should return false when setup complete.
+		$this->assertFalse( $needs_setup );
+	}
+
+	/**
+	 * Test is_in_test_mode returns true when WC_Payments mode reports test mode.
+	 */
+	public function test_is_in_test_mode_when_woopayments_reports_test() {
+		// Arrange.
+		$fake_gateway = new FakePaymentGateway( 'woocommerce_payments', array() );
+
+		// Arrange - Mock WooPayments mode to return test mode.
+		$this->mock_woopayments_mode
+			->method( 'is_test' )
+			->willReturn( true );
+
+		// Act.
+		$is_test_mode = $this->sut->is_in_test_mode( $fake_gateway );
+
+		// Assert.
+		$this->assertTrue( $is_test_mode );
+	}
+
+	/**
+	 * Test is_in_test_mode returns false when WC_Payments mode reports live mode.
+	 */
+	public function test_is_in_test_mode_when_woopayments_reports_live() {
+		// Arrange.
+		$fake_gateway = new FakePaymentGateway( 'woocommerce_payments', array() );
+
+		// Arrange - Mock WooPayments mode to return live mode.
+		$this->mock_woopayments_mode
+			->method( 'is_test' )
+			->willReturn( false );
+
+		// Act.
+		$is_test_mode = $this->sut->is_in_test_mode( $fake_gateway );
+
+		// Assert.
+		$this->assertFalse( $is_test_mode );
+	}
+
+	/**
+	 * Test is_in_test_mode delegates to parent when WC_Payments unavailable.
+	 */
+	public function test_is_in_test_mode_delegates_to_parent_when_woopayments_unavailable() {
+		// Arrange - Gateway with test mode flag.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'test_mode' => true,
+			)
+		);
+
+		// Arrange - Mock WC_Payments as not available.
+		$this->mockable_proxy->register_function_mocks(
+			array(
+				'class_exists' => function ( $class_name ) {
+					if ( 'WC_Payments' === $class_name ) {
+						return false;
+					}
+					return false;
+				},
+			)
+		);
+
+		// Act.
+		$is_test_mode = $this->sut->is_in_test_mode( $fake_gateway );
+
+		// Assert - Should delegate to parent when WC_Payments unavailable.
+		$this->assertTrue( $is_test_mode );
+	}
+
+	/**
+	 * Test is_in_dev_mode returns true when WC_Payments mode reports dev mode.
+	 */
+	public function test_is_in_dev_mode_when_woopayments_reports_dev() {
+		// Arrange.
+		$fake_gateway = new FakePaymentGateway( 'woocommerce_payments', array() );
+
+		// Arrange - Mock WooPayments mode to return dev mode.
+		$this->mock_woopayments_mode
+			->method( 'is_dev' )
+			->willReturn( true );
+
+		// Act.
+		$is_dev_mode = $this->sut->is_in_dev_mode( $fake_gateway );
+
+		// Assert.
+		$this->assertTrue( $is_dev_mode );
+	}
+
+	/**
+	 * Test is_in_dev_mode returns false when WC_Payments mode reports not dev mode.
+	 */
+	public function test_is_in_dev_mode_when_woopayments_reports_not_dev() {
+		// Arrange.
+		$fake_gateway = new FakePaymentGateway( 'woocommerce_payments', array() );
+
+		// Arrange - Mock WooPayments mode to return not dev mode.
+		$this->mock_woopayments_mode
+			->method( 'is_dev' )
+			->willReturn( false );
+
+		// Act.
+		$is_dev_mode = $this->sut->is_in_dev_mode( $fake_gateway );
+
+		// Assert.
+		$this->assertFalse( $is_dev_mode );
+	}
+
+	/**
+	 * Test is_in_dev_mode delegates to parent when WC_Payments unavailable.
+	 */
+	public function test_is_in_dev_mode_delegates_to_parent_when_woopayments_unavailable() {
+		// Arrange - Gateway with dev mode flag.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'dev_mode' => true,
+			)
+		);
+
+		// Arrange - Mock WC_Payments as not available.
+		$this->mockable_proxy->register_function_mocks(
+			array(
+				'class_exists' => function ( $class_name ) {
+					if ( 'WC_Payments' === $class_name ) {
+						return false;
+					}
+					return false;
+				},
+			)
+		);
+
+		// Act.
+		$is_dev_mode = $this->sut->is_in_dev_mode( $fake_gateway );
+
+		// Assert - Should delegate to parent when WC_Payments unavailable.
+		$this->assertTrue( $is_dev_mode );
+	}
+
+	/**
+	 * Test is_in_test_mode_onboarding returns true when WC_Payments mode reports test mode onboarding.
+	 */
+	public function test_is_in_test_mode_onboarding_when_woopayments_reports_test_mode() {
+		// Arrange.
+		$fake_gateway = new FakePaymentGateway( 'woocommerce_payments', array() );
+
+		// Arrange - Mock WooPayments mode to return test mode onboarding.
+		$this->mock_woopayments_mode
+			->method( 'is_test_mode_onboarding' )
+			->willReturn( true );
+
+		// Act.
+		$is_test_mode_onboarding = $this->sut->is_in_test_mode_onboarding( $fake_gateway );
+
+		// Assert.
+		$this->assertTrue( $is_test_mode_onboarding );
+	}
+
+	/**
+	 * Test is_in_test_mode_onboarding returns false when WC_Payments mode reports not test mode onboarding.
+	 */
+	public function test_is_in_test_mode_onboarding_when_woopayments_reports_not_test_mode() {
+		// Arrange.
+		$fake_gateway = new FakePaymentGateway( 'woocommerce_payments', array() );
+
+		// Arrange - Mock WooPayments mode to return not test mode onboarding.
+		$this->mock_woopayments_mode
+			->method( 'is_test_mode_onboarding' )
+			->willReturn( false );
+
+		// Act.
+		$is_test_mode_onboarding = $this->sut->is_in_test_mode_onboarding( $fake_gateway );
+
+		// Assert.
+		$this->assertFalse( $is_test_mode_onboarding );
+	}
+
+	/**
+	 * Test is_in_test_mode_onboarding delegates to parent when WC_Payments unavailable.
+	 */
+	public function test_is_in_test_mode_onboarding_delegates_to_parent_when_woopayments_unavailable() {
+		// Arrange - Gateway with test mode onboarding flag.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'test_mode_onboarding' => true,
+			)
+		);
+
+		// Arrange - Mock WC_Payments as not available.
+		$this->mockable_proxy->register_function_mocks(
+			array(
+				'class_exists' => function ( $class_name ) {
+					if ( 'WC_Payments' === $class_name ) {
+						return false;
+					}
+					return false;
+				},
+			)
+		);
+
+		// Act.
+		$is_test_mode_onboarding = $this->sut->is_in_test_mode_onboarding( $fake_gateway );
+
+		// Assert - Should delegate to parent when WC_Payments unavailable.
+		$this->assertTrue( $is_test_mode_onboarding );
+	}
+
+	/**
+	 * Test get_onboarding_url with connected account returns URL with base params only.
+	 */
+	public function test_get_onboarding_url_with_connected_account() {
+		// Arrange - Gateway with connected account.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => true,
+			)
+		);
+
+		// Act.
+		$onboarding_url = $this->sut->get_onboarding_url( $fake_gateway );
+
+		// Assert - Should contain base URL and standard params but NO test_drive params.
+		$this->assertStringContainsString( 'https://example.com/wp-admin/woopayments/connect-url', $onboarding_url );
+		$this->assertStringContainsString( 'from=', $onboarding_url );
+		$this->assertStringContainsString( 'source=', $onboarding_url );
+		$this->assertStringContainsString( 'redirect_to_settings_page=true', $onboarding_url );
+		// Should NOT include test_drive params for connected accounts.
+		$this->assertStringNotContainsString( 'test_drive=true', $onboarding_url );
+		$this->assertStringNotContainsString( 'auto_start_test_drive_onboarding=true', $onboarding_url );
+	}
+
+	/**
+	 * Test get_onboarding_url for new store in coming soon mode with already selling profile.
+	 */
+	public function test_get_onboarding_url_coming_soon_mode_already_selling_online() {
+		// Arrange - Gateway without connected account.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => false,
+			)
+		);
+
+		// Arrange - Store in coming soon mode.
+		update_option( 'woocommerce_coming_soon', 'yes' );
+
+		// Arrange - Onboarding profile: already selling online.
+		update_option(
+			'woocommerce_onboarding_profile',
+			array(
+				'business_choice'       => 'im_already_selling',
+				'selling_online_answer' => 'yes_im_selling_online',
+			)
+		);
+
+		try {
+			// Act.
+			$onboarding_url = $this->sut->get_onboarding_url( $fake_gateway );
+
+			// Assert - Should do LIVE onboarding (no test_drive params).
+			$this->assertStringContainsString( 'https://example.com/wp-admin/woopayments/connect-url', $onboarding_url );
+			$this->assertStringNotContainsString( 'test_drive=true', $onboarding_url );
+			$this->assertStringNotContainsString( 'auto_start_test_drive_onboarding=true', $onboarding_url );
+		} finally {
+			// Clean up.
+			delete_option( 'woocommerce_coming_soon' );
+			delete_option( 'woocommerce_onboarding_profile' );
+		}
+	}
+
+	/**
+	 * Test get_onboarding_url for new store in coming soon mode with both online and offline.
+	 */
+	public function test_get_onboarding_url_coming_soon_mode_selling_both() {
+		// Arrange - Gateway without connected account.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => false,
+			)
+		);
+
+		// Arrange - Store in coming soon mode.
+		update_option( 'woocommerce_coming_soon', 'yes' );
+
+		// Arrange - Onboarding profile: selling both online and offline.
+		update_option(
+			'woocommerce_onboarding_profile',
+			array(
+				'business_choice'       => 'im_already_selling',
+				'selling_online_answer' => 'im_selling_both_online_and_offline',
+			)
+		);
+
+		try {
+			// Act.
+			$onboarding_url = $this->sut->get_onboarding_url( $fake_gateway );
+
+			// Assert - Should do LIVE onboarding (no test_drive params).
+			$this->assertStringContainsString( 'https://example.com/wp-admin/woopayments/connect-url', $onboarding_url );
+			$this->assertStringNotContainsString( 'test_drive=true', $onboarding_url );
+			$this->assertStringNotContainsString( 'auto_start_test_drive_onboarding=true', $onboarding_url );
+		} finally {
+			// Clean up.
+			delete_option( 'woocommerce_coming_soon' );
+			delete_option( 'woocommerce_onboarding_profile' );
+		}
+	}
+
+	/**
+	 * Test get_onboarding_url for new store in coming soon mode with other profile.
+	 */
+	public function test_get_onboarding_url_coming_soon_mode_other_profile() {
+		// Arrange - Gateway without connected account.
+		$fake_gateway = new FakePaymentGateway(
+			'woocommerce_payments',
+			array(
+				'enabled'           => true,
+				'account_connected' => false,
+			)
+		);
+
+		// Arrange - Store in coming soon mode.
+		update_option( 'woocommerce_coming_soon', 'yes' );
+
+		// Arrange - Onboarding profile: not already selling.
+		update_option(
+			'woocommerce_onboarding_profile',
+			array(
+				'business_choice' => 'im_just_starting',
+			)
+		);
+
+		try {
+			// Act.
+			$onboarding_url = $this->sut->get_onboarding_url( $fake_gateway );
+
+			// Assert - Should do TEST-DRIVE onboarding (includes test_drive params).
+			$this->assertStringContainsString( 'https://example.com/wp-admin/woopayments/connect-url', $onboarding_url );
+			$this->assertStringContainsString( 'test_drive=true', $onboarding_url );
+			$this->assertStringContainsString( 'auto_start_test_drive_onboarding=true', $onboarding_url );
+		} finally {
+			// Clean up.
+			delete_option( 'woocommerce_coming_soon' );
+			delete_option( 'woocommerce_onboarding_profile' );
+		}
+	}
+
+	/**
+	 * Setup WPCOM connection manager mock.
+	 *
+	 * Creates a mock of the WPCOM connection manager and configures it to
+	 * simulate a connected Jetpack site with a connected owner.
+	 */
+	private function setup_wpcom_connection_mock(): void {
+		$this->mock_wpcom_connection_manager = $this->getMockBuilder( WPCOM_Connection_Manager::class )
+													->onlyMethods(
+														array(
+															'is_connected',
+															'has_connected_owner',
+															'is_connection_owner',
+														)
+													)
+													->getMock();
+	}
+
+	/**
+	 * Configure the WPCOM connection manager mock.
+	 *
+	 * @param bool $is_connected         Whether the site is connected to WPCOM.
+	 * @param bool $has_connected_owner  Whether the connected owner exists.
+	 * @param bool $is_connection_owner  Whether the current user is the connection owner.
+	 */
+	private function mock_wpcom_connection( bool $is_connected = true, bool $has_connected_owner = true, bool $is_connection_owner = true ): void {
+		$this->mock_wpcom_connection_manager
+			->method( 'is_connected' )
+			->willReturn( $is_connected );
+		$this->mock_wpcom_connection_manager
+			->method( 'has_connected_owner' )
+			->willReturn( $has_connected_owner );
+		$this->mock_wpcom_connection_manager
+			->method( 'is_connection_owner' )
+			->willReturn( $is_connection_owner );
+	}
+
+	/**
+	 * Setup WooPayments DI container mock.
+	 *
+	 * Creates a mock WooPayments DI container with standard methods for testing.
+	 */
+	private function setup_woopayments_container_mock(): void {
+		$this->mock_woopayments_container = $this->getMockBuilder( \stdClass::class )
+												->addMethods( array( 'get' ) )
+												->getMock();
+	}
+
+	/**
+	 * Setup WooPayments WooPaymentsRestController mock.
+	 */
+	private function setup_woopayments_reset_controller_mock(): void {
+		// Mock the WooPaymentsRestController to provide REST URL paths.
+		$this->mock_rest_controller = $this->createMock( WooPaymentsRestController::class );
+		$this->mock_rest_controller
+			->method( 'get_rest_url_path' )
+			->willReturnCallback(
+				function ( $relative_path = '' ) {
+					$path = '/some/rest/for/woopayments';
+					if ( ! empty( $relative_path ) ) {
+						$path .= '/' . ltrim( $relative_path, '/' );
+					}
+					return $path;
+				}
+			);
+	}
+
+	/**
+	 * Setup WooPayments account service mock.
+	 *
+	 * Creates a mock account service with standard methods for testing.
+	 */
+	private function setup_woopayments_account_service_mock(): void {
+		$this->mock_woopayments_account_service = $this->getMockBuilder( \stdClass::class )
+														->addMethods( array( 'is_stripe_account_valid', 'get_account_status_data' ) )
+														->getMock();
+	}
+
+	/**
+	 * Setup WooPayments mode mock.
+	 *
+	 * Creates a mock WooPayments mode with standard methods for testing.
+	 */
+	private function setup_woopayments_mode_mock(): void {
+		$this->mock_woopayments_mode = $this->getMockBuilder( \stdClass::class )
+											->addMethods( array( 'is_test', 'is_test_mode_onboarding', 'is_dev' ) )
+											->getMock();
+	}
+
+	/**
+	 * Setup legacy proxy mocks.
+	 *
+	 * Configures the mockable legacy proxy with class, static, and function mocks
+	 * needed for testing the payments functionality.
+	 */
+	private function setup_legacy_proxy_mocks(): void {
+		/**
+		 * TestingContainer instance.
+		 *
+		 * @var TestingContainer $container
+		 */
+		$container = wc_get_container();
+
+		$this->mockable_proxy = $container->get( LegacyProxy::class );
+		$this->mockable_proxy->register_class_mocks(
+			array(
+				WPCOM_Connection_Manager::class  => $this->mock_wpcom_connection_manager,
+				WooPaymentsRestController::class => $this->mock_rest_controller,
+			)
+		);
+
+		// We have no way of knowing if the container has already resolved the mocked classes,
+		// so we need to reset all resolved instances.
+		$container->reset_all_resolved();
+
+		$this->mockable_proxy->register_static_mocks(
+			array(
+				'WC_Payments_Utils'   => array(
+					'supported_countries' => function () {
+						return $this->get_woopayments_supported_countries();
+					},
+				),
+				'WC_Payments'         => array(
+					'get_account_service' => function () {
+						return $this->mock_woopayments_account_service;
+					},
+					'mode'                => function () {
+						return $this->mock_woopayments_mode;
+					},
+				),
+				'WC_Payments_Account' => array(
+					'get_connect_url' => function () {
+						return 'https://example.com/wp-admin/woopayments/connect-url?existing=param';
+					},
+				),
+				PluginsHelper::class  => array(
+					'get_plugin_data' => function ( $plugin_file ) {
+						if ( 'woocommerce-payments/woocommerce-payments.php' === $plugin_file ) {
+							return array(
+								'Version' => '4.0.0',
+							);
+						}
+
+						return false;
+					},
+				),
+			)
+		);
+
+		$this->mockable_proxy->register_function_mocks(
+			array(
+				'class_exists'        => function ( $class_name ) {
+					if ( in_array( $class_name, array( 'WC_Payments', 'WC_Payments_Account', 'WC_Payments_Utils' ), true ) ) {
+						return true;
+					}
+
+					// For other classes, delegate to PHP's native class_exists()
+					// so tests mirror runtime reality.
+					return \class_exists( $class_name );
+				},
+				'method_exists'       => function ( $object_or_class, $method_name ) {
+					if ( is_object( $object_or_class ) && $object_or_class === $this->mock_woopayments_mode ) {
+						if ( in_array( $method_name, array( 'is_test', 'is_test_mode_onboarding', 'is_dev' ), true ) ) {
+							return true;
+						}
+					}
+
+					if ( is_object( $object_or_class ) && $object_or_class === $this->mock_woopayments_account_service ) {
+						if ( in_array( $method_name, array( 'get_account_status_data' ), true ) ) {
+							return true;
+						}
+					}
+					if ( is_string( $object_or_class ) ) {
+						if ( 'WC_Payments' === $object_or_class ) {
+							if ( in_array( $method_name, array( 'mode' ), true ) ) {
+								return true;
+							}
+						}
+
+						if ( 'WC_Payments_Account' === $object_or_class ) {
+							if ( in_array( $method_name, array( 'get_connect_url' ), true ) ) {
+								return true;
+							}
+						}
+
+						if ( 'WC_Payments_Utils' === $object_or_class ) {
+							if ( in_array( $method_name, array( 'supported_countries' ), true ) ) {
+								return true;
+							}
+						}
+					}
+					// For other methods, delegate to PHP's native method_exists()
+					// so tests mirror runtime reality.
+					return \method_exists( $object_or_class, $method_name );
+				},
+				'is_callable'         => function () {
+					if ( func_num_args() > 0 ) {
+						$callable = func_get_arg( 0 );
+						if ( is_array( $callable ) && count( $callable ) === 2 ) {
+							$object_or_class = $callable[0];
+							$method_name     = $callable[1];
+							if ( $object_or_class === $this->mock_woopayments_mode &&
+								in_array( $method_name, array( 'is_test', 'is_test_mode_onboarding', 'is_dev' ), true ) ) {
+								return true;
+							}
+
+							if ( $object_or_class === $this->mock_woopayments_account_service &&
+								in_array( $method_name, array( 'get_account_status_data' ), true ) ) {
+								return true;
+							}
+						} elseif ( is_string( $callable ) ) {
+							if ( in_array(
+								$callable,
+								array( 'WC_Payments::mode', 'WC_Payments_Account::get_connect_url', 'WC_Payments_Utils::supported_countries' ),
+								true
+							)
+							) {
+
+								return true;
+							}
+						}
+
+						// For other callables, delegate to PHP's native is_callable()
+						// so tests mirror runtime reality.
+						return \is_callable( $callable );
+					}
+
+					// No arguments provided.
+					return false;
+				},
+				'wcpay_get_container' => function () {
+					return $this->mock_woopayments_container;
+				},
+			),
+		);
+	}
+
+	/**
+	 * Get the list of supported countries for WooPayments.
+	 *
+	 * @return array Array of country codes and names.
+	 */
+	private function get_woopayments_supported_countries(): array {
+		// This is just a subset of countries that WooPayments supports.
+		// But it should cover our testing needs.
+		return array(
+			'us' => 'United States',
+			'gb' => 'United Kingdom',
+			'de' => 'Germany',
+		);
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
index 63c0309682..0c8d0d52e9 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
@@ -410,6 +410,190 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * Test getting payment gateway provider instance returns specific provider.
+	 */
+	public function test_get_payment_gateway_provider_instance_returns_specific_provider() {
+		// Arrange - woocommerce_payments is mapped to WooPayments provider.
+		$gateway_id = 'woocommerce_payments';
+
+		// Act.
+		$provider = $this->sut->get_payment_gateway_provider_instance( $gateway_id );
+
+		// Assert.
+		$this->assertInstanceOf(
+			PaymentsProviders\WooPayments::class,
+			$provider,
+			'Should return specific WooPayments provider instance'
+		);
+	}
+
+	/**
+	 * Test getting payment gateway provider instance returns specific provider with wildcard match.
+	 */
+	public function test_get_payment_gateway_provider_instance_returns_specific_provider_with_wildcard() {
+		// Arrange - stripe_* pattern matches stripe_ideal, and should return Stripe provider.
+		$gateway_id = 'stripe_ideal';
+
+		// Act.
+		$provider = $this->sut->get_payment_gateway_provider_instance( $gateway_id );
+
+		// Assert.
+		$this->assertInstanceOf(
+			PaymentsProviders\Stripe::class,
+			$provider,
+			'Should return Stripe provider for wildcard match'
+		);
+	}
+
+	/**
+	 * Test getting payment gateway provider instance returns generic provider when no mapping exists.
+	 */
+	public function test_get_payment_gateway_provider_instance_returns_generic_provider_when_no_mapping() {
+		// Arrange - Use a gateway ID that has no mapping.
+		$gateway_id = 'unknown_gateway_id';
+
+		// Act.
+		$provider = $this->sut->get_payment_gateway_provider_instance( $gateway_id );
+
+		// Assert - Verify it's the generic provider, not a specific subclass.
+		$this->assertSame(
+			PaymentGateway::class,
+			get_class( $provider ),
+			'Should return generic PaymentGateway instance when no mapping found'
+		);
+	}
+
+	/**
+	 * Test getting payment gateway provider instance returns cached instance.
+	 */
+	public function test_get_payment_gateway_provider_instance_returns_cached_instance() {
+		// Arrange.
+		$gateway_id = 'woocommerce_payments';
+
+		// Act - Get provider instance twice.
+		$provider1 = $this->sut->get_payment_gateway_provider_instance( $gateway_id );
+		$provider2 = $this->sut->get_payment_gateway_provider_instance( $gateway_id );
+
+		// Assert - Should return the same instance (cached).
+		$this->assertSame(
+			$provider1,
+			$provider2,
+			'Should return same cached instance for same gateway ID'
+		);
+	}
+
+	/**
+	 * Test getting payment gateway provider instance returns generic provider for invalid provider class.
+	 */
+	public function test_get_payment_gateway_provider_instance_returns_generic_provider_for_invalid_provider_class() {
+		// Arrange - Use reflection to inject an invalid provider class mapping.
+		$reflection = new \ReflectionClass( $this->sut );
+		$property   = $reflection->getProperty( 'payment_gateways_providers_class_map' );
+		$property->setAccessible( true );
+
+		// Get current map and add invalid mapping.
+		$current_map                    = $property->getValue( $this->sut );
+		$current_map['invalid_gateway'] = \stdClass::class; // stdClass does not extend PaymentGateway.
+		$property->setValue( $this->sut, $current_map );
+
+		// Expect the wc_doing_it_wrong notice.
+		$this->setExpectedIncorrectUsage( PaymentsProviders::class . '::get_payment_gateway_provider_instance' );
+
+		// Act.
+		$provider = $this->sut->get_payment_gateway_provider_instance( 'invalid_gateway' );
+
+		// Assert - Verify it's the generic provider, not a specific subclass.
+		$this->assertSame(
+			PaymentGateway::class,
+			get_class( $provider ),
+			'Should return generic PaymentGateway instance for invalid provider class'
+		);
+	}
+
+	/**
+	 * Test getting payment extension suggestion provider instance returns specific provider.
+	 */
+	public function test_get_payment_extension_suggestion_provider_instance_returns_specific_provider() {
+		// Arrange - woopayments PES ID is mapped to WooPayments provider.
+		$pes_id = ExtensionSuggestions::WOOPAYMENTS;
+
+		// Act.
+		$provider = $this->sut->get_payment_extension_suggestion_provider_instance( $pes_id );
+
+		// Assert.
+		$this->assertInstanceOf(
+			PaymentsProviders\WooPayments::class,
+			$provider,
+			'Should return specific WooPayments provider instance'
+		);
+	}
+
+	/**
+	 * Test getting payment extension suggestion provider instance returns generic provider when no mapping exists.
+	 */
+	public function test_get_payment_extension_suggestion_provider_instance_returns_generic_provider_when_no_mapping() {
+		// Arrange - Use a PES ID that has no mapping.
+		$pes_id = 'unknown_pes_id';
+
+		// Act.
+		$provider = $this->sut->get_payment_extension_suggestion_provider_instance( $pes_id );
+
+		// Assert - Verify it's the generic provider, not a specific subclass.
+		$this->assertSame(
+			PaymentGateway::class,
+			get_class( $provider ),
+			'Should return generic PaymentGateway instance when no mapping found'
+		);
+	}
+
+	/**
+	 * Test getting payment extension suggestion provider instance returns cached instance.
+	 */
+	public function test_get_payment_extension_suggestion_provider_instance_returns_cached_instance() {
+		// Arrange.
+		$pes_id = ExtensionSuggestions::WOOPAYMENTS;
+
+		// Act - Get provider instance twice.
+		$provider1 = $this->sut->get_payment_extension_suggestion_provider_instance( $pes_id );
+		$provider2 = $this->sut->get_payment_extension_suggestion_provider_instance( $pes_id );
+
+		// Assert - Should return the same instance (cached).
+		$this->assertSame(
+			$provider1,
+			$provider2,
+			'Should return same cached instance for same PES ID'
+		);
+	}
+
+	/**
+	 * Test getting payment extension suggestion provider instance returns generic provider for invalid provider class.
+	 */
+	public function test_get_payment_extension_suggestion_provider_instance_returns_generic_provider_for_invalid_provider_class() {
+		// Arrange - Use reflection to inject an invalid provider class mapping.
+		$reflection = new \ReflectionClass( $this->sut );
+		$property   = $reflection->getProperty( 'payment_extension_suggestions_providers_class_map' );
+		$property->setAccessible( true );
+
+		// Get current map and add invalid mapping.
+		$current_map                = $property->getValue( $this->sut );
+		$current_map['invalid_pes'] = \stdClass::class; // stdClass does not extend PaymentGateway.
+		$property->setValue( $this->sut, $current_map );
+
+		// Expect the wc_doing_it_wrong notice.
+		$this->setExpectedIncorrectUsage( PaymentsProviders::class . '::get_payment_extension_suggestion_provider_instance' );
+
+		// Act.
+		$provider = $this->sut->get_payment_extension_suggestion_provider_instance( 'invalid_pes' );
+
+		// Assert - Verify it's the generic provider, not a specific subclass.
+		$this->assertSame(
+			PaymentGateway::class,
+			get_class( $provider ),
+			'Should return generic PaymentGateway instance for invalid provider class'
+		);
+	}
+
 	/**
 	 * Test getting payment gateway base details.
 	 */