"""CLI smoke tests via typer.testing.CliRunner."""

import os
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd
import pytest
from typer.testing import CliRunner

from directrs.cli import app

runner = CliRunner()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_classification_csv(path: str, n: int = 120, n_features: int = 4) -> None:
    rng = np.random.default_rng(42)
    X = rng.standard_normal((n, n_features))
    y = rng.integers(0, 3, size=n)
    df = pd.DataFrame(X, columns=[f"f{i}" for i in range(n_features)])
    df["target"] = y
    df.to_csv(path, index=False)


def _make_regression_csv(path: str, n: int = 120, n_features: int = 5) -> None:
    rng = np.random.default_rng(42)
    X = rng.standard_normal((n, n_features))
    y = X[:, 0] * 2 + X[:, 1] - X[:, 2] + rng.standard_normal(n) * 0.5
    df = pd.DataFrame(X, columns=[f"f{i}" for i in range(n_features)])
    df["target"] = y
    df.to_csv(path, index=False)


# ---------------------------------------------------------------------------
# train — classification
# ---------------------------------------------------------------------------

class TestTrainClassification:
    def test_train_exits_zero(self, tmp_path):
        csv = tmp_path / "clf_data.csv"
        _make_classification_csv(str(csv))
        result = runner.invoke(
            app,
            [
                "train",
                "--task", "classification",
                "--input", str(csv),
                "--target", "target",
                "--output-dir", str(tmp_path / "out"),
                "--output-model", str(tmp_path / "model.pkl"),
                "--max-depth", "3",
                "--random-state", "0",
            ],
        )
        assert result.exit_code == 0, f"CLI failed:\n{result.output}\n{result.exception}"

    def test_train_creates_model_file(self, tmp_path):
        csv = tmp_path / "clf_data.csv"
        _make_classification_csv(str(csv))
        model_path = tmp_path / "model.pkl"
        runner.invoke(
            app,
            [
                "train", "--task", "classification",
                "--input", str(csv), "--target", "target",
                "--output-dir", str(tmp_path / "out"),
                "--output-model", str(model_path),
            ],
        )
        assert model_path.exists(), "model.pkl not created"

    def test_train_creates_plots(self, tmp_path):
        csv = tmp_path / "clf_data.csv"
        _make_classification_csv(str(csv))
        out_dir = tmp_path / "out"
        runner.invoke(
            app,
            [
                "train", "--task", "classification",
                "--input", str(csv), "--target", "target",
                "--output-dir", str(out_dir),
                "--output-model", str(tmp_path / "m.pkl"),
            ],
        )
        expected_plots = [
            "g_matrix.png",
            "stretch_spectrum.png",
            "feature_loadings.png",
            "feature_importance.png",
            "embedding_scatter.png",
            "train_predictions.png",
            "test_predictions.png",
            "metrics_comparison.png",
        ]
        for plot in expected_plots:
            assert (out_dir / plot).exists(), f"Missing plot: {plot}"

    def test_train_creates_csv_tables(self, tmp_path):
        csv = tmp_path / "clf_data.csv"
        _make_classification_csv(str(csv))
        out_dir = tmp_path / "out"
        runner.invoke(
            app,
            [
                "train", "--task", "classification",
                "--input", str(csv), "--target", "target",
                "--output-dir", str(out_dir),
                "--output-model", str(tmp_path / "m.pkl"),
            ],
        )
        for fname in ["train_metrics.csv", "test_metrics.csv",
                      "predictions_train.csv", "predictions_test.csv"]:
            assert (out_dir / fname).exists(), f"Missing table: {fname}"


# ---------------------------------------------------------------------------
# train — regression
# ---------------------------------------------------------------------------

class TestTrainRegression:
    def test_train_exits_zero(self, tmp_path):
        csv = tmp_path / "reg_data.csv"
        _make_regression_csv(str(csv))
        result = runner.invoke(
            app,
            [
                "train",
                "--task", "regression",
                "--input", str(csv),
                "--target", "target",
                "--output-dir", str(tmp_path / "out"),
                "--output-model", str(tmp_path / "model.pkl"),
                "--max-depth", "3",
                "--random-state", "0",
            ],
        )
        assert result.exit_code == 0, f"CLI failed:\n{result.output}\n{result.exception}"

    def test_train_creates_model_and_plots(self, tmp_path):
        csv = tmp_path / "reg_data.csv"
        _make_regression_csv(str(csv))
        out_dir = tmp_path / "out"
        model_path = tmp_path / "model.pkl"
        runner.invoke(
            app,
            [
                "train", "--task", "regression",
                "--input", str(csv), "--target", "target",
                "--output-dir", str(out_dir),
                "--output-model", str(model_path),
            ],
        )
        assert model_path.exists()
        assert (out_dir / "g_matrix.png").exists()
        assert (out_dir / "train_predictions.png").exists()


# ---------------------------------------------------------------------------
# predict
# ---------------------------------------------------------------------------

class TestPredict:
    @pytest.fixture
    def trained_clf_bundle(self, tmp_path):
        """Train a classifier and return (model_path, csv_path, tmp_path)."""
        csv = tmp_path / "clf_data.csv"
        _make_classification_csv(str(csv))
        model_path = tmp_path / "model.pkl"
        runner.invoke(
            app,
            [
                "train", "--task", "classification",
                "--input", str(csv), "--target", "target",
                "--output-dir", str(tmp_path / "out"),
                "--output-model", str(model_path),
            ],
        )
        return model_path, csv, tmp_path

    def test_predict_exits_zero(self, trained_clf_bundle):
        model_path, csv, tmp_path = trained_clf_bundle
        output_csv = tmp_path / "predictions.csv"
        result = runner.invoke(
            app,
            [
                "predict",
                "--model", str(model_path),
                "--input", str(csv),
                "--output", str(output_csv),
            ],
        )
        assert result.exit_code == 0, f"predict failed:\n{result.output}\n{result.exception}"

    def test_predict_creates_output_file(self, trained_clf_bundle):
        model_path, csv, tmp_path = trained_clf_bundle
        output_csv = tmp_path / "preds.csv"
        runner.invoke(
            app,
            [
                "predict",
                "--model", str(model_path),
                "--input", str(csv),
                "--output", str(output_csv),
            ],
        )
        assert output_csv.exists(), "Output CSV not created"

    def test_predict_output_has_y_pred_column(self, trained_clf_bundle):
        model_path, csv, tmp_path = trained_clf_bundle
        output_csv = tmp_path / "preds2.csv"
        runner.invoke(
            app,
            [
                "predict",
                "--model", str(model_path),
                "--input", str(csv),
                "--output", str(output_csv),
            ],
        )
        df = pd.read_csv(output_csv)
        assert "y_pred" in df.columns

    def test_predict_regression_output(self, tmp_path):
        """End-to-end test for regression predict."""
        csv = tmp_path / "reg.csv"
        _make_regression_csv(str(csv))
        model_path = tmp_path / "reg_model.pkl"
        runner.invoke(
            app,
            [
                "train", "--task", "regression",
                "--input", str(csv), "--target", "target",
                "--output-dir", str(tmp_path / "out"),
                "--output-model", str(model_path),
            ],
        )
        output_csv = tmp_path / "reg_preds.csv"
        result = runner.invoke(
            app,
            [
                "predict",
                "--model", str(model_path),
                "--input", str(csv),
                "--output", str(output_csv),
            ],
        )
        assert result.exit_code == 0
        df = pd.read_csv(output_csv)
        assert "y_pred" in df.columns
        assert df["y_pred"].dtype.kind == "f"


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------

def test_train_invalid_task(tmp_path):
    csv = tmp_path / "data.csv"
    _make_classification_csv(str(csv))
    result = runner.invoke(
        app,
        [
            "train", "--task", "invalid_task",
            "--input", str(csv), "--target", "target",
            "--output-dir", str(tmp_path / "out"),
        ],
    )
    assert result.exit_code != 0 or "error" in result.output.lower()


def test_train_missing_target_column(tmp_path):
    csv = tmp_path / "data.csv"
    _make_classification_csv(str(csv))
    result = runner.invoke(
        app,
        [
            "train", "--task", "classification",
            "--input", str(csv), "--target", "nonexistent_col",
            "--output-dir", str(tmp_path / "out"),
        ],
    )
    assert result.exit_code != 0 or "not found" in result.output.lower()
