//! Test helpers for working with Salsa databases

use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;

pub fn assert_function_query_was_not_run<Db, Q, QDb, I, R>(
    db: &Db,
    query: Q,
    input: I,
    events: &[salsa::Event],
) where
    Db: salsa::Database,
    Q: Fn(QDb, I) -> R,
    I: salsa::plumbing::AsId + std::fmt::Debug + Copy,
{
    let id = input.as_id();
    let (query_name, will_execute_event) = find_will_execute_event(db, query, input, events);

    db.attach(|_| {
        if let Some(will_execute_event) = will_execute_event {
            panic!("Expected query {query_name}({id:?}) not to have run but it did: {will_execute_event:?}\n\n{events:#?}");
        }
    });
}

pub fn assert_const_function_query_was_not_run<Db, Q, QDb, R>(
    db: &Db,
    query: Q,
    events: &[salsa::Event],
) where
    Db: salsa::Database,
    Q: Fn(QDb) -> R,
{
    // Salsa now interns singleton ingredients. But we know that it is a singleton, so we can just search for
    // any event of that ingredient.
    let query_name = query_name(&query);

    let event = events.iter().find(|event| {
        if let salsa::EventKind::WillExecute { database_key } = event.kind {
            db.ingredient_debug_name(database_key.ingredient_index()) == query_name
        } else {
            false
        }
    });

    db.attach(|_| {
        if let Some(will_execute_event) = event {
            panic!(
                "Expected query {query_name}() not to have run but it did: {will_execute_event:?}\n\n{events:#?}"
            );
        }
    });
}

/// Assert that the Salsa query described by the generic parameter `C`
/// was executed at least once with the input `input`
/// in the history span represented by `events`.
pub fn assert_function_query_was_run<Db, Q, QDb, I, R>(
    db: &Db,
    query: Q,
    input: I,
    events: &[salsa::Event],
) where
    Db: salsa::Database,
    Q: Fn(QDb, I) -> R,
    I: salsa::plumbing::AsId + std::fmt::Debug + Copy,
{
    let id = input.as_id();
    let (query_name, will_execute_event) = find_will_execute_event(db, query, input, events);

    db.attach(|_| {
        assert!(
            will_execute_event.is_some(),
            "Expected query {query_name}({id:?}) to have run but it did not:\n{events:#?}"
        );
    });
}

pub fn find_will_execute_event<'a, Q, I>(
    db: &dyn salsa::Database,
    query: Q,
    input: I,
    events: &'a [salsa::Event],
) -> (&'static str, Option<&'a salsa::Event>)
where
    I: salsa::plumbing::AsId,
{
    let query_name = query_name(&query);

    let event = events.iter().find(|event| {
        if let salsa::EventKind::WillExecute { database_key } = event.kind {
            db.ingredient_debug_name(database_key.ingredient_index()) == query_name
                && database_key.key_index() == input.as_id()
        } else {
            false
        }
    });

    (query_name, event)
}

fn query_name<Q>(_query: &Q) -> &'static str {
    let full_qualified_query_name = std::any::type_name::<Q>();
    full_qualified_query_name
        .rsplit_once("::")
        .map(|(_, name)| name)
        .unwrap_or(full_qualified_query_name)
}

/// Sets up logging for the current thread. It captures all `ty` and `ruff` events.
///
/// Useful for capturing the tracing output in a failing test.
///
/// # Examples
/// ```
/// use ruff_db::testing::setup_logging;
/// let _logging = setup_logging();
///
/// tracing::info!("This message will be printed to stderr");
/// ```
pub fn setup_logging() -> LoggingGuard {
    LoggingBuilder::new().build()
}

/// Sets up logging for the current thread and uses the passed filter to filter the shown events.
/// Useful for capturing the tracing output in a failing test.
///
/// # Examples
/// ```
/// use ruff_db::testing::setup_logging_with_filter;
/// let _logging = setup_logging_with_filter("ty_module_resolver::resolver");
/// ```
///
/// # Filter
/// See [`tracing_subscriber::EnvFilter`] for the `filter`'s syntax.
///
pub fn setup_logging_with_filter(filter: &str) -> Option<LoggingGuard> {
    LoggingBuilder::with_filter(filter).map(LoggingBuilder::build)
}

#[derive(Debug)]
pub struct LoggingBuilder {
    filter: EnvFilter,
}

impl LoggingBuilder {
    pub fn new() -> Self {
        Self {
            filter: EnvFilter::default()
                .add_directive("ty=trace".parse().expect("Hardcoded directive to be valid"))
                .add_directive(
                    "ruff=trace"
                        .parse()
                        .expect("Hardcoded directive to be valid"),
                ),
        }
    }

    pub fn with_filter(filter: &str) -> Option<Self> {
        let filter = EnvFilter::builder().parse(filter).ok()?;

        Some(Self { filter })
    }

    pub fn build(self) -> LoggingGuard {
        let registry = tracing_subscriber::registry().with(self.filter);

        let subscriber = registry.with(
            tracing_subscriber::fmt::layer()
                .compact()
                .with_writer(std::io::stderr)
                .with_timer(tracing_subscriber::fmt::time()),
        );

        let guard = tracing::subscriber::set_default(subscriber);

        LoggingGuard { _guard: guard }
    }
}

impl Default for LoggingBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[must_use = "Dropping the guard unregisters the tracing subscriber."]
pub struct LoggingGuard {
    _guard: tracing::subscriber::DefaultGuard,
}

#[test]
fn query_was_not_run() {
    use crate::tests::TestDb;
    use salsa::prelude::*;

    #[salsa::input(debug)]
    struct Input {
        text: String,
    }

    #[salsa::tracked]
    fn len(db: &dyn salsa::Database, input: Input) -> usize {
        input.text(db).len()
    }

    let mut db = TestDb::new();

    let hello = Input::new(&db, "Hello, world!".to_string());
    let goodbye = Input::new(&db, "Goodbye!".to_string());

    assert_eq!(len(&db, hello), 13);
    assert_eq!(len(&db, goodbye), 8);

    // Change the input of one query
    goodbye.set_text(&mut db).to("Bye".to_string());
    db.clear_salsa_events();

    assert_eq!(len(&db, goodbye), 3);
    let events = db.take_salsa_events();

    assert_function_query_was_run(&db, len, goodbye, &events);
    assert_function_query_was_not_run(&db, len, hello, &events);
}

#[test]
#[should_panic(expected = "Expected query len(Id(0)) not to have run but it did:")]
fn query_was_not_run_fails_if_query_was_run() {
    use crate::tests::TestDb;
    use salsa::prelude::*;

    #[salsa::input(debug)]
    struct Input {
        text: String,
    }

    #[salsa::tracked]
    fn len(db: &dyn salsa::Database, input: Input) -> usize {
        input.text(db).len()
    }

    let mut db = TestDb::new();

    let hello = Input::new(&db, "Hello, world!".to_string());

    assert_eq!(len(&db, hello), 13);

    // Change the input
    hello.set_text(&mut db).to("Hy".to_string());
    db.clear_salsa_events();

    assert_eq!(len(&db, hello), 2);
    let events = db.take_salsa_events();

    assert_function_query_was_not_run(&db, len, hello, &events);
}

#[test]
#[should_panic(expected = "Expected query len() not to have run but it did:")]
fn const_query_was_not_run_fails_if_query_was_run() {
    use crate::tests::TestDb;
    use salsa::prelude::*;

    #[salsa::input]
    struct Input {
        text: String,
    }

    #[salsa::tracked]
    fn len(db: &dyn salsa::Database) -> usize {
        db.report_untracked_read();
        5
    }

    let mut db = TestDb::new();
    let hello = Input::new(&db, "Hello, world!".to_string());
    assert_eq!(len(&db), 5);

    // Create a new revision
    db.clear_salsa_events();
    hello.set_text(&mut db).to("Hy".to_string());

    assert_eq!(len(&db), 5);
    let events = db.take_salsa_events();

    assert_const_function_query_was_not_run(&db, len, &events);
}

#[test]
#[should_panic(expected = "Expected query len(Id(0)) to have run but it did not:")]
fn query_was_run_fails_if_query_was_not_run() {
    use crate::tests::TestDb;
    use salsa::prelude::*;

    #[salsa::input(debug)]
    struct Input {
        text: String,
    }

    #[salsa::tracked]
    fn len(db: &dyn salsa::Database, input: Input) -> usize {
        input.text(db).len()
    }

    let mut db = TestDb::new();

    let hello = Input::new(&db, "Hello, world!".to_string());
    let goodbye = Input::new(&db, "Goodbye!".to_string());

    assert_eq!(len(&db, hello), 13);
    assert_eq!(len(&db, goodbye), 8);

    // Change the input of one query
    goodbye.set_text(&mut db).to("Bye".to_string());
    db.clear_salsa_events();

    assert_eq!(len(&db, goodbye), 3);
    let events = db.take_salsa_events();

    assert_function_query_was_run(&db, len, hello, &events);
}
