use crate::ast::expr::UnaryOp;
use crate::executor::{EvaluationError, ExecutorError, Result};
use crate::planner::typed_expr::TypedExpr;
use crate::storage::SqlValue;

use super::evaluate;

pub fn eval_unary_op(
    op: &UnaryOp,
    operand: &TypedExpr,
    ctx: &super::EvalContext<'_>,
) -> Result<SqlValue> {
    let value = evaluate(operand, ctx)?;
    match op {
        UnaryOp::Not => eval_not(value),
        UnaryOp::Minus => eval_minus(value),
    }
}

fn eval_not(value: SqlValue) -> Result<SqlValue> {
    match value {
        SqlValue::Null => Ok(SqlValue::Null),
        SqlValue::Boolean(b) => Ok(SqlValue::Boolean(!b)),
        other => Err(ExecutorError::Evaluation(EvaluationError::TypeMismatch {
            expected: "Boolean".into(),
            actual: other.type_name().into(),
        })),
    }
}

fn eval_minus(value: SqlValue) -> Result<SqlValue> {
    match value {
        SqlValue::Null => Ok(SqlValue::Null),
        SqlValue::Integer(v) => v
            .checked_neg()
            .map(SqlValue::Integer)
            .ok_or(ExecutorError::Evaluation(EvaluationError::Overflow)),
        SqlValue::BigInt(v) => v
            .checked_neg()
            .map(SqlValue::BigInt)
            .ok_or(ExecutorError::Evaluation(EvaluationError::Overflow)),
        SqlValue::Float(v) => Ok(SqlValue::Float(-v)),
        SqlValue::Double(v) => Ok(SqlValue::Double(-v)),
        other => Err(ExecutorError::Evaluation(EvaluationError::TypeMismatch {
            expected: "Numeric".into(),
            actual: other.type_name().into(),
        })),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn not_null_passthrough() {
        assert_eq!(eval_not(SqlValue::Null).unwrap(), SqlValue::Null);
    }

    #[test]
    fn minus_overflow() {
        let err = eval_minus(SqlValue::Integer(i32::MIN)).unwrap_err();
        assert!(matches!(
            err,
            ExecutorError::Evaluation(EvaluationError::Overflow)
        ));
    }
}
