# Authors: The jax-sklearn developers
# SPDX-License-Identifier: BSD-3-Clause


import re
import warnings
from contextlib import suppress
from functools import partial
from inspect import isfunction

from xlearn import clone, config_context
from xlearn.calibration import CalibratedClassifierCV
from xlearn.cluster import (
    HDBSCAN,
    AffinityPropagation,
    AgglomerativeClustering,
    Birch,
    BisectingKMeans,
    FeatureAgglomeration,
    KMeans,
    MeanShift,
    MiniBatchKMeans,
    SpectralBiclustering,
    SpectralClustering,
    SpectralCoclustering,
)
from xlearn.compose import ColumnTransformer
from xlearn.covariance import GraphicalLasso, GraphicalLassoCV
from xlearn.cross_decomposition import CCA, PLSSVD, PLSCanonical, PLSRegression
from xlearn.decomposition import (
    NMF,
    PCA,
    DictionaryLearning,
    FactorAnalysis,
    FastICA,
    IncrementalPCA,
    KernelPCA,
    LatentDirichletAllocation,
    MiniBatchDictionaryLearning,
    MiniBatchNMF,
    MiniBatchSparsePCA,
    SparseCoder,
    SparsePCA,
    TruncatedSVD,
)
from xlearn.discriminant_analysis import LinearDiscriminantAnalysis
from xlearn.dummy import DummyClassifier
from xlearn.ensemble import (
    AdaBoostClassifier,
    AdaBoostRegressor,
    BaggingClassifier,
    BaggingRegressor,
    ExtraTreesClassifier,
    ExtraTreesRegressor,
    GradientBoostingClassifier,
    GradientBoostingRegressor,
    HistGradientBoostingClassifier,
    HistGradientBoostingRegressor,
    IsolationForest,
    RandomForestClassifier,
    RandomForestRegressor,
    RandomTreesEmbedding,
    StackingClassifier,
    StackingRegressor,
    VotingClassifier,
    VotingRegressor,
)
from xlearn.exceptions import SkipTestWarning
from xlearn.experimental import enable_halving_search_cv  # noqa: F401
from xlearn.feature_selection import (
    RFE,
    RFECV,
    SelectFdr,
    SelectFromModel,
    SelectKBest,
    SequentialFeatureSelector,
)
from xlearn.frozen import FrozenEstimator
from xlearn.kernel_approximation import (
    Nystroem,
    PolynomialCountSketch,
    RBFSampler,
    SkewedChi2Sampler,
)
from xlearn.linear_model import (
    ARDRegression,
    BayesianRidge,
    ElasticNet,
    ElasticNetCV,
    GammaRegressor,
    HuberRegressor,
    LarsCV,
    Lasso,
    LassoCV,
    LassoLars,
    LassoLarsCV,
    LassoLarsIC,
    LinearRegression,
    LogisticRegression,
    LogisticRegressionCV,
    MultiTaskElasticNet,
    MultiTaskElasticNetCV,
    MultiTaskLasso,
    MultiTaskLassoCV,
    OrthogonalMatchingPursuitCV,
    PassiveAggressiveClassifier,
    PassiveAggressiveRegressor,
    Perceptron,
    PoissonRegressor,
    QuantileRegressor,
    RANSACRegressor,
    Ridge,
    RidgeClassifier,
    SGDClassifier,
    SGDOneClassSVM,
    SGDRegressor,
    TheilSenRegressor,
    TweedieRegressor,
)
from xlearn.manifold import (
    MDS,
    TSNE,
    Isomap,
    LocallyLinearEmbedding,
    SpectralEmbedding,
)
from xlearn.mixture import BayesianGaussianMixture, GaussianMixture
from xlearn.model_selection import (
    FixedThresholdClassifier,
    GridSearchCV,
    HalvingGridSearchCV,
    HalvingRandomSearchCV,
    RandomizedSearchCV,
    TunedThresholdClassifierCV,
)
from xlearn.multiclass import (
    OneVsOneClassifier,
    OneVsRestClassifier,
    OutputCodeClassifier,
)
from xlearn.multioutput import (
    ClassifierChain,
    MultiOutputClassifier,
    MultiOutputRegressor,
    RegressorChain,
)
from xlearn.neighbors import (
    KernelDensity,
    KNeighborsClassifier,
    KNeighborsRegressor,
    KNeighborsTransformer,
    NeighborhoodComponentsAnalysis,
    RadiusNeighborsTransformer,
)
from xlearn.neural_network import BernoulliRBM, MLPClassifier, MLPRegressor
from xlearn.pipeline import FeatureUnion, Pipeline
from xlearn.preprocessing import (
    KBinsDiscretizer,
    OneHotEncoder,
    SplineTransformer,
    StandardScaler,
    TargetEncoder,
)
from xlearn.random_projection import (
    GaussianRandomProjection,
    SparseRandomProjection,
)
from xlearn.semi_supervised import (
    LabelPropagation,
    LabelSpreading,
    SelfTrainingClassifier,
)
from xlearn.svm import SVC, SVR, LinearSVC, LinearSVR, NuSVC, NuSVR, OneClassSVM
from xlearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from xlearn.utils import all_estimators
from xlearn.utils._tags import get_tags
from xlearn.utils._testing import SkipTest
from xlearn.utils.fixes import _IS_32BIT, parse_version, sp_base_version

CROSS_DECOMPOSITION = ["PLSCanonical", "PLSRegression", "CCA", "PLSSVD"]

# The following dictionary is to indicate constructor arguments suitable for the test
# suite, which uses very small datasets, and is intended to run rather quickly.
INIT_PARAMS = {
    AdaBoostClassifier: dict(n_estimators=5),
    AdaBoostRegressor: dict(n_estimators=5),
    AffinityPropagation: dict(max_iter=5),
    AgglomerativeClustering: dict(n_clusters=2),
    ARDRegression: dict(max_iter=5),
    BaggingClassifier: dict(n_estimators=5),
    BaggingRegressor: dict(n_estimators=5),
    BayesianGaussianMixture: dict(n_init=2, max_iter=5),
    BayesianRidge: dict(max_iter=5),
    BernoulliRBM: dict(n_iter=5, batch_size=10),
    Birch: dict(n_clusters=2),
    BisectingKMeans: dict(n_init=2, n_clusters=2, max_iter=5),
    CalibratedClassifierCV: dict(estimator=LogisticRegression(C=1), cv=3),
    CCA: dict(n_components=1, max_iter=5),
    ClassifierChain: dict(estimator=LogisticRegression(C=1), cv=3),
    ColumnTransformer: dict(transformers=[("trans1", StandardScaler(), [0, 1])]),
    DictionaryLearning: dict(max_iter=20, transform_algorithm="lasso_lars"),
    # the default strategy prior would output constant predictions and fail
    # for check_classifiers_predictions
    DummyClassifier: [dict(strategy="stratified"), dict(strategy="most_frequent")],
    ElasticNetCV: dict(max_iter=5, cv=3),
    ElasticNet: dict(max_iter=5),
    ExtraTreesClassifier: dict(n_estimators=5),
    ExtraTreesRegressor: dict(n_estimators=5),
    FactorAnalysis: dict(max_iter=5),
    FastICA: dict(max_iter=5),
    FeatureAgglomeration: dict(n_clusters=2),
    FeatureUnion: dict(transformer_list=[("trans1", StandardScaler())]),
    FixedThresholdClassifier: dict(estimator=LogisticRegression(C=1)),
    GammaRegressor: dict(max_iter=5),
    GaussianMixture: dict(n_init=2, max_iter=5),
    # Due to the jl lemma and often very few samples, the number
    # of components of the random matrix projection will be probably
    # greater than the number of features.
    # So we impose a smaller number (avoid "auto" mode)
    GaussianRandomProjection: dict(n_components=2),
    GradientBoostingClassifier: dict(n_estimators=5),
    GradientBoostingRegressor: dict(n_estimators=5),
    GraphicalLassoCV: dict(max_iter=5, cv=3),
    GraphicalLasso: dict(max_iter=5),
    GridSearchCV: [
        dict(
            cv=2,
            error_score="raise",
            estimator=Ridge(),
            param_grid={"alpha": [0.1, 1.0]},
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=LogisticRegression(),
            param_grid={"C": [0.1, 1.0]},
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(steps=[("pca", PCA()), ("ridge", Ridge())]),
            param_grid={"ridge__alpha": [0.1, 1.0]},
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(
                steps=[("pca", PCA()), ("logisticregression", LogisticRegression())]
            ),
            param_grid={"logisticregression__C": [0.1, 1.0]},
        ),
    ],
    HalvingGridSearchCV: [
        dict(
            cv=2,
            error_score="raise",
            estimator=Ridge(),
            min_resources="smallest",
            param_grid={"alpha": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=LogisticRegression(),
            min_resources="smallest",
            param_grid={"C": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(steps=[("pca", PCA()), ("ridge", Ridge())]),
            min_resources="smallest",
            param_grid={"ridge__alpha": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(
                steps=[("pca", PCA()), ("logisticregression", LogisticRegression())]
            ),
            min_resources="smallest",
            param_grid={"logisticregression__C": [0.1, 1.0]},
            random_state=0,
        ),
    ],
    HalvingRandomSearchCV: [
        dict(
            cv=2,
            error_score="raise",
            estimator=Ridge(),
            param_distributions={"alpha": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=LogisticRegression(),
            param_distributions={"C": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(steps=[("pca", PCA()), ("ridge", Ridge())]),
            param_distributions={"ridge__alpha": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(
                steps=[("pca", PCA()), ("logisticregression", LogisticRegression())]
            ),
            param_distributions={"logisticregression__C": [0.1, 1.0]},
            random_state=0,
        ),
    ],
    HDBSCAN: dict(min_samples=1),
    # The default min_samples_leaf (20) isn't appropriate for small
    # datasets (only very shallow trees are built) that the checks use.
    HistGradientBoostingClassifier: dict(max_iter=5, min_samples_leaf=5),
    HistGradientBoostingRegressor: dict(max_iter=5, min_samples_leaf=5),
    HuberRegressor: dict(max_iter=5),
    IncrementalPCA: dict(batch_size=10),
    IsolationForest: dict(n_estimators=5),
    KMeans: dict(n_init=2, n_clusters=2, max_iter=5),
    KNeighborsClassifier: [dict(n_neighbors=2), dict(metric="precomputed")],
    KNeighborsRegressor: [dict(n_neighbors=2), dict(metric="precomputed")],
    LabelPropagation: dict(max_iter=5),
    LabelSpreading: dict(max_iter=5),
    LarsCV: dict(max_iter=5, cv=3),
    LassoCV: dict(max_iter=5, cv=3),
    Lasso: dict(max_iter=5),
    LassoLarsCV: dict(max_iter=5, cv=3),
    LassoLars: dict(max_iter=5),
    # Noise variance estimation does not work when `n_samples < n_features`.
    # We need to provide the noise variance explicitly.
    LassoLarsIC: dict(max_iter=5, noise_variance=1.0),
    LatentDirichletAllocation: dict(max_iter=5, batch_size=10),
    LinearSVC: dict(max_iter=20),
    LinearSVR: dict(max_iter=20),
    LocallyLinearEmbedding: dict(max_iter=5),
    LogisticRegressionCV: dict(max_iter=5, cv=3),
    LogisticRegression: dict(max_iter=5),
    MDS: dict(n_init=2, max_iter=5),
    # In the case of check_fit2d_1sample, bandwidth is set to None and
    # is thus estimated. De facto it is 0.0 as a single sample is provided
    # and this makes the test fails. Hence we give it a placeholder value.
    MeanShift: dict(max_iter=5, bandwidth=1.0),
    MiniBatchDictionaryLearning: dict(batch_size=10, max_iter=5),
    MiniBatchKMeans: dict(n_init=2, n_clusters=2, max_iter=5, batch_size=10),
    MiniBatchNMF: dict(batch_size=10, max_iter=20, fresh_restarts=True),
    MiniBatchSparsePCA: dict(max_iter=5, batch_size=10),
    MLPClassifier: dict(max_iter=100),
    MLPRegressor: dict(max_iter=100),
    MultiOutputClassifier: dict(estimator=LogisticRegression(C=1)),
    MultiOutputRegressor: dict(estimator=Ridge()),
    MultiTaskElasticNetCV: dict(max_iter=5, cv=3),
    MultiTaskElasticNet: dict(max_iter=5),
    MultiTaskLassoCV: dict(max_iter=5, cv=3),
    MultiTaskLasso: dict(max_iter=5),
    NeighborhoodComponentsAnalysis: dict(max_iter=5),
    NMF: dict(max_iter=500),
    NuSVC: dict(max_iter=-1),
    NuSVR: dict(max_iter=-1),
    OneClassSVM: dict(max_iter=-1),
    OneHotEncoder: dict(handle_unknown="ignore"),
    OneVsOneClassifier: dict(estimator=LogisticRegression(C=1)),
    OneVsRestClassifier: dict(estimator=LogisticRegression(C=1)),
    OrthogonalMatchingPursuitCV: dict(cv=3),
    OutputCodeClassifier: dict(estimator=LogisticRegression(C=1)),
    PassiveAggressiveClassifier: dict(max_iter=5),
    PassiveAggressiveRegressor: dict(max_iter=5),
    Perceptron: dict(max_iter=5),
    Pipeline: [
        {"steps": [("scaler", StandardScaler()), ("final_estimator", Ridge())]},
        {
            "steps": [
                ("scaler", StandardScaler()),
                ("final_estimator", LogisticRegression()),
            ]
        },
    ],
    PLSCanonical: dict(n_components=1, max_iter=5),
    PLSRegression: dict(n_components=1, max_iter=5),
    PLSSVD: dict(n_components=1),
    PoissonRegressor: dict(max_iter=5),
    RandomForestClassifier: dict(n_estimators=5),
    RandomForestRegressor: dict(n_estimators=5),
    RandomizedSearchCV: [
        dict(
            cv=2,
            error_score="raise",
            estimator=Ridge(),
            param_distributions={"alpha": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=LogisticRegression(),
            param_distributions={"C": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(steps=[("pca", PCA()), ("ridge", Ridge())]),
            param_distributions={"ridge__alpha": [0.1, 1.0]},
            random_state=0,
        ),
        dict(
            cv=2,
            error_score="raise",
            estimator=Pipeline(
                steps=[("pca", PCA()), ("logisticregression", LogisticRegression())]
            ),
            param_distributions={"logisticregression__C": [0.1, 1.0]},
            random_state=0,
        ),
    ],
    RandomTreesEmbedding: dict(n_estimators=5),
    # `RANSACRegressor` will raise an error with any model other
    # than `LinearRegression` if we don't fix the `min_samples` parameter.
    # For common tests, we can enforce using `LinearRegression` that
    # is the default estimator in `RANSACRegressor` instead of `Ridge`.
    RANSACRegressor: dict(estimator=LinearRegression(), max_trials=10),
    RegressorChain: dict(estimator=Ridge(), cv=3),
    RFECV: dict(estimator=LogisticRegression(C=1), cv=3),
    RFE: dict(estimator=LogisticRegression(C=1)),
    # be tolerant of noisy datasets (not actually speed)
    SelectFdr: dict(alpha=0.5),
    # Increases coverage because SGDRegressor has partial_fit
    SelectFromModel: dict(estimator=SGDRegressor(random_state=0)),
    # SelectKBest has a default of k=10
    # which is more feature than we have in most case.
    SelectKBest: dict(k=1),
    SelfTrainingClassifier: dict(estimator=LogisticRegression(C=1), max_iter=5),
    SequentialFeatureSelector: dict(estimator=LogisticRegression(C=1), cv=3),
    SGDClassifier: dict(max_iter=5),
    SGDOneClassSVM: dict(max_iter=5),
    SGDRegressor: dict(max_iter=5),
    SparsePCA: dict(max_iter=5),
    # Due to the jl lemma and often very few samples, the number
    # of components of the random matrix projection will be probably
    # greater than the number of features.
    # So we impose a smaller number (avoid "auto" mode)
    SparseRandomProjection: dict(n_components=2),
    SpectralBiclustering: dict(n_init=2, n_best=1, n_clusters=2),
    SpectralClustering: dict(n_init=2, n_clusters=2),
    SpectralCoclustering: dict(n_init=2, n_clusters=2),
    # Default "auto" parameter can lead to different ordering of eigenvalues on
    # windows: #24105
    SpectralEmbedding: dict(eigen_tol=1e-05),
    StackingClassifier: dict(
        estimators=[
            ("est1", DecisionTreeClassifier(max_depth=3, random_state=0)),
            ("est2", DecisionTreeClassifier(max_depth=3, random_state=1)),
        ],
        cv=3,
    ),
    StackingRegressor: dict(
        estimators=[
            ("est1", DecisionTreeRegressor(max_depth=3, random_state=0)),
            ("est2", DecisionTreeRegressor(max_depth=3, random_state=1)),
        ],
        cv=3,
    ),
    SVC: [dict(max_iter=-1), dict(kernel="precomputed")],
    SVR: [dict(max_iter=-1), dict(kernel="precomputed")],
    TargetEncoder: dict(cv=3),
    TheilSenRegressor: dict(max_iter=5, max_subpopulation=100),
    # TruncatedSVD doesn't run with n_components = n_features
    TruncatedSVD: dict(n_iter=5, n_components=1),
    TSNE: dict(perplexity=2),
    TunedThresholdClassifierCV: dict(estimator=LogisticRegression(C=1), cv=3),
    TweedieRegressor: dict(max_iter=5),
    VotingClassifier: dict(
        estimators=[
            ("est1", DecisionTreeClassifier(max_depth=3, random_state=0)),
            ("est2", DecisionTreeClassifier(max_depth=3, random_state=1)),
        ]
    ),
    VotingRegressor: dict(
        estimators=[
            ("est1", DecisionTreeRegressor(max_depth=3, random_state=0)),
            ("est2", DecisionTreeRegressor(max_depth=3, random_state=1)),
        ]
    ),
}

# This dictionary stores parameters for specific checks. It also enables running the
# same check with multiple instances of the same estimator with different parameters.
# The special key "*" allows to apply the parameters to all checks.
# TODO(devtools): allow third-party developers to pass test specific params to checks
PER_ESTIMATOR_CHECK_PARAMS: dict = {
    # TODO(devtools): check that function names here exist in checks for the estimator
    AgglomerativeClustering: {"check_dict_unchanged": dict(n_clusters=1)},
    BayesianGaussianMixture: {"check_dict_unchanged": dict(max_iter=5, n_init=2)},
    BernoulliRBM: {"check_dict_unchanged": dict(n_components=1, n_iter=5)},
    Birch: {"check_dict_unchanged": dict(n_clusters=1)},
    BisectingKMeans: {"check_dict_unchanged": dict(max_iter=5, n_clusters=1, n_init=2)},
    CCA: {"check_dict_unchanged": dict(max_iter=5, n_components=1)},
    DecisionTreeRegressor: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(criterion="squared_error"),
            dict(criterion="absolute_error"),
            dict(criterion="friedman_mse"),
            dict(criterion="poisson"),
        ],
        "check_sample_weight_equivalence_on_sparse_data": [
            dict(criterion="squared_error"),
            dict(criterion="absolute_error"),
            dict(criterion="friedman_mse"),
            dict(criterion="poisson"),
        ],
    },
    DecisionTreeClassifier: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(criterion="gini"),
            dict(criterion="log_loss"),
            dict(criterion="entropy"),
        ],
        "check_sample_weight_equivalence_on_sparse_data": [
            dict(criterion="gini"),
            dict(criterion="log_loss"),
            dict(criterion="entropy"),
        ],
    },
    DictionaryLearning: {
        "check_dict_unchanged": dict(
            max_iter=20, n_components=1, transform_algorithm="lasso_lars"
        )
    },
    FactorAnalysis: {"check_dict_unchanged": dict(max_iter=5, n_components=1)},
    FastICA: {"check_dict_unchanged": dict(max_iter=5, n_components=1)},
    FeatureAgglomeration: {"check_dict_unchanged": dict(n_clusters=1)},
    FeatureUnion: {
        "check_estimator_sparse_tag": [
            dict(transformer_list=[("trans1", StandardScaler())]),
            dict(
                transformer_list=[
                    ("trans1", StandardScaler(with_mean=False)),
                    ("trans2", "drop"),
                    ("trans3", "passthrough"),
                ]
            ),
        ]
    },
    GammaRegressor: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="newton-cholesky"),
            dict(solver="lbfgs"),
        ],
    },
    GaussianMixture: {"check_dict_unchanged": dict(max_iter=5, n_init=2)},
    GaussianRandomProjection: {"check_dict_unchanged": dict(n_components=1)},
    IncrementalPCA: {"check_dict_unchanged": dict(batch_size=10, n_components=1)},
    Isomap: {"check_dict_unchanged": dict(n_components=1)},
    KMeans: {"check_dict_unchanged": dict(max_iter=5, n_clusters=1, n_init=2)},
    # TODO(1.9) simplify when averaged_inverted_cdf is the default
    KBinsDiscretizer: {
        "check_sample_weight_equivalence_on_dense_data": [
            # Using subsample != None leads to a stochastic fit that is not
            # handled by the check_sample_weight_equivalence_on_dense_data test.
            dict(strategy="quantile", subsample=None, quantile_method="inverted_cdf"),
            dict(
                strategy="quantile",
                subsample=None,
                quantile_method="averaged_inverted_cdf",
            ),
            dict(strategy="uniform", subsample=None),
            # The "kmeans" strategy leads to a stochastic fit that is not
            # handled by the check_sample_weight_equivalence test.
        ],
        "check_sample_weights_list": dict(
            strategy="quantile", quantile_method="averaged_inverted_cdf"
        ),
        "check_sample_weights_pandas_series": dict(
            strategy="quantile", quantile_method="averaged_inverted_cdf"
        ),
        "check_sample_weights_shape": dict(
            strategy="quantile", quantile_method="averaged_inverted_cdf"
        ),
        "check_sample_weights_not_an_array": dict(
            strategy="quantile", quantile_method="averaged_inverted_cdf"
        ),
        "check_sample_weights_not_overwritten": dict(
            strategy="quantile", quantile_method="averaged_inverted_cdf"
        ),
    },
    KernelPCA: {"check_dict_unchanged": dict(n_components=1)},
    LassoLars: {"check_non_transformer_estimators_n_iter": dict(alpha=0.0)},
    LatentDirichletAllocation: {
        "check_dict_unchanged": dict(batch_size=10, max_iter=5, n_components=1)
    },
    LinearDiscriminantAnalysis: {"check_dict_unchanged": dict(n_components=1)},
    LinearSVC: {
        "check_sample_weight_equivalence": [
            # TODO: dual=True is a stochastic solver: we cannot rely on
            # check_sample_weight_equivalence to check the correct handling of
            # sample_weight and we would need a statistical test instead, see
            # meta-issue #162298.
            # dict(max_iter=20, dual=True, tol=1e-12),
            dict(dual=False, tol=1e-12),
            dict(dual=False, tol=1e-12, class_weight="balanced"),
        ]
    },
    LinearRegression: {
        "check_estimator_sparse_tag": [dict(positive=False), dict(positive=True)],
        "check_sample_weight_equivalence_on_dense_data": [
            dict(positive=False),
            dict(positive=True),
        ],
        "check_sample_weight_equivalence_on_sparse_data": [dict(tol=1e-12)],
    },
    LocallyLinearEmbedding: {"check_dict_unchanged": dict(max_iter=5, n_components=1)},
    LogisticRegression: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="lbfgs"),
            dict(solver="liblinear"),
            dict(solver="newton-cg"),
            dict(solver="newton-cholesky"),
            dict(solver="newton-cholesky", class_weight="balanced"),
        ]
    },
    LogisticRegressionCV: {
        "check_sample_weight_equivalence": [
            dict(solver="lbfgs"),
            dict(solver="newton-cholesky"),
            dict(solver="newton-cholesky", class_weight="balanced"),
        ],
        "check_sample_weight_equivalence_on_sparse_data": [
            dict(solver="liblinear"),
        ],
    },
    MDS: {"check_dict_unchanged": dict(max_iter=5, n_components=1, n_init=2)},
    MLPClassifier: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="lbfgs"),
        ]
    },
    MLPRegressor: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="sgd", tol=1e-2, random_state=42),
        ]
    },
    MiniBatchDictionaryLearning: {
        "check_dict_unchanged": dict(batch_size=10, max_iter=5, n_components=1)
    },
    MiniBatchKMeans: {
        "check_dict_unchanged": dict(batch_size=10, max_iter=5, n_clusters=1, n_init=2)
    },
    MiniBatchNMF: {
        "check_dict_unchanged": dict(
            batch_size=10, fresh_restarts=True, max_iter=20, n_components=1
        )
    },
    MiniBatchSparsePCA: {
        "check_dict_unchanged": dict(batch_size=10, max_iter=5, n_components=1)
    },
    NMF: {"check_dict_unchanged": dict(max_iter=500, n_components=1)},
    NeighborhoodComponentsAnalysis: {
        "check_dict_unchanged": dict(max_iter=5, n_components=1)
    },
    Nystroem: {"check_dict_unchanged": dict(n_components=1)},
    PCA: {"check_dict_unchanged": dict(n_components=1)},
    PLSCanonical: {"check_dict_unchanged": dict(max_iter=5, n_components=1)},
    PLSRegression: {"check_dict_unchanged": dict(max_iter=5, n_components=1)},
    PLSSVD: {"check_dict_unchanged": dict(n_components=1)},
    PoissonRegressor: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="newton-cholesky"),
            dict(solver="lbfgs"),
        ],
    },
    PolynomialCountSketch: {"check_dict_unchanged": dict(n_components=1)},
    QuantileRegressor: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(quantile=0.5),
            dict(quantile=0.75),
            dict(solver="highs-ds"),
            dict(solver="highs-ipm"),
        ],
    },
    RBFSampler: {"check_dict_unchanged": dict(n_components=1)},
    Ridge: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="svd"),
            dict(solver="cholesky"),
            dict(solver="sparse_cg"),
            dict(solver="lsqr"),
            dict(solver="lbfgs", positive=True),
        ],
        "check_sample_weight_equivalence_on_sparse_data": [
            dict(solver="sparse_cg"),
            dict(solver="lsqr"),
        ],
    },
    RidgeClassifier: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="svd"),
            dict(solver="cholesky"),
            dict(solver="sparse_cg"),
            dict(solver="lsqr"),
        ],
        "check_sample_weight_equivalence_on_sparse_data": [
            dict(solver="sparse_cg"),
            dict(solver="lsqr"),
        ],
    },
    SkewedChi2Sampler: {"check_dict_unchanged": dict(n_components=1)},
    SparsePCA: {"check_dict_unchanged": dict(max_iter=5, n_components=1)},
    SparseRandomProjection: {"check_dict_unchanged": dict(n_components=1)},
    SpectralBiclustering: {
        "check_dict_unchanged": dict(n_best=1, n_clusters=1, n_components=1, n_init=2)
    },
    SpectralClustering: {
        "check_dict_unchanged": dict(n_clusters=1, n_components=1, n_init=2)
    },
    SpectralCoclustering: {"check_dict_unchanged": dict(n_clusters=1, n_init=2)},
    SpectralEmbedding: {"check_dict_unchanged": dict(eigen_tol=1e-05, n_components=1)},
    StandardScaler: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(with_mean=True),
            dict(with_mean=False),
        ],
        "check_sample_weight_equivalence_on_sparse_data": [
            dict(with_mean=False),
        ],
    },
    TSNE: {"check_dict_unchanged": dict(n_components=1, perplexity=2)},
    TruncatedSVD: {"check_dict_unchanged": dict(n_components=1)},
    TweedieRegressor: {
        "check_sample_weight_equivalence_on_dense_data": [
            dict(solver="newton-cholesky"),
            dict(solver="lbfgs"),
        ],
    },
}


def _tested_estimators(type_filter=None):
    for _, Estimator in all_estimators(type_filter=type_filter):
        with suppress(SkipTest):
            for estimator in _construct_instances(Estimator):
                yield estimator


SKIPPED_ESTIMATORS = [SparseCoder, FrozenEstimator]


def _construct_instances(Estimator):
    """Construct Estimator instances if possible.

    If parameter sets in INIT_PARAMS are provided, use them. If there are a list
    of parameter sets, return one instance for each set.
    """
    if Estimator in SKIPPED_ESTIMATORS:
        msg = f"Can't instantiate estimator {Estimator.__name__}"
        # raise additional warning to be shown by pytest
        warnings.warn(msg, SkipTestWarning)
        raise SkipTest(msg)

    if Estimator in INIT_PARAMS:
        param_sets = INIT_PARAMS[Estimator]
        if not isinstance(param_sets, list):
            param_sets = [param_sets]
        for params in param_sets:
            est = Estimator(**params)
            yield est
    else:
        yield Estimator()


def _get_check_estimator_ids(obj):
    """Create pytest ids for checks.

    When `obj` is an estimator, this returns the pprint version of the
    estimator (with `print_changed_only=True`). When `obj` is a function, the
    name of the function is returned with its keyword arguments.

    `_get_check_estimator_ids` is designed to be used as the `id` in
    `pytest.mark.parametrize` where `check_estimator(..., generate_only=True)`
    is yielding estimators and checks.

    Parameters
    ----------
    obj : estimator or function
        Items generated by `check_estimator`.

    Returns
    -------
    id : str or None

    See Also
    --------
    check_estimator
    """
    if isfunction(obj):
        return obj.__name__
    if isinstance(obj, partial):
        if not obj.keywords:
            return obj.func.__name__
        kwstring = ",".join(["{}={}".format(k, v) for k, v in obj.keywords.items()])
        return "{}({})".format(obj.func.__name__, kwstring)
    if hasattr(obj, "get_params"):
        with config_context(print_changed_only=True):
            return re.sub(r"\s", "", str(obj))


def _yield_instances_for_check(check, estimator_orig):
    """Yield instances for a check.

    For most estimators, this is a no-op.

    For estimators which have an entry in PER_ESTIMATOR_CHECK_PARAMS, this will yield
    an estimator for each parameter set in PER_ESTIMATOR_CHECK_PARAMS[estimator].
    """
    # TODO(devtools): enable this behavior for third party estimators as well
    if type(estimator_orig) not in PER_ESTIMATOR_CHECK_PARAMS:
        yield estimator_orig
        return

    check_params = PER_ESTIMATOR_CHECK_PARAMS[type(estimator_orig)]

    try:
        check_name = check.__name__
    except AttributeError:
        # partial tests
        check_name = check.func.__name__

    if check_name not in check_params:
        yield estimator_orig
        return

    param_set = check_params[check_name]
    if isinstance(param_set, dict):
        param_set = [param_set]

    for params in param_set:
        estimator = clone(estimator_orig)
        estimator.set_params(**params)
        yield estimator


PER_ESTIMATOR_XFAIL_CHECKS = {
    AdaBoostClassifier: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    AdaBoostRegressor: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    BaggingClassifier: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    BaggingRegressor: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    BernoulliRBM: {
        "check_methods_subset_invariance": ("fails for the decision_function method"),
        "check_methods_sample_order_invariance": ("fails for the score_samples method"),
    },
    BisectingKMeans: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    ColumnTransformer: {
        "check_estimators_empty_data_messages": "FIXME",
        "check_estimators_nan_inf": "FIXME",
        "check_estimator_sparse_array": "FIXME",
        "check_estimator_sparse_matrix": "FIXME",
        "check_fit1d": "FIXME",
        "check_fit2d_predict1d": "FIXME",
        "check_complex_data": "FIXME",
        "check_fit2d_1feature": "FIXME",
    },
    DummyClassifier: {
        "check_methods_subset_invariance": "fails for the predict method",
        "check_methods_sample_order_invariance": "fails for the predict method",
    },
    FeatureUnion: {
        "check_estimators_overwrite_params": "FIXME",
        "check_estimators_nan_inf": "FIXME",
        "check_dont_overwrite_parameters": "FIXME",
    },
    FixedThresholdClassifier: {
        "check_classifiers_train": "Threshold at probability 0.5 does not hold",
        "check_sample_weight_equivalence_on_dense_data": (
            "Due to the cross-validation and sample ordering, removing a sample"
            " is not strictly equal to putting is weight to zero. Specific unit"
            " tests are added for TunedThresholdClassifierCV specifically."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    GradientBoostingClassifier: {
        # TODO: investigate failure see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    GradientBoostingRegressor: {
        # TODO: investigate failure see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    GridSearchCV: {
        "check_supervised_y_2d": "DataConversionWarning not caught",
        "check_requires_y_none": "Doesn't fail gracefully",
    },
    HalvingGridSearchCV: {
        "check_fit2d_1sample": (
            "Fail during parameter check since min/max resources requires more samples"
        ),
        "check_estimators_nan_inf": "FIXME",
        "check_classifiers_one_label_sample_weights": "FIXME",
        "check_fit2d_1feature": "FIXME",
        "check_supervised_y_2d": "DataConversionWarning not caught",
        "check_requires_y_none": "Doesn't fail gracefully",
    },
    HalvingRandomSearchCV: {
        "check_fit2d_1sample": (
            "Fail during parameter check since min/max resources requires more samples"
        ),
        "check_estimators_nan_inf": "FIXME",
        "check_classifiers_one_label_sample_weights": "FIXME",
        "check_fit2d_1feature": "FIXME",
        "check_supervised_y_2d": "DataConversionWarning not caught",
        "check_requires_y_none": "Doesn't fail gracefully",
    },
    HistGradientBoostingClassifier: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    HistGradientBoostingRegressor: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    IsolationForest: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    KernelDensity: {
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight must have positive values"
        ),
    },
    KMeans: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    KNeighborsTransformer: {
        "check_methods_sample_order_invariance": "check is not applicable."
    },
    LinearSVC: {
        # TODO: replace by a statistical test when _dual=True, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_non_transformer_estimators_n_iter": (
            "n_iter_ cannot be easily accessed."
        ),
    },
    LinearSVR: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    LogisticRegression: {
        # TODO: fix sample_weight handling of this estimator, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    MiniBatchKMeans: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    NuSVC: {
        "check_class_weight_classifiers": "class_weight is ignored.",
        # TODO: fix sample_weight handling of this estimator when probability=False
        # TODO: replace by a statistical test when probability=True
        # see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_classifiers_one_label_sample_weights": (
            "specified nu is infeasible for the fit."
        ),
    },
    NuSVR: {
        # TODO: fix sample_weight handling of this estimator, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    Nystroem: {
        "check_transformer_preserves_dtypes": (
            "dtypes are preserved but not at a close enough precision"
        )
    },
    OneClassSVM: {
        # TODO: fix sample_weight handling of this estimator, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    Perceptron: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    Pipeline: {
        "check_dont_overwrite_parameters": (
            "Pipeline changes the `steps` parameter, which it shouldn't."
            "Therefore this test is x-fail until we fix this."
        ),
        "check_estimators_overwrite_params": (
            "Pipeline changes the `steps` parameter, which it shouldn't."
            "Therefore this test is x-fail until we fix this."
        ),
    },
    RadiusNeighborsTransformer: {
        "check_methods_sample_order_invariance": "check is not applicable."
    },
    RandomForestClassifier: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    RandomForestRegressor: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    RandomizedSearchCV: {
        "check_supervised_y_2d": "DataConversionWarning not caught",
        "check_requires_y_none": "Doesn't fail gracefully",
    },
    RandomTreesEmbedding: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    RANSACRegressor: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    Ridge: {
        "check_non_transformer_estimators_n_iter": (
            "n_iter_ cannot be easily accessed."
        )
    },
    RidgeClassifier: {
        "check_non_transformer_estimators_n_iter": (
            "n_iter_ cannot be easily accessed."
        )
    },
    SelfTrainingClassifier: {
        "check_non_transformer_estimators_n_iter": "n_iter_ can be 0."
    },
    SGDClassifier: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    SGDOneClassSVM: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    SGDRegressor: {
        # TODO: replace by a statistical test, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    SpectralCoclustering: {
        "check_estimators_dtypes": "raises nan error",
        "check_fit2d_1sample": "_scale_normalize fails",
        "check_fit2d_1feature": "raises apply_along_axis error",
        "check_estimator_sparse_matrix": "does not fail gracefully",
        "check_estimator_sparse_array": "does not fail gracefully",
        "check_methods_subset_invariance": "empty array passed inside",
        "check_dont_overwrite_parameters": "empty array passed inside",
        "check_fit2d_predict1d": "empty array passed inside",
        # ValueError: Found array with 0 feature(s) (shape=(23, 0))
        # while a minimum of 1 is required.
        "check_dict_unchanged": "FIXME",
    },
    SpectralBiclustering: {
        "check_estimators_dtypes": "raises nan error",
        "check_fit2d_1sample": "_scale_normalize fails",
        "check_fit2d_1feature": "raises apply_along_axis error",
        "check_estimator_sparse_matrix": "does not fail gracefully",
        "check_estimator_sparse_array": "does not fail gracefully",
        "check_methods_subset_invariance": "empty array passed inside",
        "check_dont_overwrite_parameters": "empty array passed inside",
        "check_fit2d_predict1d": "empty array passed inside",
    },
    SVC: {
        # TODO: fix sample_weight handling of this estimator when probability=False
        # TODO: replace by a statistical test when probability=True
        # see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    SVR: {
        # TODO: fix sample_weight handling of this estimator, see meta-issue #16298
        "check_sample_weight_equivalence_on_dense_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
        "check_sample_weight_equivalence_on_sparse_data": (
            "sample_weight is not equivalent to removing/repeating samples."
        ),
    },
    TunedThresholdClassifierCV: {
        "check_classifiers_train": "Threshold at probability 0.5 does not hold",
        "check_sample_weight_equivalence_on_dense_data": (
            "Due to the cross-validation and sample ordering, removing a sample"
            " is not strictly equal to putting is weight to zero. Specific unit"
            " tests are added for TunedThresholdClassifierCV specifically."
        ),
    },
}

# TODO: remove when scipy min version >= 1.11
if sp_base_version < parse_version("1.11"):
    PER_ESTIMATOR_XFAIL_CHECKS[SplineTransformer] = {
        "check_estimators_pickle": (
            "scipy < 1.11 implementation of _bsplines does not"
            "support const memory views."
        ),
    }


def _get_expected_failed_checks(estimator):
    """Get the expected failed checks for all estimators in jax-sklearn."""
    failed_checks = PER_ESTIMATOR_XFAIL_CHECKS.get(type(estimator), {})

    tags = get_tags(estimator)

    # all xfail marks that depend on the instance, come here. As of now, we have only
    # these two cases.
    if type(estimator) in [KNeighborsClassifier, KNeighborsRegressor]:
        if tags.input_tags.pairwise:
            failed_checks.update(
                {
                    "check_n_features_in_after_fitting": "FIXME",
                    "check_dataframe_column_names_consistency": "FIXME",
                }
            )
    if type(estimator) == LinearRegression:
        # TODO: remove when scipy min version >= 1.16
        # Regression introduced in scipy 1.15 and fixed in 1.16, see
        # https://github.com/scipy/scipy/issues/22791
        if (
            parse_version("1.15.0") <= sp_base_version < parse_version("1.16")
            and _IS_32BIT
        ):
            failed_checks.update(
                {
                    "check_sample_weight_equivalence_on_dense_data": (
                        "Issue #31098. Fails on 32-bit platforms with recent scipy."
                    ),
                    "check_sample_weight_equivalence_on_sparse_data": (
                        "Issue #31098. Fails on 32-bit platforms with recent scipy."
                    ),
                }
            )

    return failed_checks
