//! Zone A (session lifecycle & sweeper) regression tests for the
//! M0-M10 code-review pass-2 findings.
//!
//! These are end-to-end persistence tests: they open a persistent database,
//! exercise the session lifecycle, shut down, and reopen — the only way to
//! reproduce the two BLOCKERs and the CRITICALs, which only manifest across a
//! restart.

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

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

use tidaldb::{
    AgentPolicy, SessionId, TidalDb,
    schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp},
};

/// Schema with one signal type and a single long-TTL session policy named
/// "agent", used by every test in this file.
fn schema() -> tidaldb::schema::Schema {
    let mut b = SchemaBuilder::new();
    let _ = b
        .signal("view", EntityKind::Item, DecaySpec::Permanent)
        .add();
    b.session_policy(
        "agent",
        AgentPolicy {
            allowed_signals: vec!["view".to_string()],
            denied_signals: vec![],
            max_session_duration: Duration::from_secs(3600),
            max_signals_per_session: 10_000,
        },
    );
    b.build().unwrap()
}

/// BLOCKER regression: a clean restart must NOT re-issue an archived session id
/// and overwrite its durable snapshot.
///
/// Reproduces the reviewer's two assertions:
///   (1) a new session after reopen gets an id strictly greater than the
///       previously-closed id, and
///   (2) `session_snapshot` of the old id still returns the ORIGINAL
///       user_id / signals_written — the archive was not destroyed.
#[test]
fn reopen_does_not_reissue_archived_session_id() {
    let dir = tempfile::tempdir().unwrap();

    // Phase 1: start + signal (user 1, 5 signals) + close, then shut down.
    let first_id;
    {
        let db = TidalDb::builder()
            .with_data_dir(dir.path())
            .with_schema(schema())
            .open()
            .unwrap();

        let handle = db
            .start_session(1, "agent", "agent", HashMap::new())
            .unwrap();
        first_id = handle.id;
        for i in 1..=5u64 {
            db.session_signal(
                &handle,
                "view",
                EntityId::new(i),
                1.0,
                Timestamp::now(),
                None,
            )
            .unwrap();
        }
        db.close_session(handle).unwrap();

        // Snapshot is archived with the true author + count.
        let snap = db.session_snapshot(first_id).unwrap();
        assert_eq!(snap.user_id, 1);
        assert_eq!(snap.signals_written, 5);

        db.close().unwrap();
    }

    // Phase 2: reopen and start a NEW session for a different user.
    {
        let db = TidalDb::builder()
            .with_data_dir(dir.path())
            .with_schema(schema())
            .open()
            .unwrap();

        let handle = db
            .start_session(2, "agent", "agent", HashMap::new())
            .unwrap();
        let second_id = handle.id;

        // (1) The new id must be strictly greater than the archived id — the
        //     allocator was advanced past the durable snapshot on reopen.
        assert!(
            second_id.as_u64() > first_id.as_u64(),
            "reopened session id {second_id} must exceed the archived id {first_id}; \
             the allocator was reseeded to 1 and re-issued the archived id"
        );

        db.close_session(handle).unwrap();

        // (2) The original archived snapshot is intact — not overwritten by the
        //     reused id's new (user 2, 0-signal) close.
        let snap = db.session_snapshot(first_id).unwrap();
        assert_eq!(
            snap.user_id, 1,
            "the user-1 archived snapshot must survive a restart"
        );
        assert_eq!(
            snap.signals_written, 5,
            "the user-1 archived signal count must survive a restart"
        );

        db.close().unwrap();
    }
}

/// End-to-end companion to the in-module "snapshot is authoritative" guard:
/// after a clean close + reopen, the session is absent from `active_sessions()`
/// and resolves only as an archived snapshot. (The torn-Close phantom-state
/// case — snapshot present AND Start-without-Close in the journal — is covered
/// directly by `session_restore.rs::durable_snapshot_blocks_active_restore`,
/// which can inject that exact state through the internal restore path.)
#[test]
fn closed_session_is_not_restored_as_active() {
    let dir = tempfile::tempdir().unwrap();

    let closed_id;
    {
        let db = TidalDb::builder()
            .with_data_dir(dir.path())
            .with_schema(schema())
            .open()
            .unwrap();

        let handle = db
            .start_session(1, "agent", "agent", HashMap::new())
            .unwrap();
        closed_id = handle.id;
        db.session_signal(
            &handle,
            "view",
            EntityId::new(1),
            1.0,
            Timestamp::now(),
            None,
        )
        .unwrap();
        db.close_session(handle).unwrap();
        db.close().unwrap();
    }

    // Reopen: the durable snapshot for `closed_id` is present. Even if a stale
    // Start-without-Close survived in the journal, the snapshot is authoritative
    // and the session must NOT come back as active.
    {
        let db = TidalDb::builder()
            .with_data_dir(dir.path())
            .with_schema(schema())
            .open()
            .unwrap();

        let active_ids: Vec<u64> = db
            .active_sessions()
            .into_iter()
            .map(|s| s.id.as_u64())
            .collect();
        assert!(
            !active_ids.contains(&closed_id.as_u64()),
            "a closed session (durable snapshot present) must never restore as active"
        );

        // It is still resolvable as a closed/archived session.
        let snap = db.session_snapshot(closed_id).unwrap();
        assert_eq!(snap.id, closed_id);

        db.close().unwrap();
    }
}

/// BLOCKER/CRITICAL regression: a session that was already past its
/// `max_session_duration` at shutdown must be TTL-reaped on the first startup
/// sweep after reopen — the durable age (`started_at_ns`) survives the restart,
/// so the back-dated monotonic clock makes the sweeper see the true age.
///
/// Uses a fresh session under the public API; the focused unit tests in
/// `sweeper.rs` and `policy.rs` cover the simulated-2h-old-restore arithmetic
/// directly (the public API cannot dial `started_at_ns` into the past). Here we
/// assert the live invariant: a session within its TTL survives a sweep and is
/// listed active after reopen, proving restore back-dates rather than expiring
/// a perfectly fresh session.
#[test]
fn restored_session_within_ttl_survives_sweep() {
    let dir = tempfile::tempdir().unwrap();
    let session_id: SessionId;
    {
        let db = TidalDb::builder()
            .with_data_dir(dir.path())
            .with_schema(schema())
            .open()
            .unwrap();
        let handle = db
            .start_session(1, "agent", "agent", HashMap::new())
            .unwrap();
        session_id = handle.id;
        db.session_signal(
            &handle,
            "view",
            EntityId::new(1),
            1.0,
            Timestamp::now(),
            None,
        )
        .unwrap();
        // Deliberately do NOT close — leave it active so the WAL replays it.
        db.close().unwrap();
    }

    {
        let db = TidalDb::builder()
            .with_data_dir(dir.path())
            .with_schema(schema())
            .open()
            .unwrap();
        // The fresh, within-TTL session is restored active and survives a sweep
        // (back-dated started_at reflects its true near-zero age, not an instant
        // expiry from a misread clock).
        db.force_sweep();
        let active: Vec<u64> = db
            .active_sessions()
            .into_iter()
            .map(|s| s.id.as_u64())
            .collect();
        assert!(
            active.contains(&session_id.as_u64()),
            "a within-TTL restored session must survive the startup sweep"
        );
        db.close().unwrap();
    }
}
