"""Tests for mldash._validate — input validation."""
import pytest

from mldash._errors import MLDashValidationError
from mldash._validate import (
    VALID_FEATURE_TYPES,
    VALID_FRAMEWORKS,
    VALID_TASK_TYPES,
    validate_all,
    validate_api_key,
    validate_encoder,
    validate_features,
    validate_framework,
    validate_label_map,
    validate_model,
    validate_name,
    validate_scaler,
    validate_target,
    validate_task_type,
    validate_url,
)


# ── validate_model ──


class TestValidateModel:
    def test_rejects_object_without_predict(self):
        with pytest.raises(MLDashValidationError, match="predict"):
            validate_model(object())

    def test_rejects_unfitted_model(self):
        class FakeModel:
            def predict(self, X):
                pass

        with pytest.raises(MLDashValidationError, match="fitted"):
            validate_model(FakeModel())

    def test_accepts_fitted_model(self, fitted_classifier):
        validate_model(fitted_classifier)  # should not raise

    def test_accepts_model_with_any_fitted_indicator(self):
        class FakeModel:
            def predict(self, X):
                pass

            n_features_in_ = 5

        validate_model(FakeModel())  # should not raise

    def test_accepts_model_with_coef(self):
        class FakeModel:
            def predict(self, X):
                pass

            coef_ = [1, 2]

        validate_model(FakeModel())

    def test_accepts_model_with_classes(self):
        class FakeModel:
            def predict(self, X):
                pass

            classes_ = ['a', 'b']

        validate_model(FakeModel())


# ── validate_name ──


class TestValidateName:
    def test_rejects_none(self):
        with pytest.raises(MLDashValidationError, match="name"):
            validate_name(None)

    def test_rejects_empty_string(self):
        with pytest.raises(MLDashValidationError, match="name"):
            validate_name('')

    def test_rejects_whitespace_only(self):
        with pytest.raises(MLDashValidationError, match="name"):
            validate_name('   ')

    def test_rejects_non_string(self):
        with pytest.raises(MLDashValidationError, match="name"):
            validate_name(123)

    def test_accepts_valid_name(self):
        validate_name('My Model')


# ── validate_url ──


class TestValidateUrl:
    def test_rejects_non_string(self):
        with pytest.raises(MLDashValidationError, match="url"):
            validate_url(123)

    def test_rejects_no_scheme(self):
        with pytest.raises(MLDashValidationError, match="http"):
            validate_url('example.com')

    def test_rejects_ftp(self):
        with pytest.raises(MLDashValidationError, match="http"):
            validate_url('ftp://example.com')

    def test_accepts_http(self):
        validate_url('http://localhost:8000')

    def test_accepts_https(self):
        validate_url('https://api.mldash.io')


# ── validate_api_key ──


class TestValidateApiKey:
    def test_rejects_non_string(self):
        with pytest.raises(MLDashValidationError, match="api_key"):
            validate_api_key(123)

    def test_rejects_wrong_prefix(self):
        with pytest.raises(MLDashValidationError, match="mldash_"):
            validate_api_key('sk_test_abc123')

    def test_accepts_valid_key(self):
        validate_api_key('mldash_abc123')


# ── validate_scaler ──


class TestValidateScaler:
    def test_none_is_ok(self):
        validate_scaler(None)

    def test_rejects_without_transform(self):
        with pytest.raises(MLDashValidationError, match="transform"):
            validate_scaler(object())

    def test_accepts_fitted_scaler(self, fitted_scaler):
        validate_scaler(fitted_scaler)


# ── validate_encoder ──


class TestValidateEncoder:
    def test_none_is_ok(self):
        validate_encoder(None)

    def test_rejects_without_transform(self):
        with pytest.raises(MLDashValidationError, match="transform"):
            validate_encoder(object())

    def test_accepts_fitted_encoder(self, fitted_encoder):
        validate_encoder(fitted_encoder)


# ── validate_label_map ──


class TestValidateLabelMap:
    def test_none_is_ok(self):
        validate_label_map(None)

    def test_rejects_non_dict(self):
        with pytest.raises(MLDashValidationError, match="dict"):
            validate_label_map([1, 2, 3])

    def test_rejects_non_string_keys(self):
        with pytest.raises(MLDashValidationError, match="string keys"):
            validate_label_map({0: 'neg', 1: 'pos'})

    def test_accepts_empty_dict(self):
        validate_label_map({})

    def test_accepts_valid_label_map(self, sample_label_map):
        validate_label_map(sample_label_map)


# ── validate_task_type ──


class TestValidateTaskType:
    def test_none_is_ok(self):
        validate_task_type(None)

    def test_rejects_invalid(self):
        with pytest.raises(MLDashValidationError):
            validate_task_type('clustering')

    def test_accepts_classification(self):
        validate_task_type('classification')

    def test_accepts_regression(self):
        validate_task_type('regression')


# ── validate_framework ──


class TestValidateFramework:
    def test_none_is_ok(self):
        validate_framework(None)

    def test_rejects_unknown(self):
        with pytest.raises(MLDashValidationError, match="Unknown framework"):
            validate_framework('keras')

    def test_accepts_all_valid(self):
        for fw in VALID_FRAMEWORKS:
            validate_framework(fw)


# ── validate_features ──


class TestValidateFeatures:
    def test_none_is_ok(self):
        validate_features(None)

    def test_rejects_non_dict(self):
        with pytest.raises(MLDashValidationError, match="dict"):
            validate_features(['a', 'b'])

    def test_rejects_invalid_type(self):
        with pytest.raises(MLDashValidationError, match="numeric"):
            validate_features({'col1': 'integer'})

    def test_accepts_valid_features(self):
        validate_features({'age': 'numeric', 'dept': 'categorical', 'review': 'text'})


# ── validate_target ──


class TestValidateTarget:
    def test_none_is_ok(self):
        validate_target(None)

    def test_rejects_non_dict(self):
        with pytest.raises(MLDashValidationError):
            validate_target('label')

    def test_rejects_multiple_entries(self):
        with pytest.raises(MLDashValidationError, match="exactly one"):
            validate_target({'col1': 'numeric', 'col2': 'numeric'})

    def test_accepts_single_entry(self):
        validate_target({'label': 'categorical'})


# ── validate_all ──


class TestValidateAll:
    def test_runs_all_checks(self, fitted_classifier, fitted_scaler, fitted_encoder):
        validate_all(
            fitted_classifier,
            'My Model',
            scaler=fitted_scaler,
            encoder=fitted_encoder,
            label_map={'0': 'neg', '1': 'pos'},
            task_type='classification',
            framework='sklearn',
            features={'col1': 'numeric', 'col2': 'numeric'},
            target={'label': 'categorical'},
        )

    def test_fails_on_first_invalid(self):
        with pytest.raises(MLDashValidationError, match="predict"):
            validate_all(object(), 'name')
