import pytest
import numpy as np
import pandas as pd

lgb = pytest.importorskip("lightgbm", reason="lightgbm not installed")
lgb_early_stopping = lgb.early_stopping

from mllabs.adapter._lightgbm import LightGBMAdapter
from mllabs._pipeline import _params_equal


@pytest.fixture
def adapter():
    return LightGBMAdapter(eval_mode='none', verbose=0)


def make_data_dict(with_valid=False):
    X = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
    y = pd.Series([0, 1, 0])
    if with_valid:
        return {'X': (X, X), 'y': (y, y)}
    return {'X': (X, None), 'y': (y, None)}


class TestGetParams:
    def test_filters_early_stopping(self, adapter):
        params = {'n_estimators': 100, 'early_stopping': {'stopping_rounds': 50}}
        result = adapter.get_params(params)
        assert 'early_stopping' not in result
        assert result['n_estimators'] == 100

    def test_filters_eval_metric(self, adapter):
        params = {'n_estimators': 100, 'eval_metric': 'logloss'}
        result = adapter.get_params(params)
        assert 'eval_metric' not in result

    def test_passes_other_params(self, adapter):
        params = {'n_estimators': 200, 'learning_rate': 0.05}
        result = adapter.get_params(params)
        assert result == params


class TestGetFitParamsEarlyStopping:
    def test_dict_early_stopping_creates_callback(self, adapter):
        data_dict = make_data_dict()
        params = {'early_stopping': {'stopping_rounds': 50, 'verbose': False}}
        fit_params = adapter.get_fit_params(data_dict, params=params)
        assert 'callbacks' in fit_params
        assert len(fit_params['callbacks']) == 1
        cb = fit_params['callbacks'][0]
        # lgb_early_stopping 인스턴스인지 확인
        assert hasattr(cb, 'stopping_rounds') or callable(cb)

    def test_dict_early_stopping_stopping_rounds(self, adapter):
        data_dict = make_data_dict()
        params = {'early_stopping': {'stopping_rounds': 30, 'verbose': False}}
        fit_params = adapter.get_fit_params(data_dict, params=params)
        cb = fit_params['callbacks'][0]
        assert cb.stopping_rounds == 30

    def test_instance_early_stopping_passthrough(self, adapter):
        data_dict = make_data_dict()
        es_instance = lgb_early_stopping(stopping_rounds=20, verbose=False)
        params = {'early_stopping': es_instance}
        fit_params = adapter.get_fit_params(data_dict, params=params)
        assert fit_params['callbacks'][0] is es_instance

    def test_no_early_stopping_no_callbacks(self, adapter):
        data_dict = make_data_dict()
        fit_params = adapter.get_fit_params(data_dict, params={})
        assert 'callbacks' not in fit_params

    def test_eval_metric_passed_to_fit(self, adapter):
        data_dict = make_data_dict()
        params = {'eval_metric': 'auc'}
        fit_params = adapter.get_fit_params(data_dict, params=params)
        assert fit_params.get('eval_metric') == 'auc'


class TestParamsEqualWithDictEarlyStopping:
    def test_same_dict_is_equal(self):
        a = {'n_estimators': 100, 'early_stopping': {'stopping_rounds': 50, 'verbose': False}}
        b = {'n_estimators': 100, 'early_stopping': {'stopping_rounds': 50, 'verbose': False}}
        assert _params_equal(a, b)

    def test_different_dict_not_equal(self):
        a = {'early_stopping': {'stopping_rounds': 50}}
        b = {'early_stopping': {'stopping_rounds': 30}}
        assert not _params_equal(a, b)

    def test_two_instances_not_equal(self):
        # 인스턴스 두 개는 _params_equal에서 같다고 보장되지 않음 (이것이 이슈의 근본)
        a = {'early_stopping': lgb_early_stopping(stopping_rounds=50)}
        b = {'early_stopping': lgb_early_stopping(stopping_rounds=50)}
        # dict로 바꾸면 equal, 인스턴스면 같다고 보장 안됨 - 이슈 확인용
        a_dict = {'early_stopping': {'stopping_rounds': 50}}
        b_dict = {'early_stopping': {'stopping_rounds': 50}}
        assert _params_equal(a_dict, b_dict)
