//! M9p3 integration test — Retroactive Purge & Deterministic Rematerialization.
//!
//! Validates the full purge flow: multiple users contribute to a community,
//! one user purges their prior contributions, and the community aggregate
//! rematerializes to exactly the remaining contributors — deterministically,
//! idempotently, with the local profile untouched and the purge surviving a
//! close/reopen.
//!
//! ROADMAP §Milestone 9 Phase 3.

#![allow(clippy::too_many_lines, clippy::unwrap_used)]

use std::time::Duration;

use tidaldb::{
    CommunityId, ShareIntent, SharePolicy, TidalDb, UserId,
    schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window},
};

fn schema() -> tidaldb::schema::Schema {
    let mut b = SchemaBuilder::new();
    let _ = b
        .signal(
            "like",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    b.build().unwrap()
}

fn policy() -> SharePolicy {
    SharePolicy::community_share(1, [ShareIntent::Like])
}

/// Join `users` and have each contribute one `like` on `item` to `community`.
fn seed(db: &TidalDb, community: CommunityId, item: EntityId, users: &[u64]) {
    let now = Timestamp::now();
    for &u in users {
        db.join_community(UserId(u), community, policy()).unwrap();
        assert!(
            db.signal_for_community("like", item, 1.0, now, UserId(u), community)
                .unwrap()
        );
    }
}

#[test]
fn purge_removes_only_target_user_and_rematerializes() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let (community, item) = (CommunityId(1), EntityId::new(100));
    seed(&db, community, item, &[1, 2, 3]);

    // Aggregate reflects all 3 contributors.
    assert_eq!(
        db.read_community_windowed_count(community, item, "like", Window::AllTime)
            .unwrap(),
        3
    );

    // User 2 purges their prior contributions (all epochs).
    let receipt = db
        .purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
        .unwrap();
    assert_eq!(receipt.contributions_removed, 1);
    assert!(receipt.watermark_ns > 0);

    // Rematerialized aggregate = remaining 2 contributors.
    assert_eq!(
        db.read_community_windowed_count(community, item, "like", Window::AllTime)
            .unwrap(),
        2
    );
    // Purge watermark is exposed.
    assert_eq!(
        db.community_purge_watermark(community),
        Some(receipt.watermark_ns)
    );
}

#[test]
fn purge_is_idempotent() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let (community, item) = (CommunityId(1), EntityId::new(1));
    seed(&db, community, item, &[1, 2, 3]);

    db.purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
        .unwrap();
    let after_first = db
        .read_community_windowed_count(community, item, "like", Window::AllTime)
        .unwrap();

    // Re-purge the same range: removes nothing further, aggregate unchanged.
    let receipt2 = db
        .purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
        .unwrap();
    assert_eq!(receipt2.contributions_removed, 0);
    assert_eq!(
        db.read_community_windowed_count(community, item, "like", Window::AllTime)
            .unwrap(),
        after_first,
        "repeated purge must yield an identical aggregate"
    );
}

#[test]
fn purge_does_not_touch_local_profile() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let (community, item) = (CommunityId(1), EntityId::new(7));
    db.join_community(UserId(1), community, policy()).unwrap();
    let now = Timestamp::now();

    // User 1 contributes to the community AND has local likes on the same item.
    db.signal_for_community("like", item, 1.0, now, UserId(1), community)
        .unwrap();
    db.signal("like", item, 1.0, now).unwrap();
    db.signal("like", item, 1.0, now).unwrap();
    let local_before = db
        .read_windowed_count(item, "like", Window::AllTime)
        .unwrap();
    assert_eq!(local_before, 2);

    // Purge the user's community contributions.
    db.purge_prior_contributions(UserId(1), community, 0..=u32::MAX)
        .unwrap();

    // Community aggregate emptied, but the LOCAL profile is untouched.
    assert_eq!(
        db.read_community_windowed_count(community, item, "like", Window::AllTime)
            .unwrap(),
        0
    );
    assert_eq!(
        db.read_windowed_count(item, "like", Window::AllTime)
            .unwrap(),
        2,
        "purge must never touch the local profile"
    );
}

#[test]
fn purge_respects_epoch_range_via_rejoin() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let (community, item) = (CommunityId(2), EntityId::new(3));
    let user = UserId(5);
    let now = Timestamp::now();

    // Epoch 0 contribution.
    db.join_community(user, community, policy()).unwrap();
    db.signal_for_community("like", item, 1.0, now, user, community)
        .unwrap();
    // Leave + rejoin -> epoch 1 contribution.
    db.leave_community(user, community, tidaldb::LeaveMode::StopForward)
        .unwrap();
    db.rejoin_community(user, community, policy()).unwrap();
    db.signal_for_community("like", item, 1.0, now, user, community)
        .unwrap();
    assert_eq!(
        db.read_community_windowed_count(community, item, "like", Window::AllTime)
            .unwrap(),
        2
    );

    // Purge only epoch 0; the epoch-1 (post-rejoin) contribution survives.
    let receipt = db
        .purge_prior_contributions(user, community, 0..=0)
        .unwrap();
    assert_eq!(receipt.contributions_removed, 1);
    assert_eq!(
        db.read_community_windowed_count(community, item, "like", Window::AllTime)
            .unwrap(),
        1
    );
}

#[test]
fn purge_survives_reopen() {
    let home = tidaldb::TempTidalHome::new().unwrap();
    let (community, item) = (CommunityId(1), EntityId::new(100));

    {
        let db = TidalDb::builder()
            .with_data_dir(home.path())
            .with_schema(schema())
            .open()
            .unwrap();
        seed(&db, community, item, &[1, 2, 3]);
        db.purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
            .unwrap();
        assert_eq!(
            db.read_community_windowed_count(community, item, "like", Window::AllTime)
                .unwrap(),
            2
        );
        db.shutdown().unwrap();
    }

    // Reopen: the community aggregate is restored from the Tag::Community
    // checkpoint with the purge already applied.
    let db = TidalDb::builder()
        .with_data_dir(home.path())
        .with_schema(schema())
        .open()
        .unwrap();
    assert_eq!(
        db.read_community_windowed_count(community, item, "like", Window::AllTime)
            .unwrap(),
        2,
        "purged community aggregate must survive reopen"
    );
}
