"""Tests for descriptive stats, cleaning, and ML modules."""

import numpy as np
import pandas as pd
import pytest

import bizanalyticsai as bai
from bizanalyticsai import stats, ml


@pytest.fixture
def df():
    np.random.seed(0)
    return pd.DataFrame({
        "revenue":  np.random.lognormal(10, 1, 200),
        "cost":     np.random.lognormal(9, 1, 200),
        "churn":    np.random.uniform(0, 10, 200),
        "segment":  np.random.choice(["A", "B", "C", "D"], 200),
        "country":  np.random.choice(["BR", "AR", "MX", None], 200),
        "date":     pd.date_range("2022-01-01", periods=200, freq="D"),
    })


class TestClean:
    def test_clean_returns_bizframe(self, df):
        out = bai.clean(df)
        assert isinstance(out, bai.BizFrame)

    def test_clean_fixes_column_names(self):
        d = pd.DataFrame({"My Column": [1], "AnotherCol": [2], "  Spaced  ": [3]})
        out = bai.clean(d)
        assert "my_column" in out.columns
        assert "another_col" in out.columns

    def test_fill_missing_median(self):
        d = pd.DataFrame({"x": [1, 2, None, 4, 5]})
        out = bai.fill_missing(d, strategy="median")
        assert out["x"].isna().sum() == 0
        assert out.loc[2, "x"] == pytest.approx(3.0)

    def test_fill_missing_ffill(self):
        d = pd.DataFrame({"x": [1.0, None, None, 4.0]})
        out = bai.fill_missing(d, strategy="ffill")
        assert out["x"].isna().sum() == 0

    def test_remove_outliers_clip(self, df):
        out = bai.remove_outliers(df, cols=["revenue"], action="clip")
        assert len(out) == len(df)  # clip doesn't drop rows

    def test_remove_outliers_drop(self, df):
        out = bai.remove_outliers(df, cols=["revenue"], action="drop")
        assert len(out) < len(df)

    def test_normalize_minmax(self, df):
        out = bai.normalize(df, cols=["revenue", "cost"], method="minmax")
        assert "revenue_norm" in out.columns
        assert out["revenue_norm"].min() >= 0.0 - 1e-9
        assert out["revenue_norm"].max() <= 1.0 + 1e-9

    def test_normalize_zscore(self, df):
        out = bai.normalize(df, cols=["revenue"], method="zscore")
        assert "revenue_norm" in out.columns
        assert abs(out["revenue_norm"].mean()) < 0.1

    def test_encode_label(self, df):
        out = bai.encode(df, cols=["segment"], method="label")
        assert "segment_enc" in out.columns
        assert out["segment_enc"].dtype in [np.int64, np.int32, object]

    def test_encode_onehot(self, df):
        out = bai.encode(df, cols=["segment"], method="onehot")
        assert any("segment_" in c for c in out.columns)


class TestStats:
    def test_describe_returns_result(self, df):
        result = stats.describe(df)
        assert len(result.numeric_cols) >= 2
        assert isinstance(result.summary, pd.DataFrame)

    def test_describe_insights(self, df):
        result = stats.describe(df, include_insights=True)
        assert isinstance(result.insights, list)

    def test_correlation_matrix(self, df):
        corr = stats.correlation_matrix(df)
        assert isinstance(corr, pd.DataFrame)
        num_count = len(df.select_dtypes(include="number").columns)
        assert corr.shape == (num_count, num_count)

    def test_correlation_threshold(self, df):
        pairs = stats.correlation_matrix(df, threshold=0.5)
        assert "col_a" in pairs.columns
        assert "correlation" in pairs.columns
        assert (pairs["correlation"].abs() >= 0.5).all()

    def test_percentiles(self, df):
        p = stats.percentiles(df)
        assert "p50" in p.index
        assert "p90" in p.index

    def test_group_stats(self, df):
        gs = stats.group_stats(df, "segment", "revenue")
        assert "segment" in gs.columns
        assert "mean" in gs.columns

    def test_distribution(self, df):
        d = stats.distribution(df["revenue"], bins=20)
        assert "bins" in d
        assert "normality_test" in d
        assert len(d["bins"]) == 20

    def test_compare_groups(self, df):
        result = stats.compare_groups(df, "segment", "revenue", test="anova")
        assert "p_value" in result
        assert "significant" in result
        assert isinstance(result["significant"], bool)


class TestML:
    def test_cluster_adds_column(self, df):
        out = ml.cluster(df, cols=["revenue", "cost"], n_clusters=3)
        assert "cluster" in out.columns
        assert out["cluster"].nunique() <= 3

    def test_cluster_full(self, df):
        result = ml.cluster_full(df, cols=["revenue", "cost"], n_clusters=3)
        assert isinstance(result, ml.ClusterResult)
        assert result.n_clusters == 3
        assert -1 <= result.silhouette <= 1

    def test_find_optimal_k(self, df):
        result = ml.find_optimal_k(df, cols=["revenue", "cost"], k_range=(2, 5))
        assert len(result) == 4
        assert "recommended" in result.columns
        assert result["recommended"].sum() == 1

    def test_rfm_segments(self):
        np.random.seed(42)
        df = pd.DataFrame({
            "customer_id": np.repeat(range(50), 10),
            "date": pd.date_range("2022-01-01", periods=500, freq="7D"),
            "revenue": np.random.uniform(50, 500, 500),
        })
        rfm = ml.rfm_segments(df, "customer_id", "date", "revenue")
        assert "segment" in rfm.columns
        assert set(rfm.columns).issuperset({"recency", "frequency", "monetary"})

    def test_detect_anomalies_isolation_forest(self, df):
        result = ml.detect_anomalies(df, cols=["revenue", "cost"], method="isolation_forest", contamination=0.05)
        assert isinstance(result, ml.AnomalyResult)
        assert result.n_anomalies > 0
        assert "is_anomaly" in result.df.columns

    def test_detect_anomalies_iqr(self, df):
        result = ml.detect_anomalies(df, cols=["revenue"], method="iqr")
        assert "anomaly_score" in result.df.columns

    def test_anomaly_subset(self, df):
        result = ml.detect_anomalies(df, cols=["revenue"], method="isolation_forest", contamination=0.1)
        anomalies = result.anomalies()
        assert len(anomalies) == result.n_anomalies
        assert (anomalies["is_anomaly"] == True).all()
