//! Test that "on-demand" input pattern works.
//!
//! On-demand inputs are inputs computed lazily on the fly. They are simulated
//! via a b query with zero inputs, which uses `add_synthetic_read` to
//! tweak durability and `invalidate` to clear the input.

#![allow(clippy::disallowed_types, clippy::type_complexity)]

use std::{cell::RefCell, collections::HashMap, rc::Rc};

use salsa::{Database as _, Durability, EventKind};

#[salsa::query_group(QueryGroupStorage)]
trait QueryGroup: salsa::Database + AsRef<HashMap<u32, u32>> {
    fn a(&self, x: u32) -> u32;
    fn b(&self, x: u32) -> u32;
    fn c(&self, x: u32) -> u32;
}

fn a(db: &dyn QueryGroup, x: u32) -> u32 {
    let durability = if x % 2 == 0 { Durability::LOW } else { Durability::HIGH };
    db.salsa_runtime().report_synthetic_read(durability);
    let external_state: &HashMap<u32, u32> = db.as_ref();
    external_state[&x]
}

fn b(db: &dyn QueryGroup, x: u32) -> u32 {
    db.a(x)
}

fn c(db: &dyn QueryGroup, x: u32) -> u32 {
    db.b(x)
}

#[salsa::database(QueryGroupStorage)]
#[derive(Default)]
struct Database {
    storage: salsa::Storage<Self>,
    external_state: HashMap<u32, u32>,
    on_event: Option<Box<dyn Fn(&Database, salsa::Event)>>,
}

impl salsa::Database for Database {
    fn salsa_event(&self, event: salsa::Event) {
        if let Some(cb) = &self.on_event {
            cb(self, event)
        }
    }
}

impl AsRef<HashMap<u32, u32>> for Database {
    fn as_ref(&self) -> &HashMap<u32, u32> {
        &self.external_state
    }
}

#[test]
fn on_demand_input_works() {
    let mut db = Database::default();

    db.external_state.insert(1, 10);
    assert_eq!(db.b(1), 10);
    assert_eq!(db.a(1), 10);

    // We changed external state, but haven't signaled about this yet,
    // so we expect to see the old answer
    db.external_state.insert(1, 92);
    assert_eq!(db.b(1), 10);
    assert_eq!(db.a(1), 10);

    AQuery.in_db_mut(&mut db).invalidate(&1);
    assert_eq!(db.b(1), 92);
    assert_eq!(db.a(1), 92);

    // Downstream queries should also be rerun if we call `a` first.
    db.external_state.insert(1, 50);
    AQuery.in_db_mut(&mut db).invalidate(&1);
    assert_eq!(db.a(1), 50);
    assert_eq!(db.b(1), 50);
}

#[test]
fn on_demand_input_durability() {
    let mut db = Database::default();

    let events = Rc::new(RefCell::new(vec![]));
    db.on_event = Some(Box::new({
        let events = events.clone();
        move |db, event| {
            if let EventKind::WillCheckCancellation = event.kind {
                // these events are not interesting
            } else {
                events.borrow_mut().push(format!("{:?}", event.debug(db)))
            }
        }
    }));

    events.replace(vec![]);
    db.external_state.insert(1, 10);
    db.external_state.insert(2, 20);
    assert_eq!(db.b(1), 10);
    assert_eq!(db.b(2), 20);
    expect_test::expect![[r#"
        RefCell {
            value: [
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::BQuery::b(1) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::AQuery::a(1) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::BQuery::b(2) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::AQuery::a(2) } }",
            ],
        }
    "#]].assert_debug_eq(&events);

    db.synthetic_write(Durability::LOW);
    events.replace(vec![]);
    assert_eq!(db.c(1), 10);
    assert_eq!(db.c(2), 20);
    // Re-execute `a(2)` because that has low durability, but not `a(1)`
    expect_test::expect![[r#"
        RefCell {
            value: [
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::CQuery::c(1) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: on_demand_inputs::BQuery::b(1) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::CQuery::c(2) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::AQuery::a(2) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: on_demand_inputs::BQuery::b(2) } }",
            ],
        }
    "#]].assert_debug_eq(&events);

    db.synthetic_write(Durability::HIGH);
    events.replace(vec![]);
    assert_eq!(db.c(1), 10);
    assert_eq!(db.c(2), 20);
    // Re-execute both `a(1)` and `a(2)`, but we don't re-execute any `b` queries as the
    // result didn't actually change.
    expect_test::expect![[r#"
        RefCell {
            value: [
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::AQuery::a(1) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: on_demand_inputs::CQuery::c(1) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillExecute { database_key: on_demand_inputs::AQuery::a(2) } }",
                "Event { runtime_id: RuntimeId { counter: 0 }, kind: DidValidateMemoizedValue { database_key: on_demand_inputs::CQuery::c(2) } }",
            ],
        }
    "#]].assert_debug_eq(&events);
}
