"""Test image pre-processing functions."""

import platform
import re
import warnings
from collections.abc import Iterable
from pathlib import Path

import joblib
import numpy as np
import pandas as pd
import pytest
from nibabel import AnalyzeImage, Nifti1Image, Nifti2Image, load, spatialimages
from nibabel.freesurfer import MGHImage
from numpy.testing import (
    assert_allclose,
    assert_almost_equal,
    assert_array_almost_equal,
    assert_array_equal,
    assert_equal,
)

import nilearn as ni
from nilearn import signal
from nilearn._utils import testing
from nilearn._utils.data_gen import (
    _basic_confounds,
    generate_fake_fmri,
    generate_labeled_regions,
    generate_maps,
)
from nilearn._utils.niimg import _get_data
from nilearn._utils.testing import (
    assert_memory_less_than,
    with_memory_profiler,
    write_imgs_to_path,
)
from nilearn.conftest import (
    _affine_eye,
    _img_3d_rand,
    _rng,
    _shape_4d_default,
)
from nilearn.exceptions import DimensionError
from nilearn.image.image import (
    _crop_img_to,
    _fast_smooth_array,
    binarize_img,
    check_niimg,
    check_niimg_3d,
    check_niimg_4d,
    check_same_fov,
    clean_img,
    concat_imgs,
    copy_img,
    crop_img,
    get_data,
    high_variance_confounds,
    index_img,
    iter_check_niimg,
    iter_img,
    largest_connected_component_img,
    math_img,
    mean_img,
    new_img_like,
    smooth_array,
    smooth_img,
    swap_img_hemispheres,
    threshold_img,
)
from nilearn.image.resampling import resample_img
from nilearn.image.tests._testing import match_headers_keys
from nilearn.surface.surface import SurfaceImage, extract_data
from nilearn.surface.surface import get_data as get_surface_data
from nilearn.surface.utils import (
    assert_polymesh_equal,
    assert_surface_image_equal,
)

X64 = platform.architecture()[0] == "64bit"


AFFINE_TO_TEST = [
    _affine_eye(),
    np.diag((1, 1, -1, 1)),
    np.diag((0.6, 1, 0.6, 1)),
]

NON_EYE_AFFINE = np.array(
    [
        [1.0, 2.0, 3.0, 4.0],
        [5.0, 6.0, 7.0, 8.0],
        [9.0, 10.0, 11.0, 12.0],
        [0.0, 0.0, 0.0, 1.0],
    ]
)


def _new_data_for_smooth_array():
    # Impulse in 3D
    data = np.zeros((40, 41, 42))
    data[20, 20, 20] = 1
    return data


def _make_largest_cc_img_test_data():
    shapes = ((10, 11, 12), (13, 14, 15))
    regions = [1, 3]

    img1 = generate_labeled_regions(shape=shapes[0], n_regions=regions[0])
    img2 = generate_labeled_regions(shape=shapes[1], n_regions=regions[1])
    return img1, img2, shapes


def _images_to_mean():
    """Return a mixture of 4D and 3D images."""
    rng = _rng()

    data1 = np.zeros((5, 6, 7))
    data2 = rng.uniform(size=(5, 6, 7))
    data3 = rng.uniform(size=(5, 6, 7, 3))

    affine = np.diag((4, 3, 2, 1))

    img1 = Nifti1Image(data1, affine=affine)
    img2 = Nifti1Image(data2, affine=affine)
    img3 = Nifti1Image(data3, affine=affine)

    imgs = (
        [img1],
        [img1, img2],
        [img2, img1, img2],
        [img3, img1, img2],
    )
    return imgs


def _check_fwhm(data, affine, fwhm):
    """Expect a full-width at half maximum of fwhm / voxel_size."""
    vmax = data.max()
    above_half_max = data > 0.5 * vmax
    for axis in [0, 1, 2]:
        proj = np.any(
            np.any(np.rollaxis(above_half_max, axis=axis), axis=-1),
            axis=-1,
        )
        assert_equal(proj.sum(), fwhm / np.abs(affine[axis, axis]))


def _mean_ground_truth(imgs):
    arrays = []
    for img in imgs:
        img = get_data(img)
        if img.ndim == 4:
            img = np.mean(img, axis=-1)
        arrays.append(img)
    return np.mean(arrays, axis=0)


@pytest.fixture(scope="session")
def smooth_array_data():
    return _new_data_for_smooth_array()


@pytest.fixture(scope="session")
def stat_img_test_data():
    shape = (20, 20, 30)
    affine = _affine_eye()
    data = np.zeros(shape, dtype="int32")
    data[:2, :2, :2] = 4  # 8-voxel positive cluster
    data[4:6, :2, :2] = -4  # 8-voxel negative cluster
    data[8:11, 0, 0] = 5  # 3-voxel positive cluster
    data[13:16, 0, 0] = -5  # 3-voxel positive cluster
    data[:6, 4:10, :6] = 1  # 216-voxel positive cluster with low value
    data[13:19, 4:10, :6] = -1  # 216-voxel negative cluster with low value

    stat_img = Nifti1Image(data, affine)

    return stat_img


def test_get_data(tmp_path, shape_3d_default):
    img, *_ = generate_fake_fmri(shape=shape_3d_default)

    data = get_data(img)

    assert data.shape == img.shape
    assert data is img._data_cache

    mask_img = new_img_like(img, data > 0)
    data = get_data(mask_img)

    assert data.dtype == np.dtype("uint8")

    img_3d = index_img(img, 0)

    filename = str(tmp_path / "img_{}.nii.gz")
    img_3d.to_filename(filename.format("a"))
    img_3d.to_filename(filename.format("b"))

    data = get_data(filename.format("a"))

    assert len(data.shape) == 3

    data = get_data(filename.format("*"))

    assert len(data.shape) == 4


def test_high_variance_confounds(shape_3d_default):
    """Check high_variance_confounds returns proper shape.

    See also test_signals.test_high_variance_confounds()
    There is only tests on what is added by high_variance_confounds()
    compared to signal.high_variance_confounds()
    """
    length = 17
    n_confounds = 10

    img, mask_img = generate_fake_fmri(shape=shape_3d_default, length=length)

    confounds1 = high_variance_confounds(
        img, mask_img=mask_img, percentile=10.0, n_confounds=n_confounds
    )

    assert confounds1.shape == (length, n_confounds)

    # No mask.
    confounds2 = high_variance_confounds(
        img, percentile=10.0, n_confounds=n_confounds
    )

    assert confounds2.shape == (length, n_confounds)


def test_high_variance_confounds_surface(surf_mask_1d, surface_glm_data):
    """Check high_variance_confounds returns proper shape from surface."""
    length = 17
    n_confounds = 10

    img, _ = surface_glm_data(length)

    confounds1 = high_variance_confounds(
        img, mask_img=surf_mask_1d, percentile=10.0, n_confounds=n_confounds
    )

    assert confounds1.shape == (length, n_confounds)

    # No mask.
    confounds2 = high_variance_confounds(
        img, percentile=10.0, n_confounds=n_confounds
    )

    assert confounds2.shape == (length, n_confounds)


def test_fast_smooth_array():
    N = 4
    shape = (N, N, N)
    # hardcoded in _fast_smooth_array
    neighbor_weight = 0.2
    # 6 neighbors in 3D if you are not on an edge
    n_neighbors_max = 6

    data = np.ones(shape)
    smooth_data = _fast_smooth_array(data)

    # this contains the number of neighbors for each cell in the array
    n_neighbors_arr = np.empty(shape)
    for (i, j, k), __ in np.ndenumerate(n_neighbors_arr):
        n_neighbors_arr[i, j, k] = (
            3 + (0 < i < N - 1) + (0 < j < N - 1) + (0 < k < N - 1)
        )

    expected = (1 + neighbor_weight * n_neighbors_arr) / (
        1 + neighbor_weight * n_neighbors_max
    )
    assert_allclose(smooth_data, expected)


@pytest.mark.parametrize("affine", AFFINE_TO_TEST)
def test_smooth_array_fwhm_is_odd_with_copy(smooth_array_data, affine):
    """Test that fwhm divided by any affine is odd.

    Otherwise assertion below will fail.
    ( 9 / 0.6 = 15 is fine)
    """
    data = smooth_array_data
    fwhm = 9

    filtered = smooth_array(data, affine, fwhm=fwhm, copy=True)

    assert not np.may_share_memory(filtered, data)

    _check_fwhm(filtered, affine, fwhm)


@pytest.mark.parametrize("affine", AFFINE_TO_TEST)
def test_smooth_array_fwhm_is_odd_no_copy(affine):
    """Test that fwhm divided by any affine is odd.

    Otherwise assertion below will fail.
    ( 9 / 0.6 = 15 is fine)
    """
    data = _new_data_for_smooth_array()
    fwhm = 9

    smooth_array(data, affine, fwhm=fwhm, copy=False)

    _check_fwhm(data, affine, fwhm)


def test_smooth_array_nan_do_not_propagate():
    data = _new_data_for_smooth_array()
    data[10, 10, 10] = np.nan
    fwhm = 9
    affine = AFFINE_TO_TEST[2]

    filtered = smooth_array(
        data, affine, fwhm=fwhm, ensure_finite=True, copy=True
    )

    assert np.all(np.isfinite(filtered))


def test_smooth_array_same_result_with_fwhm_none_or_zero(
    smooth_array_data,
):
    affine = AFFINE_TO_TEST[2]

    out_fwhm_none = smooth_array(smooth_array_data, affine, fwhm=None)
    out_fwhm_zero = smooth_array(smooth_array_data, affine, fwhm=0.0)

    assert_array_equal(out_fwhm_none, out_fwhm_zero)


@pytest.mark.parametrize("affine", AFFINE_TO_TEST)
def test_fast_smooth_array_give_same_result_as_smooth_array(
    smooth_array_data, affine
):
    assert_equal(
        smooth_array(smooth_array_data, affine, fwhm="fast"),
        _fast_smooth_array(smooth_array_data),
    )


def test_smooth_array_raise_warning_if_fwhm_is_zero(smooth_array_data):
    """See https://github.com/nilearn/nilearn/issues/1537."""
    affine = AFFINE_TO_TEST[2]
    with pytest.warns(UserWarning):
        smooth_array(smooth_array_data, affine, fwhm=0.0)


def test_smooth_img(affine_eye, tmp_path):
    """Checks added functionalities compared to image._smooth_array()."""
    shapes = ((10, 11, 12), (13, 14, 15))
    lengths = (17, 18)
    fwhm = (1.0, 2.0, 3.0)

    img1, _ = generate_fake_fmri(shape=shapes[0], length=lengths[0])
    img2, _ = generate_fake_fmri(shape=shapes[1], length=lengths[1])

    for create_files in (False, True):
        imgs = testing.write_imgs_to_path(
            img1, img2, file_path=tmp_path, create_files=create_files
        )
        # List of images as input
        out = smooth_img(imgs, fwhm)

        assert isinstance(out, list)
        assert len(out) == 2
        for o, s, l in zip(out, shapes, lengths, strict=False):
            assert o.shape == (*s, l)

        # Single image as input
        out = smooth_img(imgs[0], fwhm)

        assert isinstance(out, Nifti1Image)
        assert out.shape == (shapes[0] + (lengths[0],))

    # Check corner case situations when fwhm=0, See issue #1537
    # Test whether function smooth_img raises a warning when fwhm=0.
    with pytest.warns(UserWarning):
        smooth_img(img1, fwhm=0.0)

    # Test output equal when fwhm=None and fwhm=0
    out_fwhm_none = smooth_img(img1, fwhm=None)
    out_fwhm_zero = smooth_img(img1, fwhm=0.0)

    assert_array_equal(get_data(out_fwhm_none), get_data(out_fwhm_zero))

    data1 = np.zeros((10, 11, 12))
    data1[2:4, 1:5, 3:6] = 1
    data2 = np.zeros((13, 14, 15))
    data2[2:4, 1:5, 3:6] = 9
    img1_nifti2 = Nifti2Image(data1, affine=affine_eye)
    img2_nifti2 = Nifti2Image(data2, affine=affine_eye)
    out = smooth_img([img1_nifti2, img2_nifti2], fwhm=1.0)


def test_crop_img_to():
    data = np.zeros((5, 6, 7))
    data[2:4, 1:5, 3:6] = 1
    affine = np.diag((4, 3, 2, 1))
    img = Nifti1Image(data, affine=affine)

    slices = [slice(2, 4), slice(1, 5), slice(3, 6)]
    cropped_img = _crop_img_to(img, slices, copy=False)

    new_origin = np.array((4, 3, 2)) * np.array((2, 1, 3))

    # check that correct part was extracted:
    assert (get_data(cropped_img) == 1).all()
    assert cropped_img.shape == (2, 4, 3)

    # check that affine was adjusted correctly
    assert (cropped_img.affine[:3, 3] == new_origin).all()

    # check that data was really not copied
    data[2:4, 1:5, 3:6] = 2

    assert (get_data(cropped_img) == 2).all()

    # check that copying works
    copied_cropped_img = _crop_img_to(img, slices)

    data[2:4, 1:5, 3:6] = 1
    assert (get_data(copied_cropped_img) == 2).all()


def test_crop_img():
    data = np.zeros((5, 6, 7))
    data[2:4, 1:5, 3:6] = 1
    affine = np.diag((4, 3, 2, 1))
    img = Nifti1Image(data, affine=affine)

    cropped_img = crop_img(img)

    # correction for padding with "-1"
    # check that correct part was extracted:
    # This also corrects for padding
    assert (get_data(cropped_img)[1:-1, 1:-1, 1:-1] == 1).all()
    assert cropped_img.shape == (2 + 2, 4 + 2, 3 + 2)


def test_crop_img_copied_header(img_4d_mni_tr2):
    # Test equality of header fields between input and output
    # create zero padded data
    data = np.zeros((10, 10, 10, 10))
    data[0:4, 0:4, 0:4, :] = 1
    # replace the img_4d_mni_tr2 values with data
    img_4d_mni_tr2_zero_padded = new_img_like(
        img_4d_mni_tr2, data=data, affine=img_4d_mni_tr2.affine
    )
    cropped_img = crop_img(img_4d_mni_tr2_zero_padded)
    # only dim[1:4] should be different
    assert (
        cropped_img.header["dim"][1:4]
        != img_4d_mni_tr2_zero_padded.header["dim"][1:4]
    ).all()
    # other dim indices should be the same
    assert (
        cropped_img.header["dim"][0]
        == img_4d_mni_tr2_zero_padded.header["dim"][0]
    )
    assert (
        cropped_img.header["dim"][4:]
        == img_4d_mni_tr2_zero_padded.header["dim"][4:]
    ).all()
    # other header fields should also be same
    match_headers_keys(
        cropped_img, img_4d_mni_tr2_zero_padded, except_keys=["dim"]
    )


def test_crop_threshold_tolerance(affine_eye):
    """Check if crop can skip values that are extremely close to zero.

    In a relative sense and will crop them away
    """
    data = np.zeros([10, 14, 12])
    data[3:7, 3:7, 5:9] = 1.0
    active_shape = (4 + 2, 4 + 2, 4 + 2)  # add padding

    # add an infinitesimal outside this block
    data[3, 3, 3] = 1e-12
    img = Nifti1Image(data, affine=affine_eye)

    cropped_img = crop_img(img)

    assert cropped_img.shape == active_shape


@pytest.mark.parametrize("pad", [True, False])
def test_crop_image_empty_image(affine_eye, pad):
    """Test nilearn.image.image.crop_img with empty image specified.

    Added as a regression test for
    https://github.com/nilearn/nilearn/issues/5837.
    """
    data = np.zeros([10, 14, 12])
    img = Nifti1Image(data, affine=affine_eye)

    img_copy = new_img_like(img, get_data(img), img.affine)
    with pytest.warns(UserWarning, match="No values above "):
        cropped_img = crop_img(img_copy, pad=pad)
    assert_array_equal(get_data(img), get_data(cropped_img))


@pytest.mark.thread_unsafe
@pytest.mark.parametrize("images_to_mean", _images_to_mean())
def test_mean_img(images_to_mean, tmp_path):
    affine = np.diag((4, 3, 2, 1))

    truth = _mean_ground_truth(images_to_mean)

    img = mean_img(images_to_mean)

    assert_array_equal(img.affine, affine)
    assert_array_equal(get_data(img), truth)

    # Test with files
    imgs = testing.write_imgs_to_path(*images_to_mean, file_path=tmp_path)
    img = mean_img(imgs)

    assert_array_equal(img.affine, affine)
    if X64:
        assert_array_equal(get_data(img), truth)
    else:
        # We don't really understand but arrays are not
        # exactly equal on 32bit. Given that you can not do
        # much real world data analysis with nilearn on a
        # 32bit machine it is not worth investigating more
        assert_allclose(
            get_data(img),
            truth,
            rtol=np.finfo(truth.dtype).resolution,
            atol=0,
        )


def test_mean_img_resample(rng):
    # Test resampling in mean_img with a permutation of the axes
    data = rng.uniform(size=(5, 6, 7, 40))
    affine = np.diag((4, 3, 2, 1))
    img = Nifti1Image(data, affine=affine)
    mean_img_to_resample = Nifti1Image(data.mean(axis=-1), affine=affine)

    target_affine = affine[:, [1, 0, 2, 3]]  # permutation of axes

    mean_img_with_resampling = mean_img(img, target_affine=target_affine)

    resampled_mean_image = resample_img(
        mean_img_to_resample, target_affine=target_affine
    )

    assert_array_equal(
        get_data(resampled_mean_image), get_data(mean_img_with_resampling)
    )
    assert_array_equal(
        resampled_mean_image.affine, mean_img_with_resampling.affine
    )
    assert_array_equal(mean_img_with_resampling.affine, target_affine)


def test_mean_img_copied_header(img_4d_mni_tr2):
    # Test equality of header fields between input and output
    result = mean_img(img_4d_mni_tr2)
    match_headers_keys(
        result,
        img_4d_mni_tr2,
        except_keys=["dim", "pixdim", "cal_max", "cal_min"],
    )


def test_mean_img_surface(surf_img_1d, surf_img_2d):
    """Check that mean is properly computed over 'time points'."""
    # one 'time point' image returns same
    img = mean_img(surf_img_1d)

    assert_surface_image_equal(img, surf_img_1d)

    # image with left hemisphere
    # where timepoint 1 has all values == 0
    # and timepoint 2 == 1
    two_time_points_img = surf_img_2d(2)
    two_time_points_img.data.parts["left"][:, 0] = np.zeros(shape=4)
    two_time_points_img.data.parts["left"][:, 1] = np.ones(shape=4)

    img = mean_img(two_time_points_img)

    assert_array_equal(img.data.parts["left"], np.ones(shape=(4,)) * 0.5)
    assert img.shape == (img.mesh.n_vertices,)


def test_mean_img_surface_list(surf_img_2d):
    """Check that mean_img computes mean of mean."""
    surf_img_1 = surf_img_2d(2)
    surf_img_2 = surf_img_2d(3)

    mean_surf_img_1 = mean_img(surf_img_1)
    mean_surf_img_2 = mean_img(surf_img_2)

    direct_mean = mean_img([surf_img_1, surf_img_2])

    assert_surface_image_equal(
        direct_mean, mean_img([mean_surf_img_1, mean_surf_img_2])
    )


def test_swap_img_hemispheres(affine_eye, shape_3d_default, rng):
    # make sure input image data is not overwritten inside function
    data = rng.standard_normal(size=shape_3d_default)
    data_img = Nifti1Image(data, affine_eye)

    swap_img_hemispheres(data_img)

    assert_array_equal(get_data(data_img), data)
    # swapping operations work
    assert_array_equal(  # one turn
        get_data(swap_img_hemispheres(data_img)), data[::-1]
    )
    assert_array_equal(  # two turns -> back to original data
        get_data(swap_img_hemispheres(swap_img_hemispheres(data_img))),
        data,
    )


def test_index_img_error_3d(
    img_4d_ones_eye, img_3d_ones_eye, surf_img_1d, surf_img_2d
):
    """Check index_img only works on 4D volume data or 2D surface data."""
    index_img(img_4d_ones_eye, 0)
    expected_error_msg = (
        "Input data has incompatible dimensionality: "
        "Expected dimension is 4D and you provided "
        "a 3D image."
    )
    with pytest.raises(DimensionError, match=expected_error_msg):
        index_img(img_3d_ones_eye, 0)

    index_img(surf_img_2d(2), 0)
    with pytest.raises(
        ValueError, match="Data for each part of imgs should be 2D"
    ):
        index_img(surf_img_1d, 0)


def test_index_img_volume():
    """Test index_img with volume data."""
    img_4d, _ = generate_fake_fmri(affine=NON_EYE_AFFINE)

    fourth_dim_size = img_4d.shape[3]
    tested_indices = [
        *range(fourth_dim_size),
        slice(2, 8, 2),
        [1, 2, 3, 2],
        np.asarray([1, 2, 3, 2]),
        (np.arange(fourth_dim_size) % 3) == 1,  # boolean indexing with array
        (
            (np.arange(fourth_dim_size) % 3) == 1
        ).tolist(),  # boolean indexing with list
    ]
    for i in tested_indices:
        this_img = index_img(img_4d, i)

        expected_data_3d = get_data(img_4d)[..., i]
        assert_array_equal(get_data(this_img), expected_data_3d)
        assert_array_equal(this_img.affine, img_4d.affine)


@pytest.mark.parametrize("length", [20])
@pytest.mark.parametrize(
    "index, expected_n_samples",
    [
        ([*range(20)], 20),
        (slice(2, 8, 2), 3),
        ([1, 2, 3, 2], 4),
        (np.asarray([1, 2, 3, 2]), 4),
        (
            ((np.arange(20) % 3) == 1).tolist(),
            7,
        ),  # boolean indexing with array
        ((np.arange(20) % 3) == 1, 7),  # boolean indexing with array
    ],
)
def test_index_img_surface(surf_img_2d, length, index, expected_n_samples):
    """Test index_img with surface data."""
    input_img = surf_img_2d(length)
    this_img = index_img(input_img, index)

    assert this_img.data._n_samples == expected_n_samples

    assert_polymesh_equal(this_img.mesh, input_img.mesh)

    expected_data_3d = extract_data(input_img, index)
    for hemi, value in this_img.data.parts.items():
        assert_array_equal(value, expected_data_3d[hemi])


def test_index_img_error_volume_4d(affine_eye):
    """Test impossible indices with volume data."""
    img_4d, _ = generate_fake_fmri(affine=affine_eye)
    fourth_dim_size = img_4d.shape[3]
    for i in [
        fourth_dim_size,
        -fourth_dim_size - 1,
        [0, fourth_dim_size],
        np.repeat(True, fourth_dim_size + 1),
    ]:
        with pytest.raises(
            IndexError,
            match=(
                r"too large|out of bounds|invalid index|out of range|"
                "boolean index"
            ),
        ):
            index_img(img_4d, i)


@pytest.mark.parametrize("length", [20])
@pytest.mark.parametrize("index", [20, -21, [0, 20], np.repeat(True, 21)])
def test_index_img_error_surface_2d(surf_img_2d, length, index):
    """Test impossible indices with surface data."""
    with pytest.raises(IndexError, match=r"out of bounds|boolean"):
        index_img(surf_img_2d(length), index)


def test_pd_index_img(rng, img_4d_rand_eye):
    """Confirm indices from pandas dataframes are handled correctly."""
    fourth_dim_size = img_4d_rand_eye.shape[3]

    arr = rng.uniform(size=fourth_dim_size) > 0.5

    np_index_img = index_img(img_4d_rand_eye, arr)
    pd_index_img = index_img(img_4d_rand_eye, pd.DataFrame({"arr": arr}))

    assert_array_equal(get_data(np_index_img), get_data(pd_index_img))


def test_iter_img_3d_imag_error(affine_eye):
    img_3d = Nifti1Image(np.ones((3, 4, 5)), affine_eye)
    expected_error_msg = (
        "Input data has incompatible dimensionality: "
        "Expected dimension is 4D and you provided "
        "a 3D image."
    )
    with pytest.raises(DimensionError, match=expected_error_msg):
        iter_img(img_3d)


@pytest.mark.slow
def test_iter_img(tmp_path):
    img_4d, _ = generate_fake_fmri(affine=NON_EYE_AFFINE)

    for i, img in enumerate(iter_img(img_4d)):
        expected_data_3d = get_data(img_4d)[..., i]

        assert_array_equal(get_data(img), expected_data_3d)
        assert_array_equal(img.affine, img_4d.affine)

    img_4d_filename = testing.write_imgs_to_path(img_4d, file_path=tmp_path)
    for i, img in enumerate(iter_img(img_4d_filename)):
        expected_data_3d = get_data(img_4d)[..., i]

        assert_array_equal(get_data(img), expected_data_3d)
        assert_array_equal(img.affine, img_4d.affine)

    # enables to delete "img_4d_filename" on windows
    del img

    img_3d_list = list(iter_img(img_4d))
    for i, img in enumerate(iter_img(img_3d_list)):
        expected_data_3d = get_data(img_4d)[..., i]

        assert_array_equal(get_data(img), expected_data_3d)
        assert_array_equal(img.affine, img_4d.affine)

    img_3d_filenames = testing.write_imgs_to_path(
        *img_3d_list, file_path=tmp_path
    )
    for i, img in enumerate(iter_img(img_3d_filenames)):
        expected_data_3d = get_data(img_4d)[..., i]

        assert_array_equal(get_data(img), expected_data_3d)
        assert_array_equal(img.affine, img_4d.affine)

    # enables to delete "img_3d_filename" on windows
    del img


def test_iter_surface_img(surf_img_2d):
    """Check iter_img returns list of SurfaceImage.

    Each SurfaceImage must have same mesh as input
    and data from one of the sample of the input SurfaceImage.
    """
    input = surf_img_2d(5)
    output = list(iter_img(input))

    assert isinstance(output, list)
    assert len(output) == input.shape[1]
    assert all(isinstance(x, SurfaceImage) for x in output)
    for i in range(input.shape[1]):
        assert_polymesh_equal(output[i].mesh, input.mesh)
        assert_array_equal(
            np.squeeze(output[i].data.parts["left"]),
            input.data.parts["left"][..., i],
        )


@pytest.mark.thread_unsafe
def test_iter_img_surface_2d(surf_img_1d, surf_img_2d):
    """Return as is if surface image is 2D."""
    input = surf_img_2d(1)
    output = list(iter_img(input))

    assert_surface_image_equal(output[0], input)

    output = list(iter_img(surf_img_1d))

    assert_surface_image_equal(output[0], surf_img_1d)


def test_new_img_like_mgz():
    """Check that new images can be generated with bool MGZ type.

    This is usually when computing masks using MGZ inputs, e.g.
    when using plot_stap_map
    """
    img_filename = Path(__file__).parent / "data" / "test.mgz"
    ref_img = load(img_filename)
    data = np.ones(get_data(ref_img).shape, dtype=bool)
    affine = ref_img.affine
    new_img_like(ref_img, data, affine)


def test_new_img_like():
    # Give a list to new_img_like
    data = np.zeros((5, 6, 7))
    data[2:4, 1:5, 3:6] = 1
    affine = np.diag((4, 3, 2, 1))
    img = Nifti1Image(data, affine=affine)

    img2 = new_img_like([img], data)

    assert_array_equal(get_data(img), get_data(img2))

    # test_new_img_like_with_nifti2image_copy_header
    img_nifti2 = Nifti2Image(data, affine=affine)

    img2_nifti2 = new_img_like([img_nifti2], data)

    assert_array_equal(get_data(img_nifti2), get_data(img2_nifti2))


def test_new_img_like_accepts_paths(affine_eye, tmp_path, rng):
    """Check that new_img_like can accept instances of pathlib.Path."""
    nifti_path = tmp_path / "sample.nii"
    assert isinstance(nifti_path, Path)

    data = rng.random((10, 10, 10))
    img = Nifti1Image(data, affine_eye)
    img.to_filename(nifti_path)

    new_data = rng.random((10, 10, 10))
    new_img = new_img_like(nifti_path, new_data)
    assert new_img.shape == (10, 10, 10)

    # Check that list of pathlib.Path also accepted
    new_img = new_img_like([nifti_path], new_data)
    assert new_img.shape == (10, 10, 10)


def test_new_img_like_non_iterable_header(rng):
    """Tests that when an niimg's header is not iterable \
       and it is set to be copied, an error is not raised.
    """
    fake_fmri_data = rng.uniform(size=_shape_4d_default())
    fake_affine = rng.uniform(size=(4, 4))
    fake_spatial_image = spatialimages.SpatialImage(
        fake_fmri_data, fake_affine
    )

    assert new_img_like(fake_spatial_image, data=fake_fmri_data)


def test_new_img_like_int64(shape_3d_default):
    img = generate_labeled_regions(shape=shape_3d_default, n_regions=2)

    data = get_data(img).astype("int32")

    with warnings.catch_warnings():
        warnings.simplefilter("error")
        new_img = new_img_like(img, data)
    assert get_data(new_img).dtype == "int32"

    data = data.astype("int64")

    with pytest.warns(UserWarning, match=r".*array.*contains.*64.*"):
        new_img = new_img_like(img, data)
    assert get_data(new_img).dtype == "int32"

    data[:] = 2**40

    with pytest.warns(UserWarning, match=r".*64.*too large.*"):
        new_img = new_img_like(img, data)
    assert get_data(new_img).dtype == "int64"


@pytest.mark.thread_unsafe
def test_input_in_threshold_img(
    shape_3d_default, surf_img_1d, surf_mask_1d, affine_eye
):
    """Check threshold_img only works with surface OR volume."""
    threshold = 0.5

    vol_img, _ = generate_maps(shape_3d_default, n_regions=2)
    vol_mask = Nifti1Image(np.ones(shape_3d_default), affine_eye)

    # All of those should be OK
    thr_img = threshold_img(vol_img, threshold=threshold, mask_img=None)

    _check_thresholded_output(vol_img, thr_img, threshold)

    thr_img = threshold_img(surf_img_1d, threshold=threshold, mask_img=None)

    # same but with a mask
    threshold_img(
        vol_img,
        threshold=threshold,
        mask_img=vol_mask,
    )
    threshold_img(
        surf_img_1d,
        threshold=threshold,
        mask_img=surf_mask_1d,
    )


@pytest.mark.thread_unsafe
def test_input_in_threshold_img_several_timepoints(
    img_4d_rand_eye, surf_img_2d
):
    """Check threshold_img works with 2D surface OR 4D volume."""
    threshold = 0.5

    thr_img = threshold_img(img_4d_rand_eye, threshold=0.5)

    _check_thresholded_output(img_4d_rand_eye, thr_img, threshold)

    original_image = surf_img_2d(5)
    thr_img = threshold_img(original_image, threshold=0.5)

    _check_thresholded_output(original_image, thr_img, threshold)


def _check_thresholded_output(input, output, threshold):
    """Check data was properly thresholed.

    Assumes:
    - a one-sided threshold that keeps data > threshold
    - no extra mask used
    """
    if isinstance(input, Nifti1Image):
        original_data = input.get_fdata()
        data = output.get_fdata()
    elif isinstance(input, SurfaceImage):
        original_data = get_surface_data(input)
        data = get_surface_data(output)

    non_zero = data != 0
    assert np.all(data[non_zero] > threshold)

    data_to_mask = original_data < threshold
    assert np.all(data[data_to_mask] == 0)


@pytest.mark.thread_unsafe
def test_input_in_threshold_img_errors(
    shape_3d_default, surf_img_1d, surf_mask_1d, affine_eye
):
    """Check invalid inputs to threshold_img ."""
    vol_img, _ = generate_maps(shape_3d_default, n_regions=2)
    vol_mask = Nifti1Image(np.ones(shape_3d_default), affine_eye)

    # invalid input: img is an int
    with pytest.raises(
        TypeError,
        match=r"'img' should be a 3D/4D Niimg-like object or a SurfaceImage.",
    ):
        threshold_img(img=1, threshold=1)

    # incompatible inputs raise errors
    with pytest.raises(
        TypeError,
        match=r"Mask and input images must be of compatible types.",
    ):
        threshold_img(vol_img, threshold=1, mask_img=surf_mask_1d)
    with pytest.raises(
        TypeError,
        match=r"Mask and input images must be of compatible types.",
    ):
        threshold_img(surf_img_1d, threshold=1, mask_img=vol_mask)


@pytest.mark.thread_unsafe
def test_threshold_img_warning_smoke(surf_img_1d):
    """Check threshold_img with cluster."""
    threshold_img(surf_img_1d, threshold=1, cluster_threshold=10)


@pytest.mark.thread_unsafe
@pytest.mark.parametrize("two_sided", [True, False])
def test_validity_threshold_value_in_threshold_img(
    shape_3d_default, two_sided
):
    """Check that invalid values to threshold_img's threshold parameter \
       raise Exceptions.
    """
    maps, _ = generate_maps(shape_3d_default, n_regions=2)

    # testing to raise same error when threshold=None case
    with pytest.raises(
        TypeError,
        match="threshold should be either a number or a string",
    ):
        threshold_img(maps, threshold=None)

    threshold = object()
    with pytest.raises(TypeError, match="be of type"):
        threshold_img(
            maps,
            threshold=threshold,
            two_sided=two_sided,
        )

    invalid_threshold_values = ["90t%", "s%", "t", "0.1"]
    name = "threshold"
    for thr in invalid_threshold_values:
        with pytest.raises(
            ValueError,
            match=f"{name}.+should be a number followed by the percent",
        ):
            threshold_img(
                maps,
                threshold=thr,
                two_sided=two_sided,
            )


@pytest.mark.thread_unsafe
def test_validity_negative_threshold_value_in_threshold_img(shape_3d_default):
    """Check that negative values to threshold_img's threshold parameter \
       raise Exceptions.
    """
    maps, _ = generate_maps(shape_3d_default, n_regions=2)

    # invalid threshold values when two_sided=True
    thresholds = [-10, "-10%"]
    for wrong_threshold in thresholds:
        with pytest.raises(ValueError, match="should not be a negative"):
            threshold_img(
                maps,
                threshold=wrong_threshold,
                two_sided=True,
            )

    with pytest.raises(ValueError, match="should not be a negative"):
        threshold_img(maps, threshold="-10%", two_sided=False)


@pytest.mark.thread_unsafe
@pytest.mark.slow
def test_threshold_img(affine_eye):
    """Smoke test for threshold_img with valid threshold inputs."""
    shape = (10, 20, 30)
    maps, _ = generate_maps(shape, n_regions=4)
    mask_img = Nifti1Image(np.ones((shape), dtype=np.int8), affine_eye)

    for img in iter_img(maps):
        # when threshold is a float value
        threshold_img(img, threshold=0.8)

        # when we provide mask image
        threshold_img(img, threshold=1, mask_img=mask_img)

        # when threshold is a percentile
        threshold_img(img, threshold="2%")


@pytest.mark.thread_unsafe
@pytest.mark.parametrize(
    "threshold, expected_n_non_zero",
    [
        (1, {"left": 2, "right": 4}),
        (10, {"left": 0, "right": 3}),
        (50, {"left": 0, "right": 0}),
        ("50%", {"left": 0, "right": 4}),
    ],
)
def test_threshold_surf_img_1d(surf_img_1d, threshold, expected_n_non_zero):
    """Check number of elements surviving thresholding 1D surface image.

    For left hemisphere: 1 < values < 10
    For right hemisphere: 10 < values < 50
    """
    thr_img = threshold_img(surf_img_1d, threshold=threshold)
    for hemi in thr_img.data.parts:
        data = thr_img.data.parts[hemi]
        assert len(data[np.nonzero(data)]) == expected_n_non_zero[hemi]


@pytest.mark.thread_unsafe
@pytest.mark.parametrize(
    "threshold, expected_n_non_zero",
    [
        (0.9, {"left": 2, "right": 3}),
        (9, {"left": 0, "right": 3}),
        (50, {"left": 0, "right": 0}),
        ("50%", {"left": 0, "right": 2}),
    ],
)
def test_threshold_surf_img_1d_with_mask(
    surf_img_1d, threshold, expected_n_non_zero, surf_mask_1d
):
    """Check number of elements surviving thresholding 1D surface image.

    Fewer elements survive thresholding when a mask is provided.

    For left hemisphere: 1 <= values < 10
    For right hemisphere: 10 <= values < 50
    """
    thr_img = threshold_img(
        surf_img_1d, threshold=threshold, mask_img=surf_mask_1d
    )
    for hemi in thr_img.data.parts:
        data = thr_img.data.parts[hemi]
        assert len(data[np.nonzero(data)]) == expected_n_non_zero[hemi]


@pytest.mark.thread_unsafe
@pytest.mark.parametrize(
    "threshold, expected_n_non_zero, two_sided",
    [
        (1, {"left": 0, "right": 4}, False),
        (29, {"left": 0, "right": 2}, False),
        (50, {"left": 0, "right": 0}, False),
        ("50%", {"left": 0, "right": 3}, False),
        (1, {"left": 3, "right": 4}, True),
        (29, {"left": 1, "right": 2}, True),
        (50, {"left": 0, "right": 0}, True),
        ("50%", {"left": 1, "right": 2}, True),
    ],
)
def test_threshold_surf_img_1d_negative_values(
    surf_img_1d, threshold, expected_n_non_zero, two_sided
):
    """Check number of elements surviving thresholding 1D surface image.

    Data also includes negative values.

    Also test 2 sided thresholding.

    For left hemisphere: -41 < values < -9
    For right hemisphere: 9 < values < 50
    """
    surf_img_1d.data.parts["left"] *= -10

    thr_img = threshold_img(
        surf_img_1d, threshold=threshold, two_sided=two_sided
    )
    for hemi in thr_img.data.parts:
        data = thr_img.data.parts[hemi]
        assert len(data[np.nonzero(data)]) == expected_n_non_zero[hemi]


@pytest.mark.thread_unsafe
@pytest.mark.parametrize(
    "threshold, two_sided, expected",
    [
        (3, True, 16),
        (3, False, 8),
        (4, True, 0),
        (4, False, 0),
        (0, True, 448),
        (0, False, 224),
        (-3, False, 8),
        (-0.5, False, 224),
        ("0%", True, 448),
        ("10%", False, 224),
        ("99%", False, 8),
        ("99%", True, 16),
        ("100%", True, 0),
    ],
)
def test_threshold_img_with_mask(
    stat_img_test_data, affine_eye, threshold, two_sided, expected
):
    """Tests `nilearn.image.threshold_img` for float and str values using
    mask.
    """
    temp_mask = np.ones((stat_img_test_data.shape), dtype=np.int8)

    temp_mask[8:11, 0, 0] = 0  # mask values 5
    temp_mask[13:16, 0, 0] = 0  # mask values -5
    temp_mask[19:, 10:, 6:] = 0
    temp_mask[:4, 10:, 6:] = 0
    temp_mask[13:19, 10:, 6:] = 0
    temp_mask[13:19, 0:4, 6:] = 0
    mask_img = Nifti1Image(temp_mask, affine_eye)

    thr_img = threshold_img(
        img=stat_img_test_data,
        mask_img=mask_img,
        threshold=threshold,
        two_sided=two_sided,
    )

    img_data = thr_img.get_fdata()
    assert len(img_data[np.nonzero(img_data)]) == expected


@pytest.mark.thread_unsafe
@pytest.mark.parametrize(
    "threshold,two_sided,cluster_threshold,expected",
    [
        (2, False, 0, [0, 4, 5]),  # cluster with values > 2
        (2, True, 0, [-5, -4, 0, 4, 5]),  # cluster with |values| > 2
        (2, True, 5, [-4, 0, 4]),  # cluster: |values| > 2 & size > 5
        (2, True, 5, [-4, 0, 4]),  # cluster: |values| > 2 & size > 5
        (0.5, True, 5, [-4, -1, 0, 1, 4]),  # cluster: |values| > 0.5 & size>5
        (0.5, False, 5, [0, 1, 4]),  # cluster: values > 0.5 & size > 5
    ],
)
def test_threshold_img_with_cluster_threshold(
    stat_img_test_data, threshold, two_sided, cluster_threshold, expected
):
    """Check that passing specific threshold and cluster threshold values \
    only gives cluster the right number of voxels with the right values.
    """
    thr_img = threshold_img(
        img=stat_img_test_data,
        threshold=threshold,
        two_sided=two_sided,
        cluster_threshold=cluster_threshold,
    )

    assert np.array_equal(np.unique(thr_img.get_fdata()), np.array(expected))


@pytest.mark.thread_unsafe
def test_threshold_img_threshold_n_clusters(stat_img_test_data):
    """With a cluster threshold of 5 we get 8 clusters with |values| > 2 \
       and cluster sizes > 5.
    """
    thr_img = threshold_img(
        img=stat_img_test_data,
        threshold=2,
        two_sided=True,
        cluster_threshold=5,
    )

    assert np.sum(thr_img.get_fdata() == 4) == 8


@pytest.mark.thread_unsafe
def test_threshold_img_no_copy_surface(surf_img_1d):
    """Test copy=False on surface data.

    Check that not copying does mutate the original image.
    """
    threshold = 15
    input_img = surf_img_1d
    result = threshold_img(input_img, threshold=threshold, copy=False)
    assert_surface_image_equal(result, surf_img_1d)


@pytest.mark.thread_unsafe
def test_threshold_img_copy_surface(surf_img_1d):
    """Test copy=True on surface data.

    Check that copying does not mutate the original image.
    """
    threshold = 15
    input_img = surf_img_1d
    result = threshold_img(input_img, threshold=threshold, copy=True)
    with pytest.raises(ValueError):
        assert_surface_image_equal(result, surf_img_1d)


@pytest.mark.thread_unsafe
def test_threshold_img_copy_volume(img_4d_ones_eye):
    """Test the behavior of threshold_img's copy parameter."""
    threshold = 1
    # Check that copy does not mutate. It returns modified copy.
    thr_img = threshold_img(img_4d_ones_eye, threshold)

    # Original img_ones should have all ones.
    assert_array_equal(get_data(img_4d_ones_eye), np.ones(_shape_4d_default()))
    # Thresholded should have all zeros.
    assert_array_equal(get_data(thr_img), np.zeros(_shape_4d_default()))

    # Check that not copying does mutate.
    img_to_mutate = img_4d_ones_eye

    thr_img = threshold_img(img_to_mutate, threshold, copy=False)

    # Check that original mutates
    assert_array_equal(get_data(img_to_mutate), np.zeros(_shape_4d_default()))
    # And that returned value is also thresholded.
    assert_array_equal(get_data(img_to_mutate), get_data(thr_img))


@pytest.mark.thread_unsafe
def test_isnan_threshold_img_data(affine_eye, shape_3d_default):
    """Check threshold_img converges properly when input image has nans."""
    maps, _ = generate_maps(shape_3d_default, n_regions=2)
    data = get_data(maps)
    data[:, :, 0] = np.nan

    maps_img = Nifti1Image(data, affine_eye)

    threshold_img(maps_img, threshold=0.8)


@pytest.mark.thread_unsafe
def test_threshold_img_copied_header(img_4d_mni_tr2):
    # Test equality of header fields between input and output
    thr_img = threshold_img(img_4d_mni_tr2, threshold=0.5)
    # only the min value should be different
    match_headers_keys(
        thr_img,
        img_4d_mni_tr2,
        except_keys=["cal_min"],
    )
    # min value should be 0 in the result
    assert thr_img.header["cal_min"] == 0


@pytest.mark.thread_unsafe
def test_math_img_exceptions(affine_eye, img_4d_ones_eye, surf_img_2d):
    img1 = img_4d_ones_eye
    img2 = Nifti1Image(np.zeros((10, 20, 10, 10)), affine_eye)
    img3 = img_4d_ones_eye
    img4 = Nifti1Image(np.ones(_shape_4d_default()), affine_eye * 2)

    formula = "np.mean(img1, axis=-1) - np.mean(img2, axis=-1)"
    # Images with different shapes should raise a ValueError exception.
    with pytest.raises(ValueError, match="Input images cannot be compared"):
        math_img(formula, img1=img1, img2=img2)

    # Images with different affines should raise a ValueError exception.
    with pytest.raises(ValueError, match="Input images cannot be compared"):
        math_img(formula, img1=img1, img2=img4)

    bad_formula = "np.toto(img1, axis=-1) - np.mean(img3, axis=-1)"
    with pytest.raises(
        AttributeError, match="Input formula couldn't be processed"
    ):
        math_img(bad_formula, img1=img1, img3=img3)
    # Same but for surface data
    with pytest.raises(
        AttributeError, match="Input formula couldn't be processed"
    ):
        math_img(bad_formula, img1=surf_img_2d(2), img3=surf_img_2d(3))

    # Test copy_header_from parameter
    # Copying header from 4d image to a result that is 3d should raise a
    # ValueError
    formula = "np.mean(img1, axis=-1) - np.mean(img3, axis=-1)"
    with pytest.raises(ValueError, match=r"Cannot copy the header."):
        math_img(formula, img1=img1, img3=img3, copy_header_from="img1")

    # Passing an 'img*' variable (to copy_header_from) that is not in the
    # formula or an img* argument should raise a KeyError exception.
    with pytest.raises(KeyError):
        math_img(formula, img1=img1, img3=img3, copy_header_from="img2")


@pytest.mark.thread_unsafe
def test_math_img(
    affine_eye, img_4d_ones_eye, img_4d_zeros_eye, shape_3d_default, tmp_path
):
    img1 = img_4d_ones_eye
    img2 = img_4d_zeros_eye
    expected_result = Nifti1Image(np.ones(shape_3d_default), affine_eye)

    formula = "np.mean(img1, axis=-1) - np.mean(img2, axis=-1)"
    for create_files in (True, False):
        imgs = testing.write_imgs_to_path(
            img1, img2, file_path=tmp_path, create_files=create_files
        )
        result = math_img(formula, img1=imgs[0], img2=imgs[1])
        assert_array_equal(get_data(result), get_data(expected_result))
        assert_array_equal(result.affine, expected_result.affine)
        assert result.shape == expected_result.shape


@pytest.mark.thread_unsafe
def test_math_img_surface(surf_img_2d):
    """Test math_img on surface data."""
    img1 = surf_img_2d(1)
    img2 = surf_img_2d(3)

    tmp = {
        part: np.mean(img1.data.parts[part], axis=-1)
        - np.mean(img2.data.parts[part], axis=-1)
        for part in img1.data.parts
    }
    expected_result = SurfaceImage(mesh=img1.mesh, data=tmp)

    formula = "np.mean(img1, axis=-1) - np.mean(img2, axis=-1)"
    result = math_img(formula, img1=img1, img2=img2)

    assert isinstance(result, SurfaceImage)
    assert_surface_image_equal(result, expected_result)


@pytest.mark.thread_unsafe
def test_math_img_copy_default_header(
    img_4d_ones_eye_default_header, img_4d_ones_eye_tr2
):
    """Check where data values are not changed & header values are not copied.

    The result should have default header values.
    """
    formula_no_change = "img * 1"
    # using img_4d_ones_eye_tr2 with edited header in the formula
    result = math_img(
        formula_no_change, img=img_4d_ones_eye_tr2, copy_header_from=None
    )
    # header values should NOT match the input image header values
    assert result.header != img_4d_ones_eye_default_header.header


@pytest.mark.thread_unsafe
def test_math_img_copied_header_from_img(img_4d_mni_tr2):
    # case where data values are not changed but header values are copied
    # the result should have the same header values as the given image
    formula_no_change = "img * 1"
    result = math_img(
        formula_no_change, img=img_4d_mni_tr2, copy_header_from="img"
    )
    # all header values should be the same
    assert result.header == img_4d_mni_tr2.header


@pytest.mark.thread_unsafe
def test_math_img_copied_header_data_values_changed(
    img_4d_ones_eye_default_header, img_4d_ones_eye_tr2
):
    # case where data values are changed and header values are copied from one
    # of the input images
    # the result should have the same header values as img_4d_ones_eye_tr2,
    # except for cal_max and cal_min that should be different
    formula_change_min_max = "img1 - img2"
    result = math_img(
        formula_change_min_max,
        img1=img_4d_ones_eye_default_header,
        img2=img_4d_ones_eye_tr2,
        copy_header_from="img2",
    )
    for key in img_4d_ones_eye_tr2.header:
        # cal_max and cal_min should be different in result
        if key in ["cal_max", "cal_min"]:
            assert result.header[key] != img_4d_ones_eye_tr2.header[key]
        # other header values should be the same
        elif isinstance(result.header[key], np.ndarray):
            assert_array_equal(
                result.header[key], img_4d_ones_eye_tr2.header[key]
            )
        else:
            assert result.header[key] == img_4d_ones_eye_tr2.header[key]


@pytest.mark.thread_unsafe
def test_binarize_img(img_4d_rand_eye):
    # Test that all output values are 1.
    img1 = binarize_img(img_4d_rand_eye)

    assert_array_equal(np.unique(img1.dataobj), np.array([1]))

    # Test that it works with threshold
    img2 = binarize_img(img_4d_rand_eye, threshold=0.5)

    assert_array_equal(np.unique(img2.dataobj), np.array([0, 1]))
    # Test that manual binarization equals binarize_img results.
    img3 = copy_img(img_4d_rand_eye)
    img3.dataobj[img_4d_rand_eye.dataobj < 0.5] = 0
    img3.dataobj[img_4d_rand_eye.dataobj >= 0.5] = 1

    assert_array_equal(img2.dataobj, img3.dataobj)


@pytest.mark.thread_unsafe
def test_binarize_img_surface(surf_img_1d):
    """Test binarize_img on surface data."""
    img = surf_img_1d
    for k, v in img.data.parts.items():
        img.data.parts[k] = v + 1
    # Test that all output values are 1.
    img1 = binarize_img(surf_img_1d)

    assert_array_equal(np.unique(get_surface_data(img1)), np.array([1]))

    # Test that it works with threshold
    img2 = binarize_img(surf_img_1d, threshold=9)
    assert_array_equal(np.unique(get_surface_data(img2)), np.array([0, 1]))


def test_binarize_negative_img(img_4d_rand_eye, rng):
    # Test option to use original or absolute values
    img_data = img_4d_rand_eye.dataobj
    # Create a mask for half of the values and make them negative
    neg_mask = rng.choice(
        [True, False], size=img_4d_rand_eye.shape, p=[0.5, 0.5]
    )
    img_data[neg_mask] *= -1
    img = new_img_like(img_4d_rand_eye, img_data)
    # Binarize using original and absolute values
    img_original = binarize_img(img, threshold=0, two_sided=False)
    img_absolute = binarize_img(img, threshold=0, two_sided=True)
    # Check that all values are 1 for absolute valued threshold
    assert_array_equal(np.unique(img_absolute.dataobj), np.array([1]))
    # Check that binarized image contains 0 and 1 for original threshold
    assert_array_equal(np.unique(img_original.dataobj), np.array([0, 1]))


def test_binarize_img_copied_header(img_4d_mni_tr2):
    # Test equality of header fields between input and output
    result = binarize_img(img_4d_mni_tr2, threshold=0.5)
    # only the min value should be different
    match_headers_keys(
        result,
        img_4d_mni_tr2,
        except_keys=["cal_min", "cal_max"],
    )
    # min value should be 0 in the result
    assert result.header["cal_min"] == 0
    # max value should be 1 in the result
    assert result.header["cal_max"] == 1


def test_binarize_img_no_userwarning(img_4d_rand_eye):
    # Test that a UserWarning is not thrown for a float64 img
    with warnings.catch_warnings():
        warnings.simplefilter("error", category=UserWarning)
        binarize_img(img_4d_rand_eye)


def test_clean_img(affine_eye, shape_3d_default, rng):
    data = rng.standard_normal(size=(10, 10, 10, 100)) + 0.5
    data_flat = data.T.reshape(100, -1)
    data_img = Nifti1Image(data, affine_eye)

    with pytest.raises(ValueError, match=r"t_r.*must be specified"):
        clean_img(data_img, t_r=None, low_pass=0.1)

    data_img_ = clean_img(
        data_img, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
    )
    data_flat_ = signal.clean(
        data_flat, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
    )

    assert_almost_equal(get_data(data_img_).T.reshape(100, -1), data_flat_)

    # if NANs
    data[:, 9, 9] = np.nan
    # if infinity
    data[:, 5, 5] = np.inf
    nan_img = Nifti1Image(data, affine_eye)

    clean_im = clean_img(
        nan_img, ensure_finite=True, standardize="zscore_sample"
    )

    assert np.any(np.isfinite(get_data(clean_im)))

    # test_clean_img_passing_nifti2image
    data_img_nifti2 = Nifti2Image(data, affine_eye)

    clean_img(
        data_img_nifti2, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
    )

    # if mask_img
    img, mask_img = generate_fake_fmri(shape=shape_3d_default, length=10)

    data_img_mask_ = clean_img(
        img, mask_img=mask_img, standardize="zscore_sample"
    )

    # Checks that output with full mask and without is equal
    data_img_ = clean_img(img, standardize="zscore_sample")

    assert_almost_equal(get_data(data_img_), get_data(data_img_mask_))


def test_clean_img_surface(surf_img_2d, surf_img_1d, surf_mask_1d) -> None:
    """Test clean on surface image.

    - check that clean returns image of same shape, geometry
      but different data
    - with mask should only clean the included vertices
    - 1D image should raise error.
    - check sample mask can be passed as a kwarg and used correctly
    """
    length = 50
    imgs = surf_img_2d(length)

    cleaned_img = clean_img(
        imgs, detrend=True, standardize=False, low_pass=0.1, t_r=1.0
    )

    assert cleaned_img.shape == imgs.shape
    assert_polymesh_equal(cleaned_img.mesh, imgs.mesh)
    with pytest.raises(ValueError, match="not equal"):
        assert_surface_image_equal(cleaned_img, imgs)

    cleaned_img_with_mask = clean_img(
        imgs,
        detrend=True,
        standardize=False,
        low_pass=0.1,
        t_r=1.0,
        mask_img=surf_mask_1d,
    )
    with pytest.raises(ValueError, match="not equal"):
        assert_surface_image_equal(cleaned_img_with_mask, cleaned_img)

    # Checks that output with full mask and without is equal
    full_mask = new_img_like(
        surf_mask_1d,
        data={k: np.ones(v.shape) for k, v in surf_mask_1d.data.parts.items()},
    )
    cleaned_img_with_full_mask = clean_img(
        imgs,
        detrend=True,
        standardize=False,
        low_pass=0.1,
        t_r=1.0,
        mask_img=full_mask,
    )
    assert_surface_image_equal(cleaned_img, cleaned_img_with_full_mask)

    # 1D fails
    with pytest.raises(ValueError, match="should be 2D"):
        clean_img(surf_img_1d, detrend=True)

    sample_mask = np.arange(length - 1)

    # check sample mask can be passed as a kwarg and used correctly
    cleaned_img = clean_img(
        imgs,
        detrend=True,
        standardize=False,
        low_pass=0.1,
        t_r=1.0,
        clean__sample_mask=sample_mask,
    )
    assert cleaned_img.shape[-1] == length - 1


@pytest.mark.thread_unsafe
@pytest.mark.parametrize("create_files", [True, False])
def test_largest_cc_img(create_files, tmp_path):
    """Check the extraction of the largest connected component, for niftis.

    Similar to smooth_img tests for largest connected_component_img, here also
    only the added features for largest_connected_component are tested.
    """
    # Test whether dimension of 3Dimg and list of 3Dimgs are kept.
    img1, img2, shapes = _make_largest_cc_img_test_data()

    imgs = testing.write_imgs_to_path(
        img1, img2, file_path=tmp_path, create_files=create_files
    )
    # List of images as input
    out = largest_connected_component_img(imgs)

    assert isinstance(out, list)
    assert len(out) == 2
    for o, s in zip(out, shapes, strict=False):
        assert o.shape == (s)

    # Single image as input
    out = largest_connected_component_img(imgs[0])

    assert isinstance(out, Nifti1Image)
    assert out.shape == (shapes[0])


@pytest.mark.thread_unsafe
@pytest.mark.parametrize("create_files", [True, False])
def test_largest_cc_img_non_native_endian_type(create_files, tmp_path):
    # Test whether dimension of 3Dimg and list of 3Dimgs are kept.
    img1, img2, shapes = _make_largest_cc_img_test_data()

    # tests adapted to non-native endian data dtype
    img1_change_dtype = Nifti1Image(
        get_data(img1).astype(">f8"), affine=img1.affine
    )
    img2_change_dtype = Nifti1Image(
        get_data(img2).astype(">f8"), affine=img2.affine
    )

    imgs = testing.write_imgs_to_path(
        img1_change_dtype,
        img2_change_dtype,
        file_path=tmp_path,
        create_files=create_files,
    )
    # List of images as input
    out = largest_connected_component_img(imgs)

    assert isinstance(out, list)
    assert len(out) == 2
    for o, s in zip(out, shapes, strict=False):
        assert o.shape == (s)

    # Single image as input
    out = largest_connected_component_img(imgs[0])

    assert isinstance(out, Nifti1Image)
    assert out.shape == (shapes[0])

    # Test the output with native and without native
    out_native = largest_connected_component_img(img1)

    out_non_native = largest_connected_component_img(img1_change_dtype)
    assert_equal(get_data(out_native), get_data(out_non_native))


@pytest.mark.thread_unsafe
def test_largest_cc_img_error(shape_3d_default):
    # Test whether 4D Nifti throws the right error.
    img_4D = generate_fake_fmri(shape_3d_default)

    with pytest.raises(DimensionError, match="dimension"):
        largest_connected_component_img(img_4D)


def test_new_img_like_mgh_image(affine_eye, shape_3d_default):
    data = np.zeros(shape_3d_default, dtype=np.uint8)
    niimg = MGHImage(dataobj=data, affine=affine_eye)

    new_img_like(niimg, data.astype(float), niimg.affine)


@pytest.mark.parametrize("image", [MGHImage, AnalyzeImage])
def test_new_img_like_boolean_data(affine_eye, image, shape_3d_default, rng):
    """Checks defaulting boolean input data to np.uint8 dtype is valid \
       forencoding with nibabel image classes MGHImage and AnalyzeImage.
    """
    data = rng.standard_normal(shape_3d_default).astype("uint8")
    in_img = image(dataobj=data, affine=affine_eye)

    out_img = new_img_like(in_img, data=in_img.get_fdata() > 0.5)

    assert get_data(out_img).dtype == "uint8"


def test_clean_img_sample_mask(img_4d_rand_eye, shape_4d_default):
    """Check sample mask can be passed as a kwarg and used correctly."""
    length = shape_4d_default[3]
    confounds = _basic_confounds(length)
    # exclude last time point
    sample_mask = np.arange(length - 1)

    img = clean_img(
        img_4d_rand_eye,
        confounds=confounds,
        clean__sample_mask=sample_mask,
        standardize="zscore_sample",
    )
    assert img.shape == (*shape_4d_default[:3], length - 1)


def test_clean_img_sample_mask_mask_img(shape_3d_default):
    """Check sample_mask and mask_img can be correctly used together."""
    length = 10
    confounds = _basic_confounds(length)
    img_4d, mask_img = generate_fake_fmri(
        shape=shape_3d_default, length=length
    )

    # exclude last time point
    sample_mask = np.arange(length - 1)

    # test with sample mask
    img = clean_img(
        img_4d,
        confounds=confounds,
        mask_img=mask_img,
        clean__sample_mask=sample_mask,
        standardize="zscore_sample",
    )
    assert img.shape == (*shape_3d_default, length - 1)


def test_concat_niimgs_errors(affine_eye, shape_3d_default):
    img1 = Nifti1Image(np.ones(shape_3d_default), affine_eye)
    img2 = Nifti1Image(np.ones(shape_3d_default), 2 * affine_eye)
    img4d = Nifti1Image(np.ones((*shape_3d_default, 2)), affine_eye)

    # check error for non-forced but necessary resampling
    with pytest.raises(ValueError, match="Field of view of image"):
        concat_imgs([img1, img2], auto_resample=False)

    # Regression test for #601.
    # Dimensionality of first image was not checked properly.
    _dimension_error_msg = (
        "Input data has incompatible dimensionality: "
        "Expected dimension is 4D and you provided "
        "a list of 4D images \\(5D\\)"
    )
    with pytest.raises(DimensionError, match=_dimension_error_msg):
        concat_imgs([img4d], ensure_ndim=4)

    with pytest.raises(DimensionError, match=_dimension_error_msg):
        concat_imgs([img1, img4d])

    img5d = Nifti1Image(np.ones((2, 2, 2, 2, 2)), affine_eye)
    with pytest.raises(
        TypeError,
        match=(
            r"Concatenated images must be 3D or 4D. "
            r"You gave a list of 5D images"
        ),
    ):
        concat_imgs([img5d, img5d])

    # check error when an empty list is specified
    with pytest.raises(TypeError, match="empty objects"):
        concat_imgs([])


def test_concat_niimgs(affine_eye, tmp_path):
    # create images different in affine and 3D/4D shape
    shape = (10, 11, 12)
    img1 = Nifti1Image(np.ones(shape), affine_eye)
    img2 = Nifti1Image(np.zeros(shape), affine_eye)

    shape2 = (12, 11, 10)
    img1b = Nifti1Image(np.ones(shape2), affine_eye)

    shape3 = (11, 22, 33)
    img1c = Nifti1Image(np.ones(shape3), affine_eye)

    # check basic concatenation with equal shape/affine
    # verbose for coverage
    concatenated = concat_imgs((img1, img2, img1), verbose=1)

    # smoke-test auto_resample
    concatenated = concat_imgs((img1, img1b, img1c), auto_resample=True)
    assert concatenated.shape == (*img1.shape, 3)

    # test list of 4D niimgs as input
    img1.to_filename(tmp_path / "1.nii")
    img2.to_filename(tmp_path / "2.nii")
    concatenated = concat_imgs(tmp_path / "*")
    assert_array_equal(get_data(concatenated)[..., 0], get_data(img1))
    assert_array_equal(get_data(concatenated)[..., 1], get_data(img2))


def test_concat_niimg_dtype(affine_eye):
    shape = [2, 3, 4]
    vols = [
        Nifti1Image(np.zeros([*shape, n_scans]).astype(np.int16), affine_eye)
        for n_scans in [1, 5]
    ]
    nimg = concat_imgs(vols)
    assert get_data(nimg).dtype == np.float32
    nimg = concat_imgs(vols, dtype=None)
    assert get_data(nimg).dtype == np.int16


def test_concat_imgs_surface(surf_img_2d):
    """Check concat_imgs returns a single SurfaceImage.

    Output must have as many samples as the sum of samples in the input.
    """
    img = concat_imgs([surf_img_2d(3), surf_img_2d(5)], dtype=np.float16)
    assert img.shape == (9, 8)
    for value in img.data.parts.values():
        assert value.ndim == 2
        assert value.dtype == np.float16


def nifti_generator(buffer):
    for _ in range(10):
        buffer.append(_img_3d_rand())
        yield buffer[-1]


def test_iterator_generator(img_3d_rand_eye):
    # Create a list of random images
    list_images = [img_3d_rand_eye for _ in range(10)]
    cc = concat_imgs(list_images)
    assert cc.shape[-1] == 10
    assert_array_almost_equal(get_data(cc)[..., 0], get_data(list_images[0]))

    # Same with iteration
    i = iter_img(list_images)
    cc = concat_imgs(i)
    assert cc.shape[-1] == 10
    assert_array_almost_equal(get_data(cc)[..., 0], get_data(list_images[0]))

    # Now, a generator
    b = []
    g = nifti_generator(b)
    cc = concat_imgs(g)
    assert cc.shape[-1] == 10
    assert len(b) == 10


@pytest.mark.thread_unsafe
def test_copy_img():
    with pytest.raises(TypeError, match="must be of type"):
        copy_img(3)


@pytest.mark.thread_unsafe
def test_copy_img_side_effect(img_4d_ones_eye):
    # load data from proxy array to ndarray to make sure that
    # data is not changed
    _get_data(img_4d_ones_eye)
    hash1 = joblib.hash(img_4d_ones_eye)
    copy_img(img_4d_ones_eye)
    hash2 = joblib.hash(img_4d_ones_eye)
    assert hash1 == hash2


def test_check_same_fov(affine_eye):
    """Check check_same_fov error messages."""
    affine_b = affine_eye * 2

    shape_a = (2, 2, 2)
    shape_b = (3, 3, 3)

    shape_a_affine_a = Nifti1Image(np.empty(shape_a), affine_eye)
    shape_a_affine_a_2 = Nifti1Image(np.empty(shape_a), affine_eye)
    shape_a_affine_b = Nifti1Image(np.empty(shape_a), affine_b)
    shape_b_affine_a = Nifti1Image(np.empty(shape_b), affine_eye)
    shape_b_affine_b = Nifti1Image(np.empty(shape_b), affine_b)

    check_same_fov(a=shape_a_affine_a, b=shape_a_affine_a_2, raise_error=True)

    with pytest.raises(
        ValueError, match=r"[ac] and [ac] do not have the same affine"
    ):
        check_same_fov(
            a=shape_a_affine_a,
            b=shape_a_affine_a_2,
            c=shape_a_affine_b,
            raise_error=True,
        )
    with pytest.raises(
        ValueError, match=r"[ab] and [ab] do not have the same shape"
    ):
        check_same_fov(
            a=shape_a_affine_a, b=shape_b_affine_a, raise_error=True
        )
    with pytest.raises(
        ValueError, match=r"[ab] and [ab] do not have the same affine"
    ):
        check_same_fov(
            a=shape_b_affine_b, b=shape_a_affine_a, raise_error=True
        )

    with pytest.raises(
        ValueError, match=r"[ab] and [ab] do not have the same shape"
    ):
        check_same_fov(
            a=shape_b_affine_b, b=shape_a_affine_a, raise_error=True
        )


@pytest.mark.thread_unsafe
def test_check_niimg(img_3d_zeros_eye, img_4d_zeros_eye):
    """Check data dtype equal with dtype='auto'."""
    img_3d_check = check_niimg(img_3d_zeros_eye, dtype="auto")
    assert (
        get_data(img_3d_zeros_eye).dtype.kind
        == get_data(img_3d_check).dtype.kind
    )

    img_4d_check = check_niimg(img_4d_zeros_eye, dtype="auto")
    assert (
        get_data(img_4d_zeros_eye).dtype.kind
        == get_data(img_4d_check).dtype.kind
    )


@pytest.mark.thread_unsafe
def test_check_niimg_return_iterator_4d_input(
    img_3d_zeros_eye, img_4d_zeros_eye
):
    """Check behavior return_iterator on 4D-like image."""
    # return a 4D image
    img = check_niimg(img_4d_zeros_eye)
    assert isinstance(img, Nifti1Image)
    assert len(img.shape) == 4

    img = check_niimg([img_3d_zeros_eye, img_3d_zeros_eye])
    assert isinstance(img, Nifti1Image)
    assert len(img.shape) == 4

    # return a generator of 3D images
    img = check_niimg(img_4d_zeros_eye, return_iterator=True)
    assert isinstance(img, Iterable)
    assert len(next(img).shape) == 3

    img = check_niimg(
        [img_3d_zeros_eye, img_3d_zeros_eye], return_iterator=True
    )
    assert isinstance(img, Iterable)
    assert len(next(img).shape) == 3


@pytest.mark.thread_unsafe
def test_check_niimg_return_iterator_3d_input(img_3d_zeros_eye):
    """Check behavior return_iterator on 3D image."""
    # return a 3D image
    img = check_niimg(img_3d_zeros_eye)
    assert isinstance(img, Nifti1Image)
    assert len(img.shape) == 3

    # return a 4D image
    img = check_niimg(img_3d_zeros_eye, atleast_4d=True)
    assert isinstance(img, Nifti1Image)
    assert len(img.shape) == 4

    # return a generator of 3D images
    # warnings only if atleast_4d=False
    with warnings.catch_warnings(record=True) as w:
        img = check_niimg(
            img_3d_zeros_eye, atleast_4d=True, return_iterator=True
        )
        assert len(w) == 0
    assert isinstance(img, Iterable)
    assert len(next(img).shape) == 3


@pytest.mark.thread_unsafe
def test_check_niimg_return_iterator_true_3d_input(img_3d_zeros_eye):
    """Check behavior return_iterator=True on 3D image."""
    expected_error_msg = (
        "Input data has incompatible dimensionality: "
        "Expected dimension is 4D and you provided "
        "a 3D image."
    )
    with pytest.raises(
        DimensionError,
        match=expected_error_msg,
    ):
        check_niimg(img_3d_zeros_eye, return_iterator=True)


@pytest.mark.thread_unsafe
def test_check_niimg_errors(img_3d_zeros_eye, img_4d_zeros_eye):
    """Check check_niimg errors."""
    with pytest.raises(TypeError, match="input should be a NiftiLike object"):
        check_niimg(0)

    with pytest.raises(TypeError, match="empty object"):
        check_niimg([])

    img_3_3d = [[[img_3d_zeros_eye, img_3d_zeros_eye]]]
    img_2_4d = [[img_4d_zeros_eye, img_4d_zeros_eye]]

    with pytest.raises(
        DimensionError,
        match="Input data has incompatible dimensionality: "
        "Expected dimension is 2D and you provided "
        "a list of list of list of 3D images \\(6D\\)",
    ):
        check_niimg(img_3_3d, ensure_ndim=2)

    with pytest.raises(
        DimensionError,
        match="Input data has incompatible dimensionality: "
        "Expected dimension is 4D and you provided "
        "a list of list of 4D images \\(6D\\)",
    ):
        check_niimg(img_2_4d, ensure_ndim=4)


@pytest.mark.thread_unsafe
def test_check_niimg_wildcards_errors():
    """Check bad filename."""
    # Non existing file (with no magic) raise a ValueError exception
    nofile_path = "/tmp/nofile"
    file_not_found_msg = "File not found: '%s'"
    with pytest.raises(ValueError, match=file_not_found_msg % nofile_path):
        check_niimg(nofile_path)

    # Non matching wildcard raises a ValueError exception
    nofile_path_wildcards = "/tmp/no*file"
    with pytest.raises(
        ValueError, match="You may have left wildcards usage activated"
    ):
        check_niimg(nofile_path_wildcards)


@pytest.mark.thread_unsafe
@pytest.mark.parametrize("shape", [(10, 10, 10), (10, 10, 10, 3)])
@pytest.mark.parametrize(
    "wildcards", [True, False]
)  # (With globbing behavior or not)
def test_check_niimg_wildcards(affine_eye, shape, wildcards, tmp_path):
    """Test wildcards behavior."""
    img = Nifti1Image(np.zeros(shape), affine_eye)

    filename = write_imgs_to_path(img, file_path=tmp_path, create_files=True)
    assert_array_equal(
        get_data(check_niimg(filename, wildcards=wildcards)),
        get_data(img),
    )


@pytest.fixture
def img_in_home_folder(img_3d_mni):
    """Create a test file in the home folder.

    Teardown: use yield instead of return to make sure the file
    is deleted after the test,
    even if the test fails.
    https://docs.pytest.org/en/stable/how-to/fixtures.html#teardown-cleanup-aka-fixture-finalization
    """
    created_file = Path("~/test.nii")
    img_3d_mni.to_filename(created_file.expanduser())
    assert created_file.expanduser().exists()

    yield img_3d_mni

    created_file.expanduser().unlink()


@pytest.mark.thread_unsafe
@pytest.mark.parametrize(
    "filename", ["~/test.nii", r"~/test.nii", Path("~/test.nii")]
)
def test_check_niimg_user_expand(img_in_home_folder, filename):
    """Check that user path are expanded."""
    found_file = check_niimg(filename)

    assert_array_equal(
        get_data(found_file),
        get_data(img_in_home_folder),
    )


@pytest.mark.thread_unsafe
@pytest.mark.parametrize(
    "filename",
    [
        "~/*.nii",
        r"~/*.nii",
        ["~/test.nii"],
        [r"~/test.nii"],
        [Path("~/test.nii")],
    ],
)
def test_check_niimg_user_expand_4d(img_in_home_folder, filename):
    """Check that user path are expanded.

    Wildcards and lists should expected 4D data to be returned.
    """
    found_file = check_niimg(filename)

    assert_array_equal(
        get_data(found_file),
        get_data(check_niimg(img_in_home_folder, atleast_4d=True)),
    )


@pytest.mark.thread_unsafe
def test_check_niimg_wildcards_one_file_name(img_3d_zeros_eye, tmp_path):
    """Test globbing behavior."""
    file_not_found_msg = "File not found: '%s'"

    # Testing with a glob matching exactly one filename
    # Using a glob matching one file containing a 3d image returns a 4d image
    # with 1 as last dimension.
    globs = write_imgs_to_path(
        img_3d_zeros_eye,
        file_path=tmp_path,
        create_files=True,
        use_wildcards=True,
    )
    assert_array_equal(
        get_data(check_niimg(globs))[..., 0],
        get_data(img_3d_zeros_eye),
    )
    # Disabled globbing behavior should raise an ValueError exception
    with pytest.raises(
        ValueError, match=file_not_found_msg % re.escape(globs)
    ):
        check_niimg(globs, wildcards=False)

    # Testing with a glob matching multiple filenames
    img_4d = check_niimg_4d((img_3d_zeros_eye, img_3d_zeros_eye))
    globs = write_imgs_to_path(
        img_3d_zeros_eye,
        img_3d_zeros_eye,
        file_path=tmp_path,
        create_files=True,
        use_wildcards=True,
    )
    assert_array_equal(get_data(check_niimg(globs)), get_data(img_4d))


@pytest.fixture
def set_expand_path_wildcards():
    """Toggles EXPAND_PATH_WILDCARDS before and after a test."""
    # Test when global variable is set to False => no globbing allowed
    ni.EXPAND_PATH_WILDCARDS = False
    yield
    # Reverting to default behavior
    ni.EXPAND_PATH_WILDCARDS = True


@pytest.mark.thread_unsafe
def test_check_niimg_no_expand_wildcards_errors(
    set_expand_path_wildcards,  # noqa: ARG001
):
    """Test errors when wildcards are not expanded if requested."""
    nofile_path = "/tmp/nofile"

    file_not_found_msg = "File not found: '%s'"

    # Non existing filename (/tmp/nofile) could match an existing one through
    # globbing but global wildcards variable overrides this feature => raises
    # a ValueError
    with pytest.raises(ValueError, match=file_not_found_msg % nofile_path):
        check_niimg(nofile_path)

    # Verify wildcards function parameter has no effect
    with pytest.raises(ValueError, match=file_not_found_msg % nofile_path):
        check_niimg(nofile_path, wildcards=False)


@pytest.mark.thread_unsafe
def test_check_niimg_no_expand_wildcards(
    img_3d_zeros_eye,
    img_4d_zeros_eye,
    tmp_path,
    set_expand_path_wildcards,  # noqa: ARG001
):
    """Test wildcards are not expanded if requested."""
    # Testing with an exact filename matching (3d case)
    filename = write_imgs_to_path(
        img_3d_zeros_eye, file_path=tmp_path, create_files=True
    )
    assert_array_equal(
        get_data(check_niimg(filename)), get_data(img_3d_zeros_eye)
    )

    # Testing with an exact filename matching (4d case)
    filename = write_imgs_to_path(
        img_4d_zeros_eye, file_path=tmp_path, create_files=True
    )
    assert_array_equal(
        get_data(check_niimg(filename)), get_data(img_4d_zeros_eye)
    )


def test_check_niimg_3d_error(img_3d_zeros_eye):
    """Test dimensionality error."""
    with pytest.raises(
        DimensionError,
        match="Input data has incompatible dimensionality",
    ):
        check_niimg_3d([img_3d_zeros_eye, img_3d_zeros_eye])


def test_check_niimg_3d_filename(affine_eye, tmp_path):
    """Check that a filename does not raise an error."""
    data = np.zeros((40, 40, 40, 1))
    data[20, 20, 20] = 1
    data_img = Nifti1Image(data, affine_eye)

    filename = write_imgs_to_path(
        data_img, file_path=tmp_path, create_files=True
    )
    check_niimg_3d(filename)


def test_check_niimg_3d_datatype(img_3d_zeros_eye):
    """Check data dtype equal with dtype='auto'."""
    img_check = check_niimg_3d(img_3d_zeros_eye, dtype="auto")
    assert (
        get_data(img_3d_zeros_eye).dtype.kind == get_data(img_check).dtype.kind
    )


@pytest.mark.thread_unsafe
def test_check_niimg_3d_pathlike(img_3d_zeros_eye, tmp_path):
    """Test check_niimg_3d with file."""
    filename = write_imgs_to_path(
        img_3d_zeros_eye, file_path=tmp_path, create_files=True
    )
    filename = Path(filename)
    check_niimg_3d(filename)


def test_check_niimg_4d_errors(affine_eye, img_3d_zeros_eye, shape_3d_default):
    """Test check_niimg_4d errors."""
    with pytest.raises(TypeError, match="input should be a NiftiLike object"):
        check_niimg_4d(0)

    with pytest.raises(TypeError, match="empty object"):
        check_niimg_4d([])

    # This should raise an error: a 3D img is given and we want a 4D
    with pytest.raises(
        DimensionError,
        match=(
            r"Input data has incompatible dimensionality: "
            r"Expected dimension is 4D and you provided a 3D image."
        ),
    ):
        check_niimg_4d(img_3d_zeros_eye)

    a = img_3d_zeros_eye
    b = np.zeros(shape_3d_default)
    with pytest.raises(TypeError, match="input should be a NiftiLike object"):
        c = check_niimg_4d([a, b], return_iterator=True)

    b = Nifti1Image(np.zeros((10, 20, 10)), affine_eye)
    c = check_niimg_4d([a, b], return_iterator=True)
    with pytest.raises(
        ValueError,
        match="Field of view of image #1 is different from reference FOV",
    ):
        list(c)


def test_check_niimg_4d(affine_eye, img_3d_zeros_eye, shape_3d_default):
    """Check check_niimg_4d basic behavior."""
    img_4d_1 = check_niimg_4d([img_3d_zeros_eye, img_3d_zeros_eye])

    assert get_data(img_4d_1).shape == (*shape_3d_default, 2)
    assert_array_equal(img_4d_1.affine, affine_eye)

    # check idempotence
    img_4d_2 = check_niimg_4d(img_4d_1)

    assert_array_equal(get_data(img_4d_2), get_data(img_4d_1))
    assert_array_equal(img_4d_2.affine, img_4d_1.affine)


def test_check_niimg_4d_return_iterator(img_3d_zeros_eye, shape_3d_default):
    """Check check_niimg_4d with return iterator."""
    img_4d_1 = check_niimg_4d([img_3d_zeros_eye, img_3d_zeros_eye])
    img_3d_iterator = check_niimg_4d(
        [img_3d_zeros_eye, img_3d_zeros_eye], return_iterator=True
    )
    img_3d_iterator_length = sum(1 for _ in img_3d_iterator)

    assert img_3d_iterator_length == 2

    img_3d_iterator_1 = check_niimg_4d(
        [img_3d_zeros_eye, img_3d_zeros_eye], return_iterator=True
    )
    img_3d_iterator_2 = check_niimg_4d(img_3d_iterator_1, return_iterator=True)

    for img_1, img_2 in zip(
        img_3d_iterator_1, img_3d_iterator_2, strict=False
    ):
        assert get_data(img_1).shape == shape_3d_default
        assert_array_equal(get_data(img_1), get_data(img_2))
        assert_array_equal(img_1.affine, img_2.affine)

    img_3d_iterator_1 = check_niimg_4d(
        [img_3d_zeros_eye, img_3d_zeros_eye], return_iterator=True
    )
    img_3d_iterator_2 = check_niimg_4d(img_4d_1, return_iterator=True)

    for img_1, img_2 in zip(
        img_3d_iterator_1, img_3d_iterator_2, strict=False
    ):
        assert get_data(img_1).shape == shape_3d_default
        assert_array_equal(get_data(img_1), get_data(img_2))
        assert_array_equal(img_1.affine, img_2.affine)


class PhonyNiimage(spatialimages.SpatialImage):
    """Dummy image."""

    def __init__(self):
        self.data = np.ones((9, 9, 9, 9))
        self.my_affine = np.ones((4, 4))

    def get_data(self):
        """Return data."""
        return self.data

    def get_affine(self):
        """Return affine."""
        return self.my_affine

    @property
    def shape(self):
        """Return shape."""
        return self.data.shape

    @property
    def _data_cache(self):
        return self.data

    @property
    def _dataobj(self):
        return self.data


def test_check_niimg_4d_phony_image():
    """Test a Niimg-like object that does not hold a shape attribute."""
    phony_img = PhonyNiimage()
    check_niimg_4d(phony_img)


@pytest.mark.thread_unsafe
def test_check_niimg_4d_wildcards_one_file_name(img_3d_zeros_eye, tmp_path):
    """Testing with a glob matching multiple filenames."""
    img_4d = check_niimg_4d((img_3d_zeros_eye, img_3d_zeros_eye))
    globs = write_imgs_to_path(
        img_3d_zeros_eye,
        img_3d_zeros_eye,
        file_path=tmp_path,
        create_files=True,
        use_wildcards=True,
    )
    assert_array_equal(get_data(check_niimg(globs)), get_data(img_4d))


def test_iter_check_niimgs_error():
    """Test error iter_check_niimg with missing files."""
    no_file_matching = "No files matching path: %s"

    for empty in ((), [], iter(())):
        with pytest.raises(ValueError, match=r"Input niimgs list is empty."):
            list(iter_check_niimg(empty))

    nofile_path = "/tmp/nofile"
    with pytest.raises(ValueError, match=no_file_matching % nofile_path):
        list(iter_check_niimg(nofile_path))


@pytest.mark.thread_unsafe
def test_iter_check_niimgs(tmp_path, img_4d_zeros_eye):
    """Test iter_check_niimg on file on disk and regular 5D image."""
    # Create a test file
    filename = tmp_path / "nilearn_test.nii"
    img_4d_zeros_eye.to_filename(filename)
    niimgs = list(iter_check_niimg([filename]))
    assert_array_equal(
        get_data(niimgs[0]), get_data(check_niimg(img_4d_zeros_eye))
    )
    del niimgs

    # Regular case
    img_5d = [[img_4d_zeros_eye, img_4d_zeros_eye]]
    niimgs = list(iter_check_niimg(img_5d))
    assert_array_equal(get_data(niimgs[0]), get_data(check_niimg(img_5d)))


def _check_memory(list_img_3d):
    # We intentionally add an offset of memory usage to avoid non trustable
    # measures with memory_profiler.
    mem_offset = b"a" * 100 * 1024**2
    list(iter_check_niimg(list_img_3d))
    return mem_offset


@with_memory_profiler
def test_iter_check_niimgs_memory(affine_eye):
    """Verify that iterating over a list of images \
       doesn't consume extra memory.
    """
    assert_memory_less_than(
        100,
        0.1,
        _check_memory,
        [Nifti1Image(np.ones((100, 100, 200)), affine_eye) for _ in range(10)],
    )
