import platform

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pytest
import sklearn
import sklearn.ensemble
from numpy.testing import assert_array_equal

import shap


@pytest.mark.mpl_image_compare
def test_summary():
    """Just make sure the summary_plot function doesn't crash."""
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(np.random.randn(20, 5), show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_with_data():
    """Just make sure the summary_plot function doesn't crash with data."""
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(np.random.randn(20, 5), np.random.randn(20, 5), show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_multi_class():
    """Check a multiclass run."""
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot([np.random.randn(20, 5) for i in range(3)], np.random.randn(20, 5), show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_multi_class_legend_decimals():
    """Check the functionality of printing the legend in the plot of a multiclass run when
    all the SHAP values are smaller than 1.
    """
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(
        [np.random.randn(20, 5) for i in range(3)], np.random.randn(20, 5), show=False, show_values_in_legend=True
    )
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_multi_class_legend():
    """Check the functionality of printing the legend in the plot of a multiclass run when
    SHAP values are bigger than 1.
    """
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(
        [(2 + np.random.randn(20, 5)) for i in range(3)],
        2 + np.random.randn(20, 5),
        show=False,
        show_values_in_legend=True,
    )
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_bar_with_data():
    """Check a bar chart."""
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(np.random.randn(20, 5), np.random.randn(20, 5), plot_type="bar", show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_dot_with_data():
    """Check a dot chart."""
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(np.random.randn(20, 5), np.random.randn(20, 5), plot_type="dot", show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.skipif(platform.system() in ["Windows", "Darwin"], reason="Images not matching on MacOS and Windows.")
@pytest.mark.mpl_image_compare
def test_summary_compact_dot_with_data():
    """Check a bar chart."""
    n_samples = 100
    n_features = 5
    np.random.seed(0)  # for reproducibility
    X = np.random.randn(n_samples, n_features)
    feature_names = [f"Feature {i + 1}" for i in range(n_features)]
    shap_values = np.random.randn(n_samples, n_features, n_features)
    fig = plt.figure()

    shap.summary_plot(shap_values, X, feature_names=feature_names, plot_type="compact_dot", show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_violin_with_data():
    """Check a violin chart."""
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(np.random.randn(20, 5), np.random.randn(20, 5), plot_type="violin", show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_layered_violin_with_data():
    """Check a layered violin chart."""
    rs = np.random.RandomState(0)
    fig = plt.figure()
    shap_values = rs.randn(200, 5)
    feats = rs.randn(200, 5)
    shap.summary_plot(
        shap_values,
        feats,
        plot_type="layered_violin",
        show=False,
    )
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare(tolerance=6)
def test_summary_with_log_scale():
    """Check a with a log scale."""
    np.random.seed(0)
    fig = plt.figure()
    shap.summary_plot(np.random.randn(20, 5), use_log_scale=True, show=False)
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.parametrize("background", [True, False])
def test_summary_binary_multiclass(background):
    # See GH #2893
    lightgbm = pytest.importorskip("lightgbm")
    num_examples, num_features = 100, 3
    rs = np.random.RandomState(0)
    X = rs.normal(size=[num_examples, num_features])
    y = ((2 * X[:, 0] + X[:, 1]) > 0).astype(int)

    train_data = lightgbm.Dataset(X, label=y)
    model = lightgbm.train(dict(objective="multiclass", num_classes=2), train_data)
    data = X if background else None
    explainer = shap.TreeExplainer(model, data=data)
    shap_values = explainer.shap_values(X)
    shap.summary_plot(shap_values, X, feature_names=["foo", "bar", "baz"], show=False)


@pytest.mark.mpl_image_compare
def test_summary_multiclass_explanation():
    """Check summary plot with multiclass model with explanation as input."""
    xgboost = pytest.importorskip("xgboost")
    n_samples = 100
    n_features = 5
    n_classes = 3
    np.random.seed(0)  # for reproducibility
    X = np.random.randn(n_samples, n_features)
    y = np.random.randint(0, n_classes, n_samples)
    feature_names = [f"Feature {i + 1}" for i in range(n_features)]
    model = xgboost.XGBClassifier(n_estimators=10, random_state=0, tree_method="exact", base_score=0.5).fit(X, y)
    explainer = shap.TreeExplainer(model)
    shap_values = explainer(X)
    fig = plt.figure()
    shap.summary_plot(shap_values, X, feature_names=feature_names, show=False)
    fig = plt.gcf()
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_bar_multiclass():
    # GH 3984
    X, y = shap.datasets.iris()
    model = sklearn.ensemble.RandomForestClassifier(max_depth=2, random_state=0)
    model.fit(X, y)
    explainer = shap.TreeExplainer(model)
    shap_values = explainer.shap_values(X)
    shap.summary_plot(
        shap_values, X, plot_type="bar", class_names=[0, 1, 2], feature_names=np.array(X.columns), show=False
    )
    fig = plt.gcf()
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_violin_regression():
    # GH 4030
    X, y = sklearn.datasets.make_regression(n_features=4, n_informative=2, random_state=0, shuffle=False)

    regr = sklearn.ensemble.RandomForestRegressor(max_depth=2, random_state=0)
    _ = regr.fit(X, y)

    explainer = shap.TreeExplainer(regr)
    shap_values = explainer.shap_values(X, y=y)
    shap.summary_plot(shap_values, features=X, plot_type="violin", show=False)
    fig = plt.gcf()
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.mpl_image_compare
def test_summary_plot_interaction():
    """Checks the summary plot with interaction effects (GH #4081)."""
    n_samples = 100
    n_features = 5
    np.random.seed(0)  # for reproducibility
    shap_values = np.random.randn(n_samples, n_features, n_features)
    feature_names = [f"Feature {i + 1}" for i in range(n_features)]
    X = pd.DataFrame(np.random.randn(n_samples, n_features), columns=feature_names)

    shap.summary_plot(shap_values, X)
    fig = plt.gcf()
    fig.set_layout_engine("tight")
    return fig


@pytest.mark.xfail(
    reason="Currently not supported since this needs an overhaul of the summary plot code. See #3920 for more information."
)
@pytest.mark.mpl_image_compare
def test_summary_plot_twice():
    # GH 3920
    xgboost = pytest.importorskip("xgboost")

    X, y = shap.datasets.california()
    model = xgboost.XGBRegressor().fit(X, y)

    explainer = shap.TreeExplainer(model)
    shapValues = explainer.shap_values(X)

    shap.summary_plot(shapValues, X, show=False)
    shap.summary_plot(shapValues, X, show=False)
    fig = plt.gcf()
    fig.set_layout_engine("tight")
    return fig


def test_summary_plot_wrong_features_shape():
    """Checks that ValueError is raised if the features data matrix
    has an incompatible shape with the shap_values matrix.
    """

    rs = np.random.RandomState(42)

    emsg = (
        r"The shape of the shap_values matrix does not match the shape of the provided data matrix\. "
        r"Perhaps the extra column in the shap_values matrix is the constant offset\? Of so just pass shap_values\[:,:-1\]\."
    )
    with pytest.raises(ValueError, match=emsg):
        shap.summary_plot(rs.randn(20, 5), rs.randn(20, 4), show=False)

    emsg = "The shape of the shap_values matrix does not match the shape of the provided data matrix."
    with pytest.raises(AssertionError, match=emsg):
        shap.summary_plot(rs.randn(20, 5), rs.randn(20, 1), show=False)


@pytest.mark.mpl_image_compare
def test_summary_plot(explainer):
    """Check a beeswarm chart renders correctly with shap_values as an Explanation
    object (default settings).
    """
    fig = plt.figure()
    shap_values = explainer(explainer.data)
    shap.plots.beeswarm(shap_values, show=False)
    plt.tight_layout()
    return fig


@pytest.mark.parametrize(
    "rng",
    [
        np.random.default_rng(167089660),
        17,
        np.random.SeedSequence(entropy=60767),
    ],
)
def test_summary_plot_seed_insulated(explainer, rng):
    # ensure that it is possible for downstream
    # projects to avoid mutating global NumPy
    # random state
    # see i.e., https://scientific-python.org/specs/spec-0007/
    shap_values = explainer(explainer.data)
    state_before = np.random.get_state()[1]  # type: ignore[index]
    shap.summary_plot(shap_values, show=False, rng=rng)
    state_after = np.random.get_state()[1]  # type: ignore[index]
    assert_array_equal(state_after, state_before)


def test_summary_plot_warning(explainer):
    # enforce FutureWarning for usage of global random
    # state as we prepare for SPEC 7 adoption
    shap_values = explainer(explainer.data)
    with pytest.warns(FutureWarning, match="NumPy global RNG"):
        shap.summary_plot(shap_values, show=False)
