//! Review pass-2, Zone D (Replication + tidal-net transport) regression tests.
//!
//! W8: `signal_for_tenant` must fail CLOSED *before any write* when a dual-write
//! resolves a non-local shard, so a caller that retries the returned error
//! cannot double-count the local half (signal accumulation is not idempotent).
//! The companion case proves a fully-local resolution still writes.

#![allow(clippy::unwrap_used)]

use std::time::Duration;

use tidaldb::{
    TidalDb, TidalError,
    replication::{RegionId, ShardAssignment, ShardId, TenantId},
    schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window},
};

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

    TidalDb::builder()
        .ephemeral()
        .with_schema(schema)
        .open()
        .expect("ephemeral db opens")
}

/// W8: a dual-write that resolves a non-local shard must fail closed with ZERO
/// local writes — the previous behavior wrote the local half and then errored,
/// so a retry double-counted the signal. After the error, the entity must have
/// NO decay score, proving the local half was never applied.
#[test]
fn signal_for_tenant_remote_shard_fails_closed_with_zero_writes() {
    let db = open_db_with_view_signal();

    // This node is the default local shard (ShardId::SINGLE = 0). Add a second
    // shard and put the default tenant into dual-write so a write resolves BOTH
    // the local shard 0 and the non-local shard 1.
    db.control_plane().update_topology(ShardAssignment {
        shard_id: ShardId(1),
        region_id: RegionId(1),
    });
    db.tenant_router()
        .set_dual_write(TenantId::DEFAULT, ShardId(0), ShardId(1));

    let item = EntityId::new(42);
    let err = db
        .signal_for_tenant(TenantId::DEFAULT, "view", item, 1.0, Timestamp::now())
        .expect_err("remote-shard dual-write must fail closed, not silently drop");
    assert!(
        matches!(err, TidalError::Internal(_)),
        "expected Internal (fail-closed) error, got: {err}"
    );

    // The local half must NOT have been applied: a retry of the Err cannot
    // double-count because nothing was written. The entity has no decay score.
    let score = db
        .read_decay_score(item, "view", 0)
        .expect("read local decay score");
    assert!(
        score.is_none() || score == Some(0.0),
        "fail-closed must write nothing locally; got score {score:?}"
    );

    // Retrying the failed op repeatedly must still leave nothing written — the
    // exact retry-amplification the W8 fix removes.
    for _ in 0..5 {
        let _ = db.signal_for_tenant(TenantId::DEFAULT, "view", item, 1.0, Timestamp::now());
    }
    let score_after_retries = db
        .read_decay_score(item, "view", 0)
        .expect("read local decay score after retries");
    assert!(
        score_after_retries.is_none() || score_after_retries == Some(0.0),
        "retries of a fail-closed op must not accumulate any local weight; got {score_after_retries:?}"
    );
}

/// Companion: when every resolved assignment is local (normal, non-migrating
/// tenant) the local write still lands — fail-closed only triggers on a
/// non-local assignment.
#[test]
fn signal_for_tenant_all_local_writes_succeed() {
    let db = open_db_with_view_signal();

    let item = EntityId::new(7);
    db.signal_for_tenant(TenantId::DEFAULT, "view", item, 1.0, Timestamp::now())
        .expect("a fully-local tenant write must succeed");

    let score = db
        .read_decay_score(item, "view", 0)
        .expect("read local decay score")
        .expect("a local write must produce a decay score");
    assert!(
        score > 0.0,
        "local write must accumulate a positive score, got {score}"
    );
}
