"""Test the second level model."""

from pathlib import Path

import numpy as np
import pandas as pd
import pytest
from nibabel import Nifti1Image, load
from numpy.testing import (
    assert_almost_equal,
    assert_array_almost_equal,
    assert_array_equal,
)
from scipy import stats
from sklearn.utils.estimator_checks import parametrize_with_checks

from nilearn._utils import testing
from nilearn._utils.data_gen import (
    generate_fake_fmri_data_and_design,
    write_fake_bold_img,
    write_fake_fmri_data_and_design,
)
from nilearn._utils.estimator_checks import (
    check_estimator,
    nilearn_check_estimator,
    return_expected_failed_checks,
)
from nilearn._utils.versions import SKLEARN_LT_1_6
from nilearn.conftest import _shape_3d_default
from nilearn.exceptions import NotImplementedWarning
from nilearn.glm.first_level import FirstLevelModel, run_glm
from nilearn.glm.second_level import SecondLevelModel, non_parametric_inference
from nilearn.glm.second_level.second_level import (
    _check_confounds,
    _check_first_level_contrast,
    _check_input_as_first_level_model,
    _check_n_rows_desmat_vs_n_effect_maps,
    _check_second_level_input,
    _infer_effect_maps,
    _process_second_level_input_as_dataframe,
    _process_second_level_input_as_firstlevelmodels,
    _sort_input_dataframe,
)
from nilearn.image import concat_imgs, get_data, new_img_like, smooth_img
from nilearn.maskers import NiftiMasker, SurfaceMasker
from nilearn.reporting import get_clusters_table
from nilearn.surface import SurfaceImage
from nilearn.surface.utils import assert_surface_image_equal

ESTIMATORS_TO_CHECK = [SecondLevelModel()]

if SKLEARN_LT_1_6:

    @pytest.mark.parametrize(
        "estimator, check, name",
        check_estimator(estimators=ESTIMATORS_TO_CHECK),
    )
    def test_check_estimator_sklearn_valid(estimator, check, name):  # noqa: ARG001
        """Check compliance with sklearn estimators."""
        check(estimator)

    @pytest.mark.xfail(reason="invalid checks should fail")
    @pytest.mark.parametrize(
        "estimator, check, name",
        check_estimator(estimators=ESTIMATORS_TO_CHECK, valid=False),
    )
    def test_check_estimator_sklearn_invalid(estimator, check, name):  # noqa: ARG001
        """Check compliance with sklearn estimators."""
        check(estimator)

else:

    @parametrize_with_checks(
        estimators=ESTIMATORS_TO_CHECK,
        expected_failed_checks=return_expected_failed_checks,
    )
    def test_check_estimator_sklearn(estimator, check):
        """Check compliance with sklearn estimators."""
        check(estimator)


@pytest.mark.slow
@pytest.mark.parametrize(
    "estimator, check, name",
    nilearn_check_estimator(estimators=ESTIMATORS_TO_CHECK),
)
def test_check_estimator_nilearn(estimator, check, name):  # noqa: ARG001
    """Check compliance with nilearn estimators rules."""
    check(estimator)


N_PERM = 5
SHAPE = (*_shape_3d_default(), 1)


@pytest.fixture()
def n_subjects():
    return 3


@pytest.fixture
def input_df():
    """Input DataFrame for testing."""
    return pd.DataFrame(
        {
            "effects_map_path": ["foo.nii", "bar.nii", "baz.nii"],
            "subject_label": ["foo", "bar", "baz"],
        }
    )


def fake_fmri_data(shape=SHAPE):
    shapes = (shape,)
    mask, fmri_data, _ = generate_fake_fmri_data_and_design(shapes)
    return fmri_data[0], mask


@pytest.mark.slow
def test_non_parametric_inference_with_flm_objects(shape_3d_default):
    """See https://github.com/nilearn/nilearn/issues/3579 ."""
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[(*shape_3d_default, 15)]
    )

    masker = NiftiMasker(mask)
    masker.fit()
    single_run_model = FirstLevelModel(mask_img=masker).fit(
        fmri_data[0], design_matrices=design_matrices[0]
    )
    single_run_model.compute_contrast("x")

    second_level_input = [single_run_model, single_run_model]

    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    non_parametric_inference(
        second_level_input=second_level_input,
        design_matrix=design_matrix,
        first_level_contrast="x",
        n_perm=N_PERM,
    )


@pytest.mark.thread_unsafe
def test_process_second_level_input_as_dataframe(input_df):
    """Unit tests for function _process_second_level_input_as_dataframe()."""
    sample_map, subjects_label = _process_second_level_input_as_dataframe(
        input_df
    )
    assert sample_map == "foo.nii"
    assert subjects_label == ["foo", "bar", "baz"]


def test_sort_input_dataframe(input_df):
    """Unit tests for function _sort_input_dataframe()."""
    output_df = _sort_input_dataframe(input_df)

    assert output_df["subject_label"].to_list() == [
        "bar",
        "baz",
        "foo",
    ]
    assert output_df["effects_map_path"].to_list() == [
        "bar.nii",
        "baz.nii",
        "foo.nii",
    ]


def test_second_level_input_as_3d_images(
    rng, affine_eye, tmp_path, shape_3d_default, n_subjects
):
    """Test second level model with a list 3D image filenames as input.

    Should act as a regression test for:
    https://github.com/nilearn/nilearn/issues/3636

    """
    images = []
    for _ in range(n_subjects):
        data = rng.random(shape_3d_default)
        images.append(Nifti1Image(data, affine_eye))

    filenames = testing.write_imgs_to_path(
        *images, file_path=tmp_path, create_files=True
    )
    second_level_input = filenames
    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    second_level_model = SecondLevelModel(smoothing_fwhm=8.0)
    second_level_model = second_level_model.fit(
        second_level_input,
        design_matrix=design_matrix,
    )


def test_slm_verbose(rng, affine_eye, shape_3d_default, n_subjects, capsys):
    """Check verbosity levels.

    Standard output content should be larger
    when we go from verbosity 1 to verbosity 3.
    """
    images = []
    for _ in range(n_subjects):
        data = rng.random(shape_3d_default)
        images.append(Nifti1Image(data, affine_eye))

    second_level_input = images
    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    SecondLevelModel(verbose=0).fit(
        second_level_input,
        design_matrix=design_matrix,
    )
    stdout_verbose_0 = capsys.readouterr().out
    assert stdout_verbose_0 == ""

    SecondLevelModel(verbose=1).fit(
        second_level_input,
        design_matrix=design_matrix,
    )
    stdout_verbose_1 = capsys.readouterr().out
    assert "Computation of second level model done in" in stdout_verbose_1

    SecondLevelModel(verbose=2).fit(
        second_level_input,
        design_matrix=design_matrix,
    )
    stdout_verbose_2 = capsys.readouterr().out

    assert len(stdout_verbose_1) > 0
    assert len(stdout_verbose_2) > len(stdout_verbose_1)


@pytest.mark.slow
def test_process_second_level_input_as_firstlevelmodels(
    shape_4d_default, n_subjects
):
    """Unit tests for function \
       _process_second_level_input_as_firstlevelmodels().
    """
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[shape_4d_default]
    )
    list_of_flm = [
        FirstLevelModel(mask_img=mask, subject_label=f"sub-{i}").fit(
            fmri_data[0], design_matrices=design_matrices[0]
        )
        for i in range(n_subjects)
    ]
    (
        sample_map,
        subjects_label,
    ) = _process_second_level_input_as_firstlevelmodels(list_of_flm)

    assert subjects_label == [f"sub-{i}" for i in range(n_subjects)]
    assert isinstance(sample_map, Nifti1Image)
    assert sample_map.shape == shape_4d_default[:3]


@pytest.mark.slow
def test_check_affine_first_level_models(
    affine_eye, shape_4d_default, n_subjects
):
    """Check all FirstLevelModel have the same affine."""
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[shape_4d_default]
    )
    list_of_flm = [
        FirstLevelModel(mask_img=mask, subject_label=f"sub-{i}").fit(
            fmri_data[0], design_matrices=design_matrices[0]
        )
        for i in range(n_subjects)
    ]
    # should pass
    _check_input_as_first_level_model(
        second_level_input=list_of_flm, none_confounds=False
    )

    # add a model with a different affine
    # should raise an error
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[shape_4d_default], affine=affine_eye * 2
    )
    list_of_flm.append(
        FirstLevelModel(mask_img=mask, subject_label="sub-4").fit(
            fmri_data[0], design_matrices=design_matrices[0]
        )
    )

    with pytest.raises(
        ValueError, match="All first level models must have the same affine"
    ):
        _check_input_as_first_level_model(
            second_level_input=list_of_flm, none_confounds=False
        )


@pytest.mark.slow
def test_check_shape_first_level_models(shape_4d_default, n_subjects):
    """Check all FirstLevelModel have the same shape."""
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[shape_4d_default]
    )
    list_of_flm = [
        FirstLevelModel(mask_img=mask, subject_label=f"sub-{i}").fit(
            fmri_data[0], design_matrices=design_matrices[0]
        )
        for i in range(n_subjects)
    ]
    # should pass
    _check_input_as_first_level_model(
        second_level_input=list_of_flm, none_confounds=False
    )

    # add a model with a different shape
    # should raise an error
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[(8, 9, 10, 15)]
    )
    list_of_flm.append(
        FirstLevelModel(mask_img=mask, subject_label="sub-4").fit(
            fmri_data[0], design_matrices=design_matrices[0]
        )
    )

    with pytest.raises(
        ValueError, match="All first level models must have the same shape"
    ):
        _check_input_as_first_level_model(
            second_level_input=list_of_flm, none_confounds=False
        )


def test_check_second_level_input(shape_4d_default):
    """Raise errors when wrong inputs are passed to SecondLevelModel."""
    with pytest.raises(TypeError, match="second_level_input must be"):
        _check_second_level_input(1, None)

    with pytest.raises(
        TypeError,
        match="A second level model requires a list with at "
        "least two first level models or niimgs",
    ):
        _check_second_level_input([FirstLevelModel()], pd.DataFrame())

    with pytest.raises(
        TypeError, match="Got object type <class 'int'> at idx 1"
    ):
        _check_second_level_input(["foo", 1], pd.DataFrame())

    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[shape_4d_default]
    )

    input_models = [
        FirstLevelModel(mask_img=mask).fit(
            fmri_data[0], design_matrices=design_matrices[0]
        )
    ]

    obj = lambda: None  # noqa: E731
    obj.results_ = "foo"
    obj.labels_ = "bar"

    with pytest.raises(
        TypeError, match="Got object type <class 'function'> at idx 1"
    ):
        _check_second_level_input([*input_models, obj], pd.DataFrame())


def test_check_second_level_input_list_wrong_type():
    """Raise errors when wrong inputs are passed to SecondLevelModel.

    Integration test: slightly higher level test than those for
    _check_second_level_input.
    """
    model = SecondLevelModel()
    second_level_input = [1, 2]
    with pytest.raises(TypeError, match="second_level_input must be"):
        model.fit(second_level_input)


def test_check_second_level_input_unfit_model():
    with pytest.raises(
        ValueError, match="Model sub_1 at index 0 has not been fit yet"
    ):
        _check_second_level_input(
            [FirstLevelModel(subject_label=f"sub_{i}") for i in range(1, 3)],
            pd.DataFrame(),
        )


def test_check_second_level_input_dataframe():
    with pytest.raises(
        ValueError,
        match="'second_level_input' DataFrame must have columns "
        "'subject_label', 'map_name' and 'effects_map_path'",
    ):
        _check_second_level_input(
            pd.DataFrame(columns=["foo", "bar"]), pd.DataFrame()
        )

    with pytest.raises(
        ValueError, match="'subject_label' column must contain only strings"
    ):
        _check_second_level_input(
            pd.DataFrame(
                {
                    "subject_label": [1, 2],
                    "map_name": ["a", "b"],
                    "effects_map_path": ["c", "d"],
                }
            ),
            pd.DataFrame(),
        )


def test_check_second_level_input_confounds(shape_4d_default):
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        shapes=[shape_4d_default]
    )

    input_models = [
        FirstLevelModel(mask_img=mask).fit(
            fmri_data[0], design_matrices=design_matrices[0]
        )
    ]

    with pytest.raises(
        ValueError,
        match="In case confounds are provided, first level "
        "objects need to provide the attribute 'subject_label'",
    ):
        _check_second_level_input(
            input_models * 2, pd.DataFrame(), confounds=pd.DataFrame()
        )


def test_check_second_level_input_design_matrix(shape_4d_default):
    """Raise errors when no design matrix is passed to SecondLevelModel.

    When passing niimg like objects.
    """
    _, fmri_data, _ = generate_fake_fmri_data_and_design(
        shapes=[shape_4d_default]
    )

    _check_second_level_input(fmri_data[0], pd.DataFrame())

    with pytest.raises(
        ValueError,
        match="List of niimgs as second_level_input "
        "require a design matrix to be provided",
    ):
        _check_second_level_input(fmri_data * 2, None)
    with pytest.raises(
        ValueError,
        match="List of niimgs as second_level_input "
        "require a design matrix to be provided",
    ):
        _check_second_level_input(fmri_data[0], None)


def test_check_confounds():
    _check_confounds(None)  # Should not do anything
    with pytest.raises(
        TypeError, match="confounds must be a pandas DataFrame"
    ):
        _check_confounds("foo")
    with pytest.raises(
        ValueError, match="confounds DataFrame must contain column"
    ):
        _check_confounds(pd.DataFrame())
    with pytest.raises(
        ValueError, match="confounds should contain at least 2 columns"
    ):
        _check_confounds(pd.DataFrame(columns=["subject_label"]))
    with pytest.raises(
        ValueError, match="subject_label column must contain only strings"
    ):
        _check_confounds(
            pd.DataFrame(
                {"subject_label": [None, None, None], "conf": [4, 5, 6]}
            )
        )


def test_check_first_level_contrast():
    _check_first_level_contrast(["foo"], None)  # Should not do anything
    _check_first_level_contrast([FirstLevelModel()], "foo")
    with pytest.raises(ValueError, match="If second_level_input was a list"):
        _check_first_level_contrast([FirstLevelModel()], None)


def test_check_n_rows_desmat_vs_n_effect_maps():
    _check_n_rows_desmat_vs_n_effect_maps(
        [1, 2, 3], np.array([[1, 2], [3, 4], [5, 6]])
    )
    with pytest.raises(
        ValueError,
        match="design_matrix does not match the number of maps considered",
    ):
        _check_n_rows_desmat_vs_n_effect_maps(
            [1, 2], np.array([[1, 2], [3, 4], [5, 6]])
        )


@pytest.mark.slow
def test_infer_effect_maps(tmp_path, shape_4d_default):
    """Check that the right input is inferred.

    second_level_input could for example
    be a list of images
    or a dataframe 'mapping' a string to an image.
    """
    rk = 3
    shapes = [SHAPE, shape_4d_default]
    mask_file, fmri_files, design_files = write_fake_fmri_data_and_design(
        shapes, rk=rk, file_path=tmp_path
    )
    second_level_input = pd.DataFrame(
        {"map_name": ["a", "b"], "effects_map_path": [fmri_files[0], "bar"]}
    )

    assert _infer_effect_maps(second_level_input, "a") == [fmri_files[0]]
    assert _infer_effect_maps([fmri_files[0]], None) == [fmri_files[0]]

    contrast = np.eye(rk)[1]
    second_level_input = [FirstLevelModel(mask_img=mask_file)] * 2
    for i, model in enumerate(second_level_input):
        model.fit(fmri_files[i], design_matrices=design_files[i])

    assert len(_infer_effect_maps(second_level_input, contrast)) == 2


def test_infer_effect_maps_error(tmp_path, shape_3d_default):
    """Check error raised when inferring 'type' for the images.

    For example if the image mapped in a dataframe does not exist.
    """
    shapes = [(*shape_3d_default, 5), (*shape_3d_default, 6)]
    _, fmri_files, _ = write_fake_fmri_data_and_design(
        shapes, file_path=tmp_path
    )
    second_level_input = pd.DataFrame(
        {"map_name": ["a", "b"], "effects_map_path": [fmri_files[0], "bar"]}
    )
    with pytest.raises(ValueError, match="File not found: 'bar'"):
        _infer_effect_maps(second_level_input, "b")


def test_mask_img_volume(n_subjects):
    """Check mask_img_ with volume data."""
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    assert isinstance(model._mask_img, Nifti1Image)


@pytest.mark.slow
def test_affine_output_mask(n_subjects):
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    c1 = np.eye(len(model.design_matrix_.columns))[0]
    z_image = model.compute_contrast(c1, output_type="z_score")

    assert_array_equal(z_image.affine, mask.affine)


@pytest.mark.slow
def test_affine_shape_output_when_provided(affine_eye, n_subjects):
    """Check fov output corresponds to the one passed to model."""
    func_img, mask = fake_fmri_data()

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    target_shape = (10, 10, 10)
    target_affine = affine_eye
    target_affine[0, 3] = 1

    model = SecondLevelModel(
        mask_img=mask,
        target_shape=target_shape,
        target_affine=target_affine,
    )
    model = model.fit(Y, design_matrix=X)

    c1 = np.eye(len(model.design_matrix_.columns))[0]
    z_image = model.fit(Y, design_matrix=X).compute_contrast(c1)

    assert_array_equal(z_image.shape, target_shape)
    assert_array_equal(z_image.affine, target_affine)


def test_slm_4d_image(img_4d_mni):
    """Compute contrast with 4D images as input.

    See https://github.com/nilearn/nilearn/issues/3058
    """
    model = SecondLevelModel()
    Y = img_4d_mni
    X = pd.DataFrame([[1]] * img_4d_mni.shape[3], columns=["intercept"])
    model = model.fit(Y, design_matrix=X)
    c1 = np.eye(len(model.design_matrix_.columns))[0]
    model.compute_contrast(c1, output_type="z_score")


def test_warning_overriding_with_masker_parameter(n_subjects):
    func_img, mask = fake_fmri_data()

    # fit model
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    # Provide a masker as mask_img
    masker = NiftiMasker(mask).fit()
    with pytest.warns(
        UserWarning,
        match=(
            "Overriding provided-default estimator parameters "
            "with provided masker parameters"
        ),
    ):
        SecondLevelModel(mask_img=masker, verbose=1).fit(Y, design_matrix=X)


@pytest.mark.slow
def test_high_level_non_parametric_inference_with_paths(tmp_path, n_subjects):
    mask_file, fmri_files, _ = write_fake_fmri_data_and_design(
        (SHAPE,), file_path=tmp_path
    )
    fmri_files = fmri_files[0]
    df_input = pd.DataFrame(
        {
            "subject_label": [f"sub-{i}" for i in range(n_subjects)],
            "effects_map_path": [fmri_files] * n_subjects,
            "map_name": [fmri_files] * n_subjects,
        }
    )
    func_img = load(fmri_files)
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    c1 = np.eye(len(X.columns))[0]
    neg_log_pvals_imgs = [
        non_parametric_inference(
            second_level_input,
            design_matrix=X,
            second_level_contrast=c1,
            first_level_contrast=fmri_files,
            mask=mask_file,
            n_perm=N_PERM,
            verbose=1,
        )
        for second_level_input in [Y, df_input]
    ]

    assert all(isinstance(img, Nifti1Image) for img in neg_log_pvals_imgs)
    for img in neg_log_pvals_imgs:
        assert_array_equal(img.affine, load(mask_file).affine)

    neg_log_pvals_list = [get_data(i) for i in neg_log_pvals_imgs]
    for neg_log_pvals in neg_log_pvals_list:
        assert np.all(neg_log_pvals <= -np.log10(1.0 / (N_PERM + 1)))
        assert np.all(neg_log_pvals >= 0)


def test_high_level_non_parametric_inference_with_paths_warning(n_subjects):
    func_img, mask = fake_fmri_data()
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    c1 = np.eye(len(X.columns))[0]

    masker = NiftiMasker(mask, smoothing_fwhm=2.0)
    with pytest.warns(
        UserWarning,
        match="Parameter 'smoothing_fwhm' of the masker overridden",
    ):
        non_parametric_inference(
            Y,
            design_matrix=X,
            second_level_contrast=c1,
            smoothing_fwhm=3.0,
            mask=masker,
            n_perm=N_PERM,
        )


def _confounds():
    return pd.DataFrame(
        [["01", 1], ["02", 2], ["03", 3]],
        columns=["subject_label", "conf1"],
    )


@pytest.fixture
def confounds():
    return _confounds()


@pytest.mark.slow
@pytest.mark.parametrize("confounds", [None, _confounds()])
def test_fmri_inputs_flms(rng, confounds, shape_4d_default):
    """Test second level model with first level model as inputs."""
    # prepare fake data
    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        [shape_4d_default], rk=1
    )

    # prepare correct input first level models
    flm = FirstLevelModel(subject_label="01").fit(
        fmri_data, design_matrices=design_matrices
    )

    # prepare correct input dataframe and lists
    p, q = 80, 10
    X = rng.standard_normal(size=(p, q))
    design_matrix = pd.DataFrame(X[:3, :3], columns=["intercept", "b", "c"])

    # smoke tests with correct input
    flms = [flm, flm, flm]

    # First level models as input
    SecondLevelModel(mask_img=mask).fit(flms)
    SecondLevelModel().fit(flms)

    # Note : the following one creates a singular design matrix
    SecondLevelModel().fit(flms, confounds)
    SecondLevelModel().fit(flms, confounds, design_matrix)


@pytest.mark.parametrize("confounds", [None, _confounds()])
def test_fmri_inputs_images(rng, shape_3d_default, confounds):
    """Test second level model with image as inputs."""
    # prepare correct input dataframe and lists
    p, q = 80, 10
    X = rng.standard_normal(size=(p, q))
    design_matrix = pd.DataFrame(X[:3, :3], columns=["intercept", "b", "c"])

    shape_3d = [(*shape_3d_default, 1)]
    _, fmri_data, _ = generate_fake_fmri_data_and_design(shape_3d)
    fmri_data = fmri_data[0]

    # niimgs as input
    niimgs = [fmri_data, fmri_data, fmri_data]
    SecondLevelModel().fit(niimgs, confounds, design_matrix)

    # 4d niimg as input
    niimg_4d = concat_imgs(niimgs)
    SecondLevelModel().fit(niimg_4d, confounds, design_matrix)


@pytest.mark.parametrize("confounds", [None, _confounds()])
def test_fmri_inputs_dataframes_as_input(tmp_path, rng, confounds):
    """Test second level model with dataframe as inputs."""
    # prepare fake data
    p, q = 80, 10
    X = rng.standard_normal(size=(p, q))

    # prepare correct input dataframe and lists
    _, fmri_files, _ = write_fake_fmri_data_and_design(
        (SHAPE,), file_path=tmp_path
    )
    fmri_files = fmri_files[0]

    design_matrix = pd.DataFrame(X[:3, :3], columns=["intercept", "b", "c"])

    # dataframes as input
    dfcols = ["subject_label", "map_name", "effects_map_path"]
    dfrows = [
        ["01", "a", fmri_files],
        ["02", "a", fmri_files],
        ["03", "a", fmri_files],
    ]
    niidf = pd.DataFrame(dfrows, columns=dfcols)

    SecondLevelModel().fit(niidf, confounds)
    SecondLevelModel().fit(niidf, confounds, design_matrix)


def test_fmri_pandas_series_as_input(tmp_path, rng):
    """Use pandas series of file paths as inputs."""
    # prepare correct input dataframe and lists
    p, q = 80, 10
    X = rng.standard_normal(size=(p, q))
    _, fmri_files, _ = write_fake_fmri_data_and_design(
        (SHAPE,), file_path=tmp_path
    )
    fmri_files = fmri_files[0]

    # dataframes as input
    design_matrix = pd.DataFrame(X[:3, :3], columns=["intercept", "b", "c"])
    niidf = pd.DataFrame({"filepaths": [fmri_files, fmri_files, fmri_files]})
    SecondLevelModel().fit(
        second_level_input=niidf["filepaths"],
        confounds=None,
        design_matrix=design_matrix,
    )


def test_fmri_inputs_pandas_errors():
    """Test wrong second level inputs."""
    # test wrong input for list and pandas requirements
    nii_img = ["01", "02", "03"]
    with pytest.raises(ValueError, match="File not found: "):
        SecondLevelModel().fit(nii_img)

    nii_series = pd.Series(nii_img)
    with pytest.raises(ValueError, match="File not found: "):
        SecondLevelModel().fit(nii_series)

    # test dataframe requirements
    dfcols = [
        "not_the_right_column_name",
    ]
    dfrows = [["01"], ["02"], ["03"]]
    niidf = pd.DataFrame(dfrows, columns=dfcols)
    with pytest.raises(
        ValueError,
        match=(
            r"'second_level_input' DataFrame must have "
            r"columns 'subject_label', 'map_name' and 'effects_map_path'."
        ),
    ):
        SecondLevelModel().fit(niidf)


def test_secondlevelmodel_fit_inputs_errors(confounds, shape_4d_default):
    """Raise the proper errors when invalid inputs are passed to fit."""
    # prepare fake data
    shapes = (shape_4d_default,)
    _, fmri_data, _ = generate_fake_fmri_data_and_design(shapes)
    fmri_data = fmri_data[0]
    n_samples = fmri_data.shape[-1]
    design_matrices = pd.DataFrame(np.ones((n_samples, 1)), columns=["a"])

    # prepare correct input first level models
    flm = FirstLevelModel(subject_label="01").fit(
        fmri_data, design_matrices=design_matrices
    )

    # test first level model requirements
    with pytest.raises(TypeError, match="second_level_input must be"):
        SecondLevelModel().fit(second_level_input=flm)
    with pytest.raises(TypeError, match="at least two"):
        SecondLevelModel().fit(second_level_input=[flm])

    # test first_level_conditions, confounds, and design
    flms = [flm, flm, flm]
    with pytest.raises(
        TypeError, match="confounds must be a pandas DataFrame"
    ):
        SecondLevelModel().fit(second_level_input=flms, confounds=["", []])
    with pytest.raises(
        TypeError, match="confounds must be a pandas DataFrame"
    ):
        SecondLevelModel().fit(second_level_input=flms, confounds=[])
    with pytest.raises(
        TypeError, match="confounds must be a pandas DataFrame"
    ):
        SecondLevelModel().fit(
            second_level_input=flms, confounds=confounds["conf1"]
        )


@pytest.mark.slow
@pytest.mark.parametrize(
    "filename, sep", [("design.csv", ","), ("design.tsv", "\t")]
)
def test_secondlevelmodel_design_matrix_path(
    img_3d_mni, tmp_path, filename, sep
):
    second_level_input = [img_3d_mni, img_3d_mni]
    design_matrix = pd.DataFrame(
        np.ones((len(second_level_input), 1)), columns=["a"]
    )

    SecondLevelModel().fit(
        second_level_input=second_level_input, design_matrix=design_matrix
    )

    design_matrix_fname = tmp_path / filename
    design_matrix.to_csv(design_matrix_fname, sep=sep)

    SecondLevelModel().fit(
        second_level_input=second_level_input,
        design_matrix=design_matrix_fname,
    )
    SecondLevelModel().fit(
        second_level_input=second_level_input,
        design_matrix=str(design_matrix_fname),
    )


@pytest.mark.parametrize("design_matrix", ["foo", Path("foo")])
def test_secondlevelmodel_design_matrix_error_path(img_3d_mni, design_matrix):
    second_level_input = [img_3d_mni, img_3d_mni, img_3d_mni]
    with pytest.raises(
        ValueError, match=r"Tables to load can only be TSV or CSV."
    ):
        SecondLevelModel().fit(
            second_level_input=second_level_input, design_matrix=design_matrix
        )


@pytest.mark.parametrize("design_matrix", [1, ["foo"]])
def test_secondlevelmodel_design_matrix_error_type(img_3d_mni, design_matrix):
    second_level_input = [img_3d_mni, img_3d_mni, img_3d_mni]

    with pytest.raises(TypeError, match="'design_matrix' must be "):
        SecondLevelModel().fit(
            second_level_input=second_level_input, design_matrix=design_matrix
        )


def test_fmri_img_inputs_errors(confounds):
    # prepare correct input
    _, fmri_data, _ = generate_fake_fmri_data_and_design((SHAPE,))
    fmri_data = fmri_data[0]

    # test niimgs requirements
    niimgs = [fmri_data, fmri_data, fmri_data]
    with pytest.raises(ValueError, match="require a design matrix"):
        SecondLevelModel().fit(niimgs)
    with pytest.raises(
        TypeError,
        match=r"Elements of second_level_input must be of the same type.",
    ):
        SecondLevelModel().fit([*niimgs, []], confounds)


@pytest.mark.slow
def test_fmri_inputs_for_non_parametric_inference_errors(
    rng, confounds, shape_3d_default, shape_4d_default
):
    # Test processing of FMRI inputs
    # prepare fake data
    _, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
        [shape_4d_default], rk=1
    )

    # prepare correct input first level models
    flm = FirstLevelModel(subject_label="01").fit(
        fmri_data, design_matrices=design_matrices
    )

    # prepare correct input dataframe and lists
    p, q = 80, 10
    X = rng.standard_normal(size=(p, q))
    sdes = pd.DataFrame(X[:3, :3], columns=["intercept", "b", "c"])

    shape_3d = [(*shape_3d_default, 1)]
    _, fmri_data, _ = generate_fake_fmri_data_and_design(shape_3d)
    fmri_data = fmri_data[0]
    niimgs = [fmri_data, fmri_data, fmri_data]
    niimg_4d = concat_imgs(niimgs)

    # test missing second-level contrast
    match = "No second-level contrast is specified."
    # niimgs as input
    with pytest.raises(ValueError, match=match):
        non_parametric_inference(niimgs, None, sdes)
    with pytest.raises(ValueError, match=match):
        non_parametric_inference(niimgs, confounds, sdes)
    # 4d niimg as input
    with pytest.raises(ValueError, match=match):
        non_parametric_inference(niimg_4d, None, sdes)

    # test wrong input errors
    # test first level model
    with pytest.raises(TypeError, match="second_level_input must be"):
        non_parametric_inference(flm)

    # test list of less than two niimgs
    with pytest.raises(TypeError, match="at least two"):
        non_parametric_inference([fmri_data])

    # test niimgs requirements
    with pytest.raises(ValueError, match="require a design matrix"):
        non_parametric_inference(niimgs)
    with pytest.raises(TypeError):
        non_parametric_inference([*niimgs, []], confounds)

    # test other objects
    with pytest.raises(ValueError, match=r"File not found: .*"):
        non_parametric_inference("random string object")


@pytest.mark.slow
def test_second_level_glm_computation(n_subjects):
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    model = model.fit(Y, design_matrix=X)
    model.compute_contrast()
    labels1 = model.labels_
    results1 = model.results_

    labels2, results2 = run_glm(model.masker_.transform(Y), X.values, "ols")
    assert_almost_equal(labels1, labels2, decimal=1)

    assert len(results1) == len(results2)


@pytest.mark.parametrize("attribute", ["residuals", "predicted", "r_square"])
def test_second_level_voxelwise_attribute_errors(attribute, n_subjects):
    """Tests that an error is raised when trying to access \
       voxelwise attributes before fitting the model, \
       before computing a contrast.
    """
    mask, fmri_data, _ = generate_fake_fmri_data_and_design((SHAPE,))

    model = SecondLevelModel(mask_img=mask, minimize_memory=False)

    Y = fmri_data * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model.fit(Y, design_matrix=X)

    with pytest.raises(ValueError, match=r"The model has no results."):
        getattr(model, attribute)
    with pytest.raises(ValueError, match="attribute must be one of"):
        model._get_element_wise_model_attribute("foo", True)


@pytest.mark.slow
@pytest.mark.parametrize("attribute", ["residuals", "predicted", "r_square"])
def test_second_level_voxelwise_attribute_errors_minimize_memory(
    attribute, n_subjects
):
    """Tests that an error is raised when trying to access \
       voxelwise attributes before fitting the model, \
       when not setting ``minimize_memory`` to ``True``.
    """
    mask, fmri_data, _ = generate_fake_fmri_data_and_design((SHAPE,))

    model = SecondLevelModel(mask_img=mask, minimize_memory=True)

    Y = fmri_data * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model.fit(Y, design_matrix=X)

    model.compute_contrast()

    with pytest.raises(ValueError, match="To access voxelwise attributes"):
        getattr(model, attribute)


@pytest.mark.slow
@pytest.mark.parametrize("attribute", ["residuals", "predicted", "r_square"])
def test_second_level_voxelwise_attribute(attribute, n_subjects):
    """Smoke test for voxelwise attributes for SecondLevelModel."""
    mask, fmri_data, _ = generate_fake_fmri_data_and_design((SHAPE,))
    model = SecondLevelModel(mask_img=mask, minimize_memory=False)
    Y = fmri_data * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model.fit(Y, design_matrix=X)
    model.compute_contrast()
    getattr(model, attribute)


@pytest.mark.slow
def test_second_level_residuals(n_subjects):
    """Tests residuals computation for SecondLevelModel."""
    mask, fmri_data, _ = generate_fake_fmri_data_and_design((SHAPE,))
    model = SecondLevelModel(mask_img=mask, minimize_memory=False)
    Y = fmri_data * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model.fit(Y, design_matrix=X)
    model.compute_contrast()

    assert isinstance(model.residuals, Nifti1Image)
    assert model.residuals.shape == (*SHAPE[:3], n_subjects)
    mean_residuals = model.masker_.transform(model.residuals).mean(0)
    assert_array_almost_equal(mean_residuals, 0)


@pytest.mark.slow
def test_non_parametric_inference_permutation_computation(n_subjects):
    func_img, mask = fake_fmri_data()

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    neg_log_pvals_img = non_parametric_inference(
        Y, design_matrix=X, model_intercept=False, mask=mask, n_perm=N_PERM
    )

    assert get_data(neg_log_pvals_img).shape == SHAPE[:3]


@pytest.mark.slow
def test_non_parametric_inference_tfce(n_subjects):
    """Test non-parametric inference with TFCE inference."""
    shapes = [SHAPE] * n_subjects
    mask, fmri_data, _ = generate_fake_fmri_data_and_design(shapes)
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    out = non_parametric_inference(
        fmri_data,
        design_matrix=X,
        model_intercept=False,
        mask=mask,
        n_perm=N_PERM,
        tfce=True,
    )
    assert isinstance(out, dict)
    assert "t" in out
    assert "tfce" in out
    assert "logp_max_t" in out
    assert "logp_max_tfce" in out

    assert get_data(out["tfce"]).shape == shapes[0][:3]
    assert get_data(out["logp_max_tfce"]).shape == shapes[0][:3]


@pytest.mark.slow
def test_non_parametric_inference_cluster_level(n_subjects):
    """Test non-parametric inference with cluster-level inference."""
    func_img, mask = fake_fmri_data()

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    out = non_parametric_inference(
        Y,
        design_matrix=X,
        model_intercept=False,
        mask=mask,
        n_perm=N_PERM,
        threshold=0.001,
    )
    assert isinstance(out, dict)
    assert "t" in out
    assert "logp_max_t" in out
    assert "size" in out
    assert "logp_max_size" in out
    assert "mass" in out
    assert "logp_max_mass" in out

    assert get_data(out["logp_max_t"]).shape == SHAPE[:3]


@pytest.mark.slow
def test_non_parametric_inference_cluster_level_with_covariates(
    shape_3d_default, rng, n_subjects
):
    """Test non-parametric inference with cluster-level inference in \
    the context of covariates.
    """
    shapes = ((*shape_3d_default, 1),)
    mask, fmri_data, _ = generate_fake_fmri_data_and_design(shapes)

    unc_pval = 0.1

    # Set up one sample t-test design with two random covariates
    cov1 = rng.random(n_subjects)
    cov2 = rng.random(n_subjects)
    X = pd.DataFrame({"cov1": cov1, "cov2": cov2, "intercept": 1})

    # make sure there is variability in the images
    kernels = rng.uniform(low=0, high=5, size=n_subjects)
    Y = [smooth_img(fmri_data[0], kernel) for kernel in kernels]

    # Set up non-parametric test
    out = non_parametric_inference(
        Y,
        design_matrix=X,
        mask=mask,
        model_intercept=False,
        second_level_contrast="intercept",
        n_perm=int(1 / unc_pval),
        threshold=unc_pval,
    )

    # Calculate uncorrected cluster sizes
    df = len(Y) - X.shape[1]
    neg_log_pval = -np.log10(stats.t.sf(get_data(out["t"]), df=df))
    logp_unc = new_img_like(out["t"], neg_log_pval)
    logp_unc_cluster_sizes = list(
        get_clusters_table(logp_unc, -np.log10(unc_pval))["Cluster Size (mm3)"]
    )

    # Calculate corrected cluster sizes
    logp_max_cluster_sizes = list(
        get_clusters_table(out["logp_max_size"], unc_pval)[
            "Cluster Size (mm3)"
        ]
    )

    # Compare cluster sizes
    logp_unc_cluster_sizes.sort()
    logp_max_cluster_sizes.sort()
    assert logp_unc_cluster_sizes == logp_max_cluster_sizes


@pytest.mark.slow
def test_non_parametric_inference_cluster_level_with_single_covariates(
    shape_3d_default, rng, n_subjects
):
    """Test non-parametric inference with cluster-level inference in \
    the context of covariates.
    """
    shapes = ((*shape_3d_default, 1),)
    mask, fmri_data, _ = generate_fake_fmri_data_and_design(shapes)

    unc_pval = 0.1

    # make sure there is variability in the images
    kernels = rng.uniform(low=0, high=5, size=n_subjects)
    Y = [smooth_img(fmri_data[0], kernel) for kernel in kernels]

    # Test single covariate
    X = pd.DataFrame({"intercept": [1] * len(Y)})
    non_parametric_inference(
        Y,
        design_matrix=X,
        mask=mask,
        model_intercept=False,
        second_level_contrast="intercept",
        n_perm=N_PERM,
        threshold=unc_pval,
    )


@pytest.mark.slow
def test_second_level_contrast_computation_smoke(n_subjects):
    """Smoke test for different contrasts in fixed effects."""
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    ncol = len(model.design_matrix_.columns)
    c1, _ = np.eye(ncol)[0, :], np.zeros(ncol)
    model.compute_contrast(second_level_contrast=c1)

    # formula should work (passing variable name directly)
    model.compute_contrast("intercept")

    # or simply pass nothing
    model.compute_contrast()


@pytest.mark.slow
@pytest.mark.parametrize(
    "output_type",
    [
        "z_score",
        "stat",
        "p_value",
        "effect_size",
        "effect_variance",
    ],
)
def test_second_level_contrast_computation_all(output_type, n_subjects):
    """Test output_type='all', and verify images are equivalent."""
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    ncol = len(model.design_matrix_.columns)
    c1, _ = np.eye(ncol)[0, :], np.zeros(ncol)

    all_images = model.compute_contrast(
        second_level_contrast=c1, output_type="all"
    )

    assert_array_equal(
        get_data(all_images[output_type]),
        get_data(
            model.compute_contrast(
                second_level_contrast=c1, output_type=output_type
            )
        ),
    )


@pytest.mark.slow
def test_second_level_contrast_computation_errors(rng, n_subjects):
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)

    # asking for contrast before model fit gives error
    with pytest.raises(ValueError, match="not fitted yet"):
        model.compute_contrast(second_level_contrast="intercept")

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    ncol = len(model.design_matrix_.columns)
    c1, cnull = np.eye(ncol)[0, :], np.zeros(ncol)

    # passing null contrast should give back a value error
    with pytest.raises(ValueError, match="Contrast is null"):
        model.compute_contrast(cnull)

    # passing wrong parameters
    with pytest.raises(ValueError, match="'stat_type' must be one of"):
        model.compute_contrast(
            second_level_contrast=c1, second_level_stat_type=""
        )
    with pytest.raises(ValueError, match="'stat_type' must be one of"):
        model.compute_contrast(
            second_level_contrast=c1, second_level_stat_type=[]
        )
    with pytest.raises(ValueError, match="'output_type' must be one of "):
        model.compute_contrast(second_level_contrast=c1, output_type="")

    # check that passing no explicit contrast when the design
    # matrix has more than one columns raises an error
    X = pd.DataFrame(rng.uniform(size=(n_subjects, 2)), columns=["r1", "r2"])
    model = model.fit(Y, design_matrix=X)
    with pytest.raises(
        ValueError, match="No second-level contrast is specified"
    ):
        model.compute_contrast(None)


def test_second_level_t_contrast_length_errors(n_subjects):
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)

    func_img, mask = fake_fmri_data()
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    with pytest.raises(
        ValueError,
        match=(r"t contrasts should be of length P=1, but it has length 2."),
    ):
        model.compute_contrast(second_level_contrast=[1, 2])


@pytest.mark.slow
def test_second_level_f_contrast_length_errors(n_subjects):
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask)

    func_img, mask = fake_fmri_data()
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    with pytest.raises(
        ValueError,
        match=(r"F contrasts should have .* columns, but it has .*"),
    ):
        model.compute_contrast(second_level_contrast=np.eye(2))


@pytest.mark.slow
@pytest.mark.parametrize("second_level_contrast", [None, "intercept", [1]])
def test_non_parametric_inference_contrast_computation(
    second_level_contrast, n_subjects
):
    func_img, mask = fake_fmri_data()

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    non_parametric_inference(
        Y,
        design_matrix=X,
        model_intercept=False,
        mask=mask,
        n_perm=N_PERM,
        second_level_contrast=second_level_contrast,
    )


@pytest.mark.slow
@pytest.mark.parametrize(
    "second_level_contrast", [[1, 0], "r1", "r1-r2", [-1, 1]]
)
def test_non_parametric_inference_contrast_formula(
    second_level_contrast, rng, n_subjects
):
    func_img, _ = fake_fmri_data()
    Y = [func_img] * n_subjects
    X = pd.DataFrame(rng.uniform(size=(n_subjects, 2)), columns=["r1", "r2"])

    non_parametric_inference(
        second_level_input=Y,
        design_matrix=X,
        second_level_contrast=second_level_contrast,
    )


@pytest.mark.slow
def test_non_parametric_inference_contrast_computation_errors(rng, n_subjects):
    func_img, mask = fake_fmri_data()

    # asking for contrast before model fit gives error
    with pytest.raises(TypeError, match="second_level_input must be either"):
        non_parametric_inference(
            second_level_input=None,
            second_level_contrast="intercept",
            mask=mask,
        )

    # fit model
    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])

    ncol = len(X.columns)
    _, cnull = np.eye(ncol)[0, :], np.zeros(ncol)

    # passing null contrast should give back a value error
    with pytest.raises(
        ValueError,
        match=("Second_level_contrast must be a valid"),
    ):
        non_parametric_inference(
            second_level_input=Y,
            design_matrix=X,
            second_level_contrast=cnull,
            mask=mask,
        )
    with pytest.raises(
        ValueError,
        match=("Second_level_contrast must be a valid"),
    ):
        non_parametric_inference(
            second_level_input=Y,
            design_matrix=X,
            second_level_contrast=[],
            mask=mask,
        )

    # check that passing no explicit contrast when the design
    # matrix has more than one columns raises an error
    X = pd.DataFrame(rng.uniform(size=(n_subjects, 2)), columns=["r1", "r2"])
    with pytest.raises(
        ValueError, match=r"No second-level contrast is specified."
    ):
        non_parametric_inference(
            second_level_input=Y,
            design_matrix=X,
            second_level_contrast=None,
        )


@pytest.mark.slow
def test_second_level_contrast_computation_with_memory_caching(n_subjects):
    func_img, mask = fake_fmri_data()

    model = SecondLevelModel(mask_img=mask, memory="nilearn_cache")

    Y = [func_img] * n_subjects
    X = pd.DataFrame([[1]] * n_subjects, columns=["intercept"])
    model = model.fit(Y, design_matrix=X)

    ncol = len(model.design_matrix_.columns)
    c1 = np.eye(ncol)[0, :]

    # test memory caching for compute_contrast
    model.compute_contrast(c1, output_type="z_score")
    # or simply pass nothing
    model.compute_contrast()


def test_second_lvl_dataframe_computation(tmp_path, shape_3d_default):
    """Check that contrast can be computed when using dataframes as input.

    See bug https://github.com/nilearn/nilearn/issues/3871
    """
    file_path = write_fake_bold_img(
        file_path=tmp_path / "img.nii.gz", shape=shape_3d_default
    )

    dfcols = ["subject_label", "map_name", "effects_map_path"]
    dfrows = [
        ["01", "a", file_path],
        ["02", "a", file_path],
        ["03", "a", file_path],
    ]
    niidf = pd.DataFrame(dfrows, columns=dfcols)

    model = SecondLevelModel().fit(niidf)
    model.compute_contrast(first_level_contrast="a")


# -----------------------surface tests----------------------- #


def test_second_level_input_as_surface_image(surf_img_1d, n_subjects):
    """Test slm with a list surface images as input."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    model = SecondLevelModel()
    model = model.fit(second_level_input, design_matrix=design_matrix)

    assert isinstance(model._mask_img, SurfaceImage)


def test_second_level_input_as_surface_image_3d(surf_img_2d, n_subjects):
    """Fit with surface image with all subjects as timepoints."""
    second_level_input = surf_img_2d(n_subjects)

    design_matrix = pd.DataFrame([1] * n_subjects, columns=["intercept"])

    model = SecondLevelModel()

    model.fit(second_level_input, design_matrix=design_matrix)


def test_second_level_input_error_surface_image_2d(surf_img_2d):
    """Err when passing a single 2D SurfaceImage with."""
    n_subjects = 1
    second_level_input = surf_img_2d(n_subjects)

    design_matrix = pd.DataFrame([1] * n_subjects, columns=["intercept"])

    model = SecondLevelModel()

    with pytest.raises(TypeError, match="must be a 3D SurfaceImage"):
        model.fit(second_level_input, design_matrix=design_matrix)


@pytest.mark.thread_unsafe
def test_second_level_input_as_surface_image_3d_same_as_list_2d(
    surf_img_1d, n_subjects
):
    """Fit all subjects as timepoints same as list of subject."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame([1] * n_subjects, columns=["intercept"])

    model = SecondLevelModel()
    model.fit(second_level_input, design_matrix=design_matrix)
    result_2d = model.compute_contrast()

    second_level_input_3d = concat_imgs(second_level_input)
    model.fit(second_level_input_3d, design_matrix=design_matrix)
    result_3d = model.compute_contrast()

    assert_surface_image_equal(result_2d, result_3d)


def test_second_level_input_as_surface_no_design_matrix(
    surf_img_1d, n_subjects
):
    """Raise error when design matrix is missing."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    model = SecondLevelModel()

    with pytest.raises(
        ValueError, match="require a design matrix to be provided"
    ):
        model.fit(second_level_input, design_matrix=None)


@pytest.mark.thread_unsafe
@pytest.mark.parametrize("surf_mask_dim", [1, 2])
def test_second_level_input_as_surface_image_with_mask(
    surf_img_1d, surf_mask_dim, surf_mask_1d, surf_mask_2d, n_subjects
):
    """Test slm with surface mask and a list surface images as input."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )
    surf_mask = surf_mask_1d if surf_mask_dim == 1 else surf_mask_2d()

    model = SecondLevelModel(mask_img=surf_mask)
    model = model.fit(second_level_input, design_matrix=design_matrix)


@pytest.mark.thread_unsafe
def test_second_level_input_with_wrong_mask(
    surf_img_1d, surf_mask_1d, img_mask_mni, n_subjects
):
    """Test slm with mask of the wrong type."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    # volume mask with surface data
    model = SecondLevelModel(mask_img=img_mask_mni)

    with pytest.raises(
        TypeError, match=r"Mask and input images must be of compatible types."
    ):
        model = model.fit(second_level_input, design_matrix=design_matrix)

    # surface mask with volume data
    func_img, _ = fake_fmri_data()
    second_level_input = [func_img] * 3
    model = SecondLevelModel(mask_img=surf_mask_1d)

    with pytest.raises(
        TypeError, match=r"Mask and input images must be of compatible types."
    ):
        model = model.fit(second_level_input, design_matrix=design_matrix)


def test_second_level_input_as_surface_image_warning_smoothing(
    surf_img_1d, n_subjects
):
    """Warn smoothing surface not implemented."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    model = SecondLevelModel(smoothing_fwhm=8.0)
    with pytest.warns(NotImplementedWarning, match="not yet supported"):
        model = model.fit(second_level_input, design_matrix=design_matrix)


def test_second_level_input_as_flm_of_surface_image(
    surface_glm_data, n_subjects
):
    """Test fitting of list of first level model with surface data."""
    second_level_input = []
    for _ in range(n_subjects):
        img, des = surface_glm_data(5)
        model = FirstLevelModel()
        model.fit(img, design_matrices=des)
        second_level_input.append(model)

    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    model = SecondLevelModel()
    model = model.fit(second_level_input, design_matrix=design_matrix)


@pytest.mark.thread_unsafe
def test_second_level_surface_image_contrast_computation(
    surf_img_1d, n_subjects
):
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame(
        [1] * len(second_level_input), columns=["intercept"]
    )

    model = SecondLevelModel()

    model = model.fit(second_level_input, design_matrix=design_matrix)

    # simply pass nothing
    model.compute_contrast()

    # formula should work (passing variable name directly)
    model.compute_contrast("intercept")

    # smoke test for different contrasts in fixed effects
    ncol = len(model.design_matrix_.columns)
    c1, _ = np.eye(ncol)[0, :], np.zeros(ncol)
    model.compute_contrast(second_level_contrast=c1)

    # Test output_type='all', and verify images are equivalent
    all_images = model.compute_contrast(
        second_level_contrast=c1, output_type="all"
    )
    for key in [
        "z_score",
        "stat",
        "p_value",
        "effect_size",
        "effect_variance",
    ]:
        assert_surface_image_equal(
            all_images[key],
            model.compute_contrast(second_level_contrast=c1, output_type=key),
        )


@pytest.mark.thread_unsafe
@pytest.mark.parametrize("two_sided_test", [True, False])
def test_non_parametric_inference_with_surface_images(
    surf_img_1d, two_sided_test, n_subjects
):
    """Smoke test non_parametric_inference on list of 1D surfaces."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame([1] * n_subjects, columns=["intercept"])

    non_parametric_inference(
        second_level_input=second_level_input,
        design_matrix=design_matrix,
        n_perm=N_PERM,
        two_sided_test=two_sided_test,
    )


def test_non_parametric_inference_with_surface_images_2d(
    surf_img_2d, n_subjects
):
    """Smoke test non_parametric_inference on 2d surfaces."""
    second_level_input = surf_img_2d(n_subjects)

    design_matrix = pd.DataFrame([1] * n_subjects, columns=["intercept"])

    non_parametric_inference(
        second_level_input=second_level_input,
        design_matrix=design_matrix,
        n_perm=N_PERM,
    )


def test_non_parametric_inference_with_surface_images_2d_mask(
    surf_img_2d, surf_mask_1d, n_subjects
):
    """Smoke test non_parametric_inference on 2d surfaces and a mask."""
    second_level_input = surf_img_2d(n_subjects)

    design_matrix = pd.DataFrame([1] * n_subjects, columns=["intercept"])

    masker = SurfaceMasker(surf_mask_1d)

    non_parametric_inference(
        second_level_input=second_level_input,
        design_matrix=design_matrix,
        n_perm=N_PERM,
        mask=masker,
    )


def test_non_parametric_inference_with_surface_images_warnings(
    surf_img_1d, n_subjects
):
    """Throw warnings for non implemented features for surface."""
    second_level_input = [surf_img_1d for _ in range(n_subjects)]

    design_matrix = pd.DataFrame([1] * n_subjects, columns=["intercept"])

    with pytest.warns(
        NotImplementedWarning,
        match="'smoothing_fwhm' is not yet supported for surface data.",
    ):
        non_parametric_inference(
            second_level_input=second_level_input,
            design_matrix=design_matrix,
            n_perm=N_PERM,
            smoothing_fwhm=6,
        )
    with pytest.warns(
        NotImplementedWarning,
        match="Cluster level inference not yet implemented for surface data.",
    ):
        non_parametric_inference(
            second_level_input=second_level_input,
            design_matrix=design_matrix,
            n_perm=N_PERM,
            tfce=True,
        )
    with pytest.warns(
        NotImplementedWarning,
        match="Cluster level inference not yet implemented for surface data.",
    ):
        non_parametric_inference(
            second_level_input=second_level_input,
            design_matrix=design_matrix,
            n_perm=N_PERM,
            threshold=0.001,
        )
