use tuphium::autograd::Value;
use tuphium::activations::Activation;
use tuphium::error::TuphiumError;

#[test]
fn test_add() {
    let a = Value::new(2.0).unwrap();
    let b = Value::new(3.0).unwrap();
    let c = &a + &b;
    assert_eq!(c.data(), 5.0);
    c.backward();
    assert_eq!(a.grad(), 1.0);
    assert_eq!(b.grad(), 1.0);
}

#[test]
fn test_mul() {
    let a = Value::new(2.0).unwrap();
    let b = Value::new(3.0).unwrap();
    let c = &a * &b;
    assert_eq!(c.data(), 6.0);
    c.backward();
    assert_eq!(a.grad(), 3.0);
    assert_eq!(b.grad(), 2.0);
}

#[test]
fn test_sigmoid() {
    let a = Value::new(0.0).unwrap();
    let b = a.sigmoid();
    assert_eq!(b.data(), 0.5);
    b.backward();
    assert_eq!(a.grad(), 0.25);
}

#[test]
fn test_relu_positive_and_negative() {
    let positive = Value::new(2.0).unwrap();
    let relu = positive.relu();
    assert_eq!(relu.data(), 2.0);
    relu.backward();
    assert_eq!(positive.grad(), 1.0);

    let negative = Value::new(-2.0).unwrap();
    let relu = negative.relu();
    assert_eq!(relu.data(), 0.0);
    relu.backward();
    assert_eq!(negative.grad(), 0.0);
}

#[test]
fn test_chain() {
    let x = Value::new(1.0).unwrap();
    let y = x.sigmoid();
    let z = y * Value::new(2.0).unwrap();
    let w = z + Value::new(1.0).unwrap();
    w.backward();
    assert!((x.grad() - (0.7310586f32 * (1.0 - 0.7310586f32) * 2.0)).abs() < 1e-5);
}

#[test]
fn test_zero_grad() {
    let a = Value::new(2.0).unwrap();
    let b = Value::new(3.0).unwrap();
    let c = &a * &b;
    c.backward();
    assert_eq!(a.grad(), 3.0);
    c.zero_grad();
    assert_eq!(a.grad(), 0.0);
    assert_eq!(b.grad(), 0.0);
}

#[test]
fn test_pow_neg_sub_div_activate() {
    let a = Value::new(3.0).unwrap();
    let b = a.pow(2.0).unwrap();
    assert_eq!(b.data(), 9.0);
    b.backward();
    assert_eq!(a.grad(), 6.0);

    let a = Value::new(5.0).unwrap();
    let b = -&a;
    assert_eq!(b.data(), -5.0);
    b.backward();
    assert_eq!(a.grad(), -1.0);

    let a = Value::new(10.0).unwrap();
    let b = Value::new(3.0).unwrap();
    let c = &a - &b;
    assert_eq!(c.data(), 7.0);
    c.backward();
    assert_eq!(a.grad(), 1.0);
    assert_eq!(b.grad(), -1.0);

    let a = Value::new(10.0).unwrap();
    let b = Value::new(2.0).unwrap();
    let c = &a / &b;
    assert_eq!(c.data(), 5.0);
    c.backward();
    assert_eq!(a.grad(), 0.5);
    assert_eq!(b.grad(), -2.5);

    let a = Value::new(-1.0).unwrap();
    let b = a.activate(&Activation::ReLU);
    assert_eq!(b.data(), 0.0);
}

#[test]
fn new_rejects_non_finite_value() {
    assert!(matches!(
        Value::new(f32::NAN).unwrap_err(),
        TuphiumError::NonFiniteValue {
            operation: "Value::new",
            ..
        }
    ));
}

#[test]
fn div_returns_division_by_zero() {
    let a = Value::new(1.0).unwrap();
    let b = Value::new(0.0).unwrap();
    assert_eq!(
        a.div(&b).unwrap_err(),
        TuphiumError::DivisionByZero {
            operation: "Value::div",
            numerator: 1.0,
        }
    );
}

#[test]
fn log_returns_numeric_domain_error() {
    let value = Value::new(0.0).unwrap();
    assert_eq!(
        value.log().unwrap_err(),
        TuphiumError::NumericDomain {
            operation: "Value::log",
            input: 0.0,
            reason: "logarithm requires a positive input",
        }
    );
}
