use std::panic::UnwindSafe;

use expect_test::expect;
use salsa::{Durability, ParallelDatabase, Snapshot};

// Axes:
//
// Threading
// * Intra-thread
// * Cross-thread -- part of cycle is on one thread, part on another
//
// Recovery strategies:
// * Panic
// * Fallback
// * Mixed -- multiple strategies within cycle participants
//
// Across revisions:
// * N/A -- only one revision
// * Present in new revision, not old
// * Present in old revision, not new
// * Present in both revisions
//
// Dependencies
// * Tracked
// * Untracked -- cycle participant(s) contain untracked reads
//
// Layers
// * Direct -- cycle participant is directly invoked from test
// * Indirect -- invoked a query that invokes the cycle
//
//
// | Thread | Recovery | Old, New | Dep style | Layers   | Test Name      |
// | ------ | -------- | -------- | --------- | ------   | ---------      |
// | Intra  | Panic    | N/A      | Tracked   | direct   | cycle_memoized |
// | Intra  | Panic    | N/A      | Untracked | direct   | cycle_volatile |
// | Intra  | Fallback | N/A      | Tracked   | direct   | cycle_cycle  |
// | Intra  | Fallback | N/A      | Tracked   | indirect | inner_cycle |
// | Intra  | Fallback | Both     | Tracked   | direct   | cycle_revalidate |
// | Intra  | Fallback | New      | Tracked   | direct   | cycle_appears |
// | Intra  | Fallback | Old      | Tracked   | direct   | cycle_disappears |
// | Intra  | Fallback | Old      | Tracked   | direct   | cycle_disappears_durability |
// | Intra  | Mixed    | N/A      | Tracked   | direct   | cycle_mixed_1 |
// | Intra  | Mixed    | N/A      | Tracked   | direct   | cycle_mixed_2 |
// | Cross  | Fallback | N/A      | Tracked   | both     | parallel/cycles.rs: recover_parallel_cycle |
// | Cross  | Panic    | N/A      | Tracked   | both     | parallel/cycles.rs: panic_parallel_cycle |

#[derive(PartialEq, Eq, Hash, Clone, Debug)]
struct Error {
    cycle: Vec<String>,
}

#[salsa::database(GroupStruct)]
#[derive(Default)]
struct DatabaseImpl {
    storage: salsa::Storage<Self>,
}

impl salsa::Database for DatabaseImpl {}

impl ParallelDatabase for DatabaseImpl {
    fn snapshot(&self) -> Snapshot<Self> {
        Snapshot::new(DatabaseImpl { storage: self.storage.snapshot() })
    }
}

/// The queries A, B, and C in `Database` can be configured
/// to invoke one another in arbitrary ways using this
/// enum.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum CycleQuery {
    None,
    A,
    B,
    C,
    AthenC,
}

#[salsa::query_group(GroupStruct)]
trait Database: salsa::Database {
    // `a` and `b` depend on each other and form a cycle
    fn memoized_a(&self) -> ();
    fn memoized_b(&self) -> ();
    fn volatile_a(&self) -> ();
    fn volatile_b(&self) -> ();

    #[salsa::input]
    fn a_invokes(&self) -> CycleQuery;

    #[salsa::input]
    fn b_invokes(&self) -> CycleQuery;

    #[salsa::input]
    fn c_invokes(&self) -> CycleQuery;

    #[salsa::cycle(recover_a)]
    fn cycle_a(&self) -> Result<(), Error>;

    #[salsa::cycle(recover_b)]
    fn cycle_b(&self) -> Result<(), Error>;

    fn cycle_c(&self) -> Result<(), Error>;
}

fn recover_a(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> {
    Err(Error { cycle: cycle.all_participants(db) })
}

fn recover_b(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> {
    Err(Error { cycle: cycle.all_participants(db) })
}

fn memoized_a(db: &dyn Database) {
    db.memoized_b()
}

fn memoized_b(db: &dyn Database) {
    db.memoized_a()
}

fn volatile_a(db: &dyn Database) {
    db.salsa_runtime().report_untracked_read();
    db.volatile_b()
}

fn volatile_b(db: &dyn Database) {
    db.salsa_runtime().report_untracked_read();
    db.volatile_a()
}

impl CycleQuery {
    fn invoke(self, db: &dyn Database) -> Result<(), Error> {
        match self {
            CycleQuery::A => db.cycle_a(),
            CycleQuery::B => db.cycle_b(),
            CycleQuery::C => db.cycle_c(),
            CycleQuery::AthenC => {
                let _ = db.cycle_a();
                db.cycle_c()
            }
            CycleQuery::None => Ok(()),
        }
    }
}

fn cycle_a(db: &dyn Database) -> Result<(), Error> {
    db.a_invokes().invoke(db)
}

fn cycle_b(db: &dyn Database) -> Result<(), Error> {
    db.b_invokes().invoke(db)
}

fn cycle_c(db: &dyn Database) -> Result<(), Error> {
    db.c_invokes().invoke(db)
}

#[track_caller]
fn extract_cycle(f: impl FnOnce() + UnwindSafe) -> salsa::Cycle {
    let v = std::panic::catch_unwind(f);
    if let Err(d) = &v {
        if let Some(cycle) = d.downcast_ref::<salsa::Cycle>() {
            return cycle.clone();
        }
    }
    panic!("unexpected value: {v:?}")
}

#[test]
fn cycle_memoized() {
    let db = DatabaseImpl::default();
    let cycle = extract_cycle(|| db.memoized_a());
    expect![[r#"
        [
            "cycles::MemoizedAQuery::memoized_a(())",
            "cycles::MemoizedBQuery::memoized_b(())",
        ]
    "#]]
    .assert_debug_eq(&cycle.unexpected_participants(&db));
}

#[test]
fn cycle_volatile() {
    let db = DatabaseImpl::default();
    let cycle = extract_cycle(|| db.volatile_a());
    expect![[r#"
        [
            "cycles::VolatileAQuery::volatile_a(())",
            "cycles::VolatileBQuery::volatile_b(())",
        ]
    "#]]
    .assert_debug_eq(&cycle.unexpected_participants(&db));
}

#[test]
fn cycle_cycle() {
    let mut query = DatabaseImpl::default();

    //     A --> B
    //     ^     |
    //     +-----+

    query.set_a_invokes(CycleQuery::B);
    query.set_b_invokes(CycleQuery::A);

    assert!(query.cycle_a().is_err());
}

#[test]
fn inner_cycle() {
    let mut query = DatabaseImpl::default();

    //     A --> B <-- C
    //     ^     |
    //     +-----+

    query.set_a_invokes(CycleQuery::B);
    query.set_b_invokes(CycleQuery::A);
    query.set_c_invokes(CycleQuery::B);

    let err = query.cycle_c();
    assert!(err.is_err());
    let cycle = err.unwrap_err().cycle;
    expect![[r#"
        [
            "cycles::CycleAQuery::cycle_a(())",
            "cycles::CycleBQuery::cycle_b(())",
        ]
    "#]]
    .assert_debug_eq(&cycle);
}

#[test]
fn cycle_revalidate() {
    let mut db = DatabaseImpl::default();

    //     A --> B
    //     ^     |
    //     +-----+
    db.set_a_invokes(CycleQuery::B);
    db.set_b_invokes(CycleQuery::A);

    assert!(db.cycle_a().is_err());
    db.set_b_invokes(CycleQuery::A); // same value as default
    assert!(db.cycle_a().is_err());
}

#[test]
fn cycle_revalidate_unchanged_twice() {
    let mut db = DatabaseImpl::default();

    //     A --> B
    //     ^     |
    //     +-----+
    db.set_a_invokes(CycleQuery::B);
    db.set_b_invokes(CycleQuery::A);

    assert!(db.cycle_a().is_err());
    db.set_c_invokes(CycleQuery::A); // force new revisi5on

    // on this run
    expect![[r#"
        Err(
            Error {
                cycle: [
                    "cycles::CycleAQuery::cycle_a(())",
                    "cycles::CycleBQuery::cycle_b(())",
                ],
            },
        )
    "#]]
    .assert_debug_eq(&db.cycle_a());
}

#[test]
fn cycle_appears() {
    let mut db = DatabaseImpl::default();

    //     A --> B
    db.set_a_invokes(CycleQuery::B);
    db.set_b_invokes(CycleQuery::None);
    assert!(db.cycle_a().is_ok());

    //     A --> B
    //     ^     |
    //     +-----+
    db.set_b_invokes(CycleQuery::A);
    tracing::debug!("Set Cycle Leaf");
    assert!(db.cycle_a().is_err());
}

#[test]
fn cycle_disappears() {
    let mut db = DatabaseImpl::default();

    //     A --> B
    //     ^     |
    //     +-----+
    db.set_a_invokes(CycleQuery::B);
    db.set_b_invokes(CycleQuery::A);
    assert!(db.cycle_a().is_err());

    //     A --> B
    db.set_b_invokes(CycleQuery::None);
    assert!(db.cycle_a().is_ok());
}

/// A variant on `cycle_disappears` in which the values of
/// `a_invokes` and `b_invokes` are set with durability values.
/// If we are not careful, this could cause us to overlook
/// the fact that the cycle will no longer occur.
#[test]
fn cycle_disappears_durability() {
    let mut db = DatabaseImpl::default();
    db.set_a_invokes_with_durability(CycleQuery::B, Durability::LOW);
    db.set_b_invokes_with_durability(CycleQuery::A, Durability::HIGH);

    let res = db.cycle_a();
    assert!(res.is_err());

    // At this point, `a` read `LOW` input, and `b` read `HIGH` input. However,
    // because `b` participates in the same cycle as `a`, its final durability
    // should be `LOW`.
    //
    // Check that setting a `LOW` input causes us to re-execute `b` query, and
    // observe that the cycle goes away.
    db.set_a_invokes_with_durability(CycleQuery::None, Durability::LOW);

    let res = db.cycle_b();
    assert!(res.is_ok());
}

#[test]
fn cycle_mixed_1() {
    let mut db = DatabaseImpl::default();
    //     A --> B <-- C
    //           |     ^
    //           +-----+
    db.set_a_invokes(CycleQuery::B);
    db.set_b_invokes(CycleQuery::C);
    db.set_c_invokes(CycleQuery::B);

    let u = db.cycle_c();
    expect![[r#"
        Err(
            Error {
                cycle: [
                    "cycles::CycleBQuery::cycle_b(())",
                    "cycles::CycleCQuery::cycle_c(())",
                ],
            },
        )
    "#]]
    .assert_debug_eq(&u);
}

#[test]
fn cycle_mixed_2() {
    let mut db = DatabaseImpl::default();

    // Configuration:
    //
    //     A --> B --> C
    //     ^           |
    //     +-----------+
    db.set_a_invokes(CycleQuery::B);
    db.set_b_invokes(CycleQuery::C);
    db.set_c_invokes(CycleQuery::A);

    let u = db.cycle_a();
    expect![[r#"
        Err(
            Error {
                cycle: [
                    "cycles::CycleAQuery::cycle_a(())",
                    "cycles::CycleBQuery::cycle_b(())",
                    "cycles::CycleCQuery::cycle_c(())",
                ],
            },
        )
    "#]]
    .assert_debug_eq(&u);
}

#[test]
fn cycle_deterministic_order() {
    // No matter whether we start from A or B, we get the same set of participants:
    let db = || {
        let mut db = DatabaseImpl::default();
        //     A --> B
        //     ^     |
        //     +-----+
        db.set_a_invokes(CycleQuery::B);
        db.set_b_invokes(CycleQuery::A);
        db
    };
    let a = db().cycle_a();
    let b = db().cycle_b();
    expect![[r#"
        (
            Err(
                Error {
                    cycle: [
                        "cycles::CycleAQuery::cycle_a(())",
                        "cycles::CycleBQuery::cycle_b(())",
                    ],
                },
            ),
            Err(
                Error {
                    cycle: [
                        "cycles::CycleAQuery::cycle_a(())",
                        "cycles::CycleBQuery::cycle_b(())",
                    ],
                },
            ),
        )
    "#]]
    .assert_debug_eq(&(a, b));
}

#[test]
fn cycle_multiple() {
    // No matter whether we start from A or B, we get the same set of participants:
    let mut db = DatabaseImpl::default();

    // Configuration:
    //
    //     A --> B <-- C
    //     ^     |     ^
    //     +-----+     |
    //           |     |
    //           +-----+
    //
    // Here, conceptually, B encounters a cycle with A and then
    // recovers.
    db.set_a_invokes(CycleQuery::B);
    db.set_b_invokes(CycleQuery::AthenC);
    db.set_c_invokes(CycleQuery::B);

    let c = db.cycle_c();
    let b = db.cycle_b();
    let a = db.cycle_a();
    expect![[r#"
        (
            Err(
                Error {
                    cycle: [
                        "cycles::CycleAQuery::cycle_a(())",
                        "cycles::CycleBQuery::cycle_b(())",
                    ],
                },
            ),
            Err(
                Error {
                    cycle: [
                        "cycles::CycleAQuery::cycle_a(())",
                        "cycles::CycleBQuery::cycle_b(())",
                    ],
                },
            ),
            Err(
                Error {
                    cycle: [
                        "cycles::CycleAQuery::cycle_a(())",
                        "cycles::CycleBQuery::cycle_b(())",
                    ],
                },
            ),
        )
    "#]]
    .assert_debug_eq(&(a, b, c));
}

#[test]
fn cycle_recovery_set_but_not_participating() {
    let mut db = DatabaseImpl::default();

    //     A --> C -+
    //           ^  |
    //           +--+
    db.set_a_invokes(CycleQuery::C);
    db.set_c_invokes(CycleQuery::C);

    // Here we expect C to panic and A not to recover:
    let r = extract_cycle(|| drop(db.cycle_a()));
    expect![[r#"
        [
            "cycles::CycleCQuery::cycle_c(())",
        ]
    "#]]
    .assert_debug_eq(&r.all_participants(&db));
}
