use mongreldb_core::procedure::{
    ProcedureBody, ProcedureCallOutput, ProcedureMode, ProcedureParam, ProcedureStep,
    ProcedureValue, StoredProcedure,
};
use mongreldb_core::{
    ColumnDef, ColumnFlags, Database, IndexDef, IndexKind, Schema, TypeId, Value,
};
use std::collections::HashMap;
use tempfile::tempdir;

#[test]
fn read_only_procedure_returns_native_query_rows() {
    let (_dir, db) = seeded_db();
    db.create_procedure(read_users_procedure("read_users"))
        .unwrap();

    let result = db.call_procedure("read_users", HashMap::new()).unwrap();

    assert_eq!(result.epoch, None);
    let ProcedureCallOutput::Rows(rows) = result.output else {
        panic!("expected rows");
    };
    assert_eq!(rows.len(), 1);
    assert_eq!(rows[0].columns.get(&1), Some(&Value::Int64(1)));
    assert_eq!(
        rows[0].columns.get(&2),
        Some(&Value::Bytes(b"active".to_vec()))
    );
}

#[test]
fn read_write_procedure_commits_put_once() {
    let dir = tempdir().unwrap();
    let db = Database::create(dir.path()).unwrap();
    db.create_table("users", users_schema()).unwrap();
    db.create_procedure(insert_user_procedure("insert_user"))
        .unwrap();

    let mut args = HashMap::new();
    args.insert("id".into(), Value::Int64(7));
    args.insert("status".into(), Value::Bytes(b"active".to_vec()));
    let result = db.call_procedure("insert_user", args).unwrap();

    assert!(result.epoch.is_some());
    let ProcedureCallOutput::Row(row) = result.output else {
        panic!("expected row");
    };
    assert_eq!(row.columns.get(&1), Some(&Value::Int64(7)));
    assert_eq!(db.table("users").unwrap().lock().count(), 1);
}

fn seeded_db() -> (tempfile::TempDir, Database) {
    let dir = tempdir().unwrap();
    let db = Database::create(dir.path()).unwrap();
    db.create_table("users", users_schema()).unwrap();
    {
        let mut tx = db.begin();
        tx.put(
            "users",
            vec![(1, Value::Int64(1)), (2, Value::Bytes(b"active".to_vec()))],
        )
        .unwrap();
        tx.commit().unwrap();
    }
    (dir, db)
}

fn users_schema() -> Schema {
    Schema {
        schema_id: 1,
        columns: vec![
            ColumnDef {
                id: 1,
                name: "id".into(),
                ty: TypeId::Int64,
                flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
                default_value: None,
            },
            ColumnDef {
                id: 2,
                name: "status".into(),
                ty: TypeId::Bytes,
                flags: ColumnFlags::empty(),
                default_value: None,
            },
        ],
        indexes: vec![
            IndexDef {
                name: "pk".into(),
                column_id: 1,
                kind: IndexKind::Bitmap,
                predicate: None,
            },
            IndexDef {
                name: "status_idx".into(),
                column_id: 2,
                kind: IndexKind::Bitmap,
                predicate: None,
            },
        ],
        colocation: Vec::new(),
        constraints: Default::default(),
        clustered: false,
    }
}

fn read_users_procedure(name: &str) -> StoredProcedure {
    StoredProcedure::new(
        name,
        ProcedureMode::ReadOnly,
        Vec::new(),
        ProcedureBody {
            steps: vec![ProcedureStep::NativeQuery {
                id: "read".into(),
                table: "users".into(),
                conditions: Vec::new(),
                projection: Some(vec![1, 2]),
                limit: Some(10),
            }],
            return_value: ProcedureValue::StepRows("read".into()),
        },
        0,
    )
    .unwrap()
}

fn insert_user_procedure(name: &str) -> StoredProcedure {
    StoredProcedure::new(
        name,
        ProcedureMode::ReadWrite,
        vec![
            ProcedureParam {
                name: "id".into(),
                ty: TypeId::Int64,
                nullable: false,
                default: None,
            },
            ProcedureParam {
                name: "status".into(),
                ty: TypeId::Bytes,
                nullable: false,
                default: None,
            },
        ],
        ProcedureBody {
            steps: vec![ProcedureStep::Put {
                id: "put".into(),
                table: "users".into(),
                cells: vec![
                    mongreldb_core::procedure::ProcedureCell {
                        column_id: 1,
                        value: ProcedureValue::Param("id".into()),
                    },
                    mongreldb_core::procedure::ProcedureCell {
                        column_id: 2,
                        value: ProcedureValue::Param("status".into()),
                    },
                ],
                returning: true,
            }],
            return_value: ProcedureValue::StepRow("put".into()),
        },
        0,
    )
    .unwrap()
}
