"""
Tests for Laboratory 1: Algorithm & Technology Research Lab
Validates all implemented optimization, predictive, search, planning, and anomaly algorithms.
"""

import numpy as np
import pytest

from app.lab_algorithms.algorithms.optimization import (
    GeneticAlgorithmOptimizer,
    BayesianSurrogateOptimizer,
    SimulatedAnnealingOptimizer,
    ScipyMathematicalOptimizer
)
from app.lab_algorithms.algorithms.predictive import (
    RidgePredictor,
    GradientBoostingPredictor,
    KNNPredictor,
    ExponentialSmoothingPredictor
)
from app.lab_algorithms.algorithms.search_planning import (
    AStarSearch,
    DijkstraSearch,
    CriticalPathPlanner
)
from app.lab_algorithms.algorithms.anomaly import (
    IsolationForestAnomalyDetector,
    RollingZScoreAnomalyDetector
)
from app.lab_algorithms.engine import AlgorithmBenchmarkingEngine


def test_genetic_algorithm_optimizer():
    ga = GeneticAlgorithmOptimizer(population_size=20, generations=15, random_seed=42)
    # Maximize negative sphere: max is 0 at x=0
    def sphere(x): return -float(np.sum(x**2))
    res = ga.optimize(sphere, dim=3)

    assert res["best_fitness"] > -5.0
    assert len(res["convergence_history"]) == 16


def test_bayesian_surrogate_optimizer():
    bo = BayesianSurrogateOptimizer(n_init_samples=4, n_iterations=8, random_seed=42)
    def sphere(x): return -float(np.sum(x**2))
    res = bo.optimize(sphere, dim=2)

    assert res["total_evaluations"] == 12
    assert res["best_fitness"] is not None


def test_simulated_annealing_optimizer():
    sa = SimulatedAnnealingOptimizer(initial_temp=20.0, cooling_rate=0.9, iterations_per_temp=5, random_seed=42)
    def sphere(x): return -float(np.sum(x**2))
    res = sa.optimize(sphere, dim=2)

    assert res["best_fitness"] is not None
    assert len(res["best_solution"]) == 2


def test_scipy_mathematical_optimizer():
    # LP: Minimize -x0 - 2x1 subject to x0 + x1 <= 10, x0 >= 0, x1 >= 0
    c = [-1.0, -2.0]
    A_ub = [[1.0, 1.0]]
    b_ub = [10.0]
    bounds = [(0, None), (0, None)]

    res = ScipyMathematicalOptimizer.solve_linear_program(c, A_ub, b_ub, bounds)
    assert res["success"] is True
    assert pytest.approx(res["optimal_value"], 0.01) == -20.0

    # Non-linear minimum: Rosenbrock
    def rosenbrock(x): return (1.0 - x[0])**2 + 100.0 * (x[1] - x[0]**2)**2
    res_nl = ScipyMathematicalOptimizer.solve_nonlinear_minimum(rosenbrock, x0=[0.0, 0.0])
    assert res_nl["success"] is True
    assert abs(res_nl["optimal_value"] - 0.0) < 0.01


def test_predictive_algorithms():
    X_train = np.array([[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]])
    y_train = np.array([3.0, 5.0, 7.0, 9.0])
    X_test = np.array([[5.0, 6.0], [6.0, 7.0]])
    y_test = np.array([11.0, 13.0])

    # Ridge (multi-sample test set -> R^2 is well-defined)
    ridge = RidgePredictor()
    m_ridge = ridge.train_and_eval(X_train, y_train, X_test, y_test)
    assert "mse" in m_ridge
    assert m_ridge["r2"] is not None
    assert m_ridge["r2_status"] == "DEFINED"

    # Single-sample defensive check: R^2 is explicitly None (no UndefinedMetricWarning)
    m_ridge_single = ridge.train_and_eval(X_train, y_train, X_test[:1], y_test[:1])
    assert m_ridge_single["r2"] is None
    assert m_ridge_single["r2_status"] == "UNDEFINED_LESS_THAN_2_SAMPLES"

    # Gradient Boosting (multi-sample test set -> R^2 is well-defined)
    gb = GradientBoostingPredictor(n_estimators=10)
    m_gb = gb.train_and_eval(X_train, y_train, X_test, y_test)
    assert "mse" in m_gb
    assert m_gb["r2"] is not None
    assert m_gb["r2_status"] == "DEFINED"

    # Exponential smoothing
    series = np.array([10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0])
    exp_smooth = ExponentialSmoothingPredictor(alpha=0.4, beta=0.2)
    m_es = exp_smooth.forecast_and_eval(series, test_horizon=2)
    assert len(m_es["forecast"]) == 2


def test_search_and_planning():
    # A* Search
    grid = [
        [0, 0, 0],
        [1, 1, 0],
        [0, 0, 0]
    ]
    res_astar = AStarSearch.search(grid, (0, 0), (2, 0))
    assert res_astar["path_found"] is True
    assert res_astar["path_length"] == 7

    # Dijkstra
    graph = {
        "A": [("B", 1.0), ("C", 4.0)],
        "B": [("C", 2.0), ("D", 5.0)],
        "C": [("D", 1.0)],
        "D": []
    }
    res_dijkstra = DijkstraSearch.search(graph, "A", "D")
    assert res_dijkstra["path_found"] is True
    assert res_dijkstra["shortest_distance"] == 4.0

    # Critical Path Method
    tasks = {
        "T1": {"duration": 3, "predecessors": []},
        "T2": {"duration": 4, "predecessors": ["T1"]},
        "T3": {"duration": 2, "predecessors": ["T1"]},
        "T4": {"duration": 3, "predecessors": ["T2", "T3"]}
    }
    res_cpm = CriticalPathPlanner.plan(tasks)
    assert res_cpm["total_project_duration"] == 10  # T1 (3) + T2 (4) + T4 (3) = 10
    assert "T2" in res_cpm["critical_path"]


def test_anomaly_detection():
    rng = np.random.RandomState(42)
    normal_data = rng.randn(100, 2)
    outliers = rng.uniform(10, 15, size=(5, 2))
    X = np.vstack([normal_data, outliers])

    iso = IsolationForestAnomalyDetector(contamination=0.05)
    res_iso = iso.detect(X)
    assert res_iso["anomalies_detected"] >= 4

    # Rolling Z-score
    series = np.array([1.0, 1.1, 0.9, 1.0, 1.2, 10.5, 1.1, 0.9])
    z_det = RollingZScoreAnomalyDetector(window_size=4, z_threshold=2.0)
    res_z = z_det.detect_series(series)
    assert 5 in res_z["anomaly_indices"]


def test_algorithm_benchmarking_engine():
    res = AlgorithmBenchmarkingEngine.benchmark_optimization(
        algorithms=["genetic_algorithm", "simulated_annealing"],
        problem_type="sphere",
        dim=3
    )
    assert len(res["runs"]) == 2
    assert res["top_candidate"] in ["genetic_algorithm", "simulated_annealing"]
    assert res["runs"][0]["outcome"] == "BASELINE"
