"""Tests for mldash._detect — auto-detection of model metadata."""
import numpy as np
import pytest

from mldash._detect import (
    _make_serializable,
    detect_all,
    detect_class_labels,
    detect_feature_names,
    detect_framework,
    detect_hyperparameters,
    detect_model_class,
    detect_n_features,
    detect_task_type,
)


# ── detect_task_type ──


class TestDetectTaskType:
    def test_detects_classifier(self, fitted_classifier):
        assert detect_task_type(fitted_classifier) == 'classification'

    def test_detects_regressor(self, fitted_regressor):
        assert detect_task_type(fitted_regressor) == 'regression'

    def test_detects_rf_classifier(self, fitted_rf_classifier):
        assert detect_task_type(fitted_rf_classifier) == 'classification'

    def test_detects_rf_regressor(self, fitted_rf_regressor):
        assert detect_task_type(fitted_rf_regressor) == 'regression'

    def test_heuristic_classifier_name(self):
        """Falls back to class name if not sklearn."""

        class MyClassifier:
            pass

        assert detect_task_type(MyClassifier()) == 'classification'

    def test_heuristic_regressor_name(self):
        class MyRegression:
            pass

        assert detect_task_type(MyRegression()) == 'regression'

    def test_heuristic_svc(self):
        class MySVC:
            pass

        assert detect_task_type(MySVC()) == 'classification'

    def test_heuristic_svr(self):
        class MySVR:
            pass

        assert detect_task_type(MySVR()) == 'regression'

    def test_heuristic_lasso(self):
        class MyLasso:
            pass

        assert detect_task_type(MyLasso()) == 'regression'

    def test_classes_attribute_fallback(self):
        """Model with classes_ attribute detected as classifier."""

        class Unknown:
            classes_ = [0, 1]

        assert detect_task_type(Unknown()) == 'classification'

    def test_returns_none_for_unknown(self):
        class Unknown:
            pass

        assert detect_task_type(Unknown()) is None


# ── detect_framework ──


class TestDetectFramework:
    def test_detects_sklearn(self, fitted_classifier):
        assert detect_framework(fitted_classifier) == 'sklearn'

    def test_detects_sklearn_regressor(self, fitted_regressor):
        assert detect_framework(fitted_regressor) == 'sklearn'

    def test_returns_none_for_unknown(self):
        class MyModel:
            pass

        assert detect_framework(MyModel()) is None

    def test_detects_xgboost_module(self):
        class FakeModel:
            pass

        FakeModel.__module__ = 'xgboost.core'
        m = FakeModel()
        assert detect_framework(m) == 'xgboost'

    def test_detects_pytorch_from_torch(self):
        class FakeModel:
            pass

        FakeModel.__module__ = 'torch.nn.modules'
        assert detect_framework(FakeModel()) == 'pytorch'


# ── detect_hyperparameters ──


class TestDetectHyperparameters:
    def test_extracts_sklearn_params(self, fitted_classifier):
        params = detect_hyperparameters(fitted_classifier)
        assert isinstance(params, dict)
        assert 'max_iter' in params
        assert params['max_iter'] == 200

    def test_extracts_rf_params(self, fitted_rf_classifier):
        params = detect_hyperparameters(fitted_rf_classifier)
        assert params['n_estimators'] == 5
        assert params['random_state'] == 42

    def test_returns_empty_for_no_get_params(self):
        class NoParams:
            pass

        assert detect_hyperparameters(NoParams()) == {}


# ── detect_class_labels ──


class TestDetectClassLabels:
    def test_extracts_from_classifier(self, fitted_classifier):
        labels = detect_class_labels(fitted_classifier)
        assert labels == ['0', '1']

    def test_extracts_string_labels(self, fitted_rf_classifier):
        labels = detect_class_labels(fitted_rf_classifier)
        assert set(labels) == {'cat', 'dog'}

    def test_returns_none_for_regressor(self, fitted_regressor):
        assert detect_class_labels(fitted_regressor) is None


# ── detect_n_features ──


class TestDetectNFeatures:
    def test_extracts_from_fitted_model(self, fitted_classifier):
        assert detect_n_features(fitted_classifier) == 2

    def test_extracts_from_rf(self, fitted_rf_classifier):
        assert detect_n_features(fitted_rf_classifier) == 3

    def test_returns_none_without_attribute(self):
        class NoFeatures:
            pass

        assert detect_n_features(NoFeatures()) is None


# ── detect_feature_names ──


class TestDetectFeatureNames:
    def test_extracts_from_pandas_fitted(self, fitted_rf_classifier):
        names = detect_feature_names(fitted_rf_classifier)
        # If pandas was available, feature names were set
        if names is not None:
            assert names == ['feat_a', 'feat_b', 'feat_c']

    def test_returns_none_without_attribute(self, fitted_classifier):
        # LogisticRegression fitted on numpy doesn't have feature_names_in_
        result = detect_feature_names(fitted_classifier)
        assert result is None


# ── detect_model_class ──


class TestDetectModelClass:
    def test_returns_class_name(self, fitted_classifier):
        assert detect_model_class(fitted_classifier) == 'LogisticRegression'

    def test_returns_rf_class_name(self, fitted_rf_classifier):
        assert detect_model_class(fitted_rf_classifier) == 'RandomForestClassifier'


# ── detect_all ──


class TestDetectAll:
    def test_returns_all_fields(self, fitted_rf_classifier):
        result = detect_all(fitted_rf_classifier)
        assert result['task_type'] == 'classification'
        assert result['framework'] == 'sklearn'
        assert result['model_class'] == 'RandomForestClassifier'
        assert isinstance(result['hyperparameters'], dict)
        assert set(result['class_labels']) == {'cat', 'dog'}
        assert result['n_features'] == 3

    def test_user_overrides_take_precedence(self, fitted_classifier):
        result = detect_all(
            fitted_classifier,
            task_type='regression',
            framework='custom',
        )
        assert result['task_type'] == 'regression'
        assert result['framework'] == 'custom'
        # Other fields still auto-detected
        assert result['model_class'] == 'LogisticRegression'


# ── _make_serializable ──


class TestMakeSerializable:
    def test_passes_through_native_types(self):
        assert _make_serializable(42) == 42
        assert _make_serializable(3.14) == 3.14
        assert _make_serializable('hello') == 'hello'
        assert _make_serializable(True) is True
        assert _make_serializable(None) is None

    def test_converts_numpy_int(self):
        result = _make_serializable(np.int64(42))
        assert result == 42
        assert type(result) is int

    def test_converts_numpy_float(self):
        result = _make_serializable(np.float64(3.14))
        assert result == pytest.approx(3.14)
        assert type(result) is float

    def test_converts_numpy_bool(self):
        result = _make_serializable(np.bool_(True))
        assert result is True
        assert type(result) is bool

    def test_converts_numpy_array(self):
        result = _make_serializable(np.array([1, 2, 3]))
        assert result == [1, 2, 3]
        assert isinstance(result, list)

    def test_converts_dict_recursively(self):
        result = _make_serializable({'a': np.int64(1), 'b': np.float64(2.0)})
        assert result == {'a': 1, 'b': 2.0}

    def test_converts_list_recursively(self):
        result = _make_serializable([np.int64(1), np.float64(2.0)])
        assert result == [1, 2.0]

    def test_falls_back_to_str(self):
        class Weird:
            def __str__(self):
                return 'weird_value'

        result = _make_serializable(Weird())
        assert result == 'weird_value'
