"""Tests for XAI Suite explainers."""

import pytest
import numpy as np

from xai_suite.explainers import BaseExplainer, ShapExplainer, LimeExplainer


class TestBaseExplainer:
    """Tests for BaseExplainer abstract class."""

    def test_cannot_instantiate_directly(self, trained_classifier, iris_data):
        """BaseExplainer should not be instantiated directly."""
        X, _ = iris_data
        with pytest.raises(TypeError):
            BaseExplainer(trained_classifier, X)


class TestShapExplainer:
    """Tests for SHAP explainer."""

    def test_initialization(self, trained_classifier, iris_data):
        """Test SHAP explainer initialization."""
        X, _ = iris_data
        explainer = ShapExplainer(trained_classifier, X)
        assert explainer is not None
        assert explainer.model == trained_classifier

    def test_explain_local_returns_dict(self, trained_classifier, iris_data, sample_instance):
        """Test that explain_local returns a dictionary."""
        X, _ = iris_data
        explainer = ShapExplainer(trained_classifier, X)
        result = explainer.explain_local(sample_instance)

        assert isinstance(result, dict)
        assert "method" in result
        assert result["method"] == "SHAP"

    def test_explain_local_has_required_keys(self, trained_classifier, iris_data, sample_instance):
        """Test that explain_local result has required keys."""
        X, _ = iris_data
        explainer = ShapExplainer(trained_classifier, X)
        result = explainer.explain_local(sample_instance)

        required_keys = ["method", "feature_names", "importance_scores"]
        for key in required_keys:
            assert key in result, f"Missing key: {key}"

    def test_explain_local_feature_count_matches(self, trained_classifier, iris_data, sample_instance):
        """Test that feature count matches input."""
        X, _ = iris_data
        explainer = ShapExplainer(trained_classifier, X)
        result = explainer.explain_local(sample_instance)

        assert len(result["feature_names"]) == X.shape[1]
        assert len(result["importance_scores"]) == X.shape[1]

    def test_importance_scores_are_numeric(self, trained_classifier, iris_data, sample_instance):
        """Test that importance scores are numeric."""
        X, _ = iris_data
        explainer = ShapExplainer(trained_classifier, X)
        result = explainer.explain_local(sample_instance)

        for score in result["importance_scores"]:
            assert isinstance(score, (int, float, np.floating))

    def test_with_isolation_forest(self, trained_anomaly_detector, binary_classification_data, sample_binary_instance):
        """Test SHAP with anomaly detection model."""
        X, _ = binary_classification_data
        explainer = ShapExplainer(trained_anomaly_detector, X)
        result = explainer.explain_local(sample_binary_instance)

        assert "method" in result
        assert len(result["importance_scores"]) == X.shape[1]


class TestLimeExplainer:
    """Tests for LIME explainer."""

    def test_initialization_classification(self, trained_classifier, iris_data):
        """Test LIME explainer initialization for classification."""
        X, _ = iris_data
        explainer = LimeExplainer(trained_classifier, X, config={"mode": "classification"})
        assert explainer is not None

    def test_initialization_regression(self, trained_classifier, iris_data):
        """Test LIME explainer initialization for regression mode."""
        X, _ = iris_data
        # Note: Using classifier with regression mode for testing initialization
        explainer = LimeExplainer(trained_classifier, X, config={"mode": "regression"})
        assert explainer is not None

    def test_explain_local_returns_dict(self, trained_classifier, iris_data, sample_instance):
        """Test that explain_local returns a dictionary."""
        X, _ = iris_data
        explainer = LimeExplainer(trained_classifier, X, config={"mode": "classification"})
        result = explainer.explain_local(sample_instance)

        assert isinstance(result, dict)
        assert "method" in result
        assert result["method"] == "LIME"

    def test_explain_local_has_required_keys(self, trained_classifier, iris_data, sample_instance):
        """Test that explain_local result has required keys."""
        X, _ = iris_data
        explainer = LimeExplainer(trained_classifier, X, config={"mode": "classification"})
        result = explainer.explain_local(sample_instance)

        required_keys = ["method", "feature_names", "importance_scores"]
        for key in required_keys:
            assert key in result, f"Missing key: {key}"

    def test_importance_scores_are_numeric(self, trained_classifier, iris_data, sample_instance):
        """Test that importance scores are numeric."""
        X, _ = iris_data
        explainer = LimeExplainer(trained_classifier, X, config={"mode": "classification"})
        result = explainer.explain_local(sample_instance)

        for score in result["importance_scores"]:
            assert isinstance(score, (int, float, np.floating))

    def test_binary_classification(self, trained_binary_classifier, binary_classification_data, sample_binary_instance):
        """Test LIME with binary classification."""
        X, _ = binary_classification_data
        explainer = LimeExplainer(trained_binary_classifier, X, config={"mode": "classification"})
        result = explainer.explain_local(sample_binary_instance)

        assert "method" in result
        assert result["method"] == "LIME"
