//! M10p1 integration test — Community Governance Policy Engine.
//!
//! Validates versioned community policies governing signal eligibility and
//! per-contribution weighting: excluded intents are rejected, admitted weights
//! are clamped, versions are monotonic with effective timestamps, the governing
//! version is queryable, and policies survive close/reopen.
//!
//! ROADMAP §Milestone 10 Phase 1.

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

use std::{collections::BTreeSet, time::Duration};

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

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

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

fn policy(community: CommunityId, version: u32, effective_at_ns: u64) -> GovernancePolicy {
    GovernancePolicy {
        community_id: community,
        version,
        effective_at_ns,
        allowed_intents: BTreeSet::new(),
        excluded_intents: BTreeSet::from(["skip_for_now".to_string()]),
        weighting: WeightingBounds { min: 0.0, max: 2.0 },
        trust_threshold: 0.0,
    }
}

#[test]
fn excluded_intent_is_rejected_by_policy() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let (c, item, user) = (CommunityId(1), EntityId::new(5), UserId(1));
    db.join_community(user, c, full_share()).unwrap();
    db.define_community_policy(policy(c, 1, 0)).unwrap();
    let now = Timestamp::now();

    // `skip_for_now` is excluded by governance -> rejected (not recorded).
    assert!(
        !db.signal_for_community("skip_for_now", item, 1.0, now, user, c)
            .unwrap()
    );
    // `like` is admitted.
    assert!(
        db.signal_for_community("like", item, 1.0, now, user, c)
            .unwrap()
    );
    assert_eq!(
        db.read_community_windowed_count(c, item, "like", Window::AllTime)
            .unwrap(),
        1
    );
    assert_eq!(
        db.read_community_windowed_count(c, item, "skip_for_now", Window::AllTime)
            .unwrap(),
        0
    );
}

#[test]
fn weight_is_clamped_by_policy() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let item = EntityId::new(5);
    let now = Timestamp::now();

    // Governed community: max weight 2.0.
    let governed = CommunityId(1);
    db.join_community(UserId(1), governed, full_share())
        .unwrap();
    db.define_community_policy(policy(governed, 1, 0)).unwrap();
    assert!(
        db.signal_for_community("like", item, 100.0, now, UserId(1), governed)
            .unwrap()
    );

    // Ungoverned community: weight passes through.
    let free = CommunityId(2);
    db.join_community(UserId(1), free, full_share()).unwrap();
    assert!(
        db.signal_for_community("like", item, 100.0, now, UserId(1), free)
            .unwrap()
    );

    let governed_score = db
        .read_community_decay_score(governed, item, "like", 0)
        .unwrap()
        .unwrap();
    let free_score = db
        .read_community_decay_score(free, item, "like", 0)
        .unwrap()
        .unwrap();
    assert!(
        governed_score <= 2.0 + 1e-6,
        "governed weight must be clamped to 2.0, got {governed_score}"
    );
    assert!(
        free_score > 2.0,
        "ungoverned weight must pass through, got {free_score}"
    );
}

#[test]
fn policy_versions_are_monotonic_and_queryable() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let c = CommunityId(1);
    db.define_community_policy(policy(c, 1, 0)).unwrap();
    assert_eq!(db.governing_policy_version(c), Some(1));

    // v2 supersedes once registered (effective at 0 here).
    db.define_community_policy(policy(c, 2, 0)).unwrap();
    assert_eq!(db.governing_policy_version(c), Some(2));

    // Re-registering a non-increasing version fails.
    assert!(db.define_community_policy(policy(c, 2, 0)).is_err());
    assert!(db.define_community_policy(policy(c, 1, 0)).is_err());
}

#[test]
fn no_policy_means_no_governance() {
    let db = TidalDb::builder()
        .ephemeral()
        .with_schema(schema())
        .open()
        .unwrap();
    let (c, item, user) = (CommunityId(9), EntityId::new(1), UserId(1));
    db.join_community(user, c, full_share()).unwrap();
    let now = Timestamp::now();
    // Without a governance policy, even `skip_for_now` flows (share policy allows it).
    assert!(
        db.signal_for_community("skip_for_now", item, 1.0, now, user, c)
            .unwrap()
    );
    assert_eq!(db.governing_policy_version(c), None);
}

#[test]
fn policy_survives_reopen() {
    let home = tidaldb::TempTidalHome::new().unwrap();
    let c = CommunityId(1);
    {
        let db = TidalDb::builder()
            .with_data_dir(home.path())
            .with_schema(schema())
            .open()
            .unwrap();
        db.define_community_policy(policy(c, 1, 0)).unwrap();
        db.define_community_policy(policy(c, 2, 0)).unwrap();
        db.shutdown().unwrap();
    }
    let db = TidalDb::builder()
        .with_data_dir(home.path())
        .with_schema(schema())
        .open()
        .unwrap();
    assert_eq!(
        db.governing_policy_version(c),
        Some(2),
        "governance policy must survive reopen"
    );
    // Monotonic check still holds against restored history.
    assert!(db.define_community_policy(policy(c, 2, 0)).is_err());
    assert!(db.define_community_policy(policy(c, 3, 0)).is_ok());
}
