use std::sync::Arc;
use std::time::Duration;

use super::*;
use crate::entities::HardNegIndex;
use crate::replication::crdt::hlc::HlcTimestamp;
use crate::replication::crdt::{CrdtSignalState, LWWRegister};
use crate::replication::shard::ShardId;
use crate::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Window};
use crate::signals::ledger::types::NoopWalWriter;
use crate::signals::{SignalLedger, SignalTypeId};

/// 7-day half-life lambda (matches the standard "view" signal).
const LAMBDA: f64 = std::f64::consts::LN_2 / (7.0 * 24.0 * 3600.0);

fn test_schema() -> crate::schema::Schema {
    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();
    builder.build().unwrap()
}

fn make_ledger() -> Arc<SignalLedger> {
    Arc::new(SignalLedger::new(test_schema(), Box::new(NoopWalWriter)))
}

fn make_engine() -> (ReconciliationEngine, Arc<SignalLedger>, Arc<HardNegIndex>) {
    let ledger = make_ledger();
    let hard_neg = Arc::new(HardNegIndex::new());
    let engine = ReconciliationEngine::new(Arc::clone(&ledger), Arc::clone(&hard_neg));
    (engine, ledger, hard_neg)
}

fn ts(wall_ns: u64, logical: u32, node_id: u16) -> HlcTimestamp {
    HlcTimestamp {
        wall_ns,
        logical,
        node_id,
    }
}

// -- StateSnapshot construction --

#[test]
fn snapshot_empty_by_default() {
    let snap = StateSnapshot::new();
    assert_eq!(snap.signal_count(), 0);
    assert_eq!(snap.hardneg_count(), 0);
}

#[test]
fn snapshot_add_and_retrieve_signal() {
    let mut snap = StateSnapshot::new();
    let entity = EntityId::new(1);
    let sig_id = SignalTypeId::new(0);
    let state = CrdtSignalState::new(LAMBDA);
    snap.add_signal_state(entity, sig_id, state);

    assert_eq!(snap.signal_count(), 1);
    assert!(snap.signal_state((entity, sig_id)).is_some());
    assert!(snap.signal_state((EntityId::new(99), sig_id)).is_none());
}

#[test]
fn snapshot_add_and_retrieve_hardneg() {
    let mut snap = StateSnapshot::new();
    let user = EntityId::new(10);
    let item = EntityId::new(20);
    let mut reg = LWWRegister::empty();
    reg.write(HardNegAction::Hide, ts(1000, 0, 0));
    snap.add_hardneg_register(user, item, reg);

    assert_eq!(snap.hardneg_count(), 1);
    let retrieved = snap.hardneg_register((user, item)).unwrap();
    assert_eq!(retrieved.get(), Some(&HardNegAction::Hide));
}

// -- ReconciliationEngine::plan() --

#[test]
fn plan_empty_snapshots_produces_empty_plan() {
    let (engine, _, _) = make_engine();
    let plan = engine.plan(&StateSnapshot::new(), &StateSnapshot::new());
    assert!(plan.is_empty());
    assert_eq!(plan.operation_count(), 0);
}

#[test]
fn plan_merges_disjoint_signal_states() {
    let (engine, _, _) = make_engine();
    let entity = EntityId::new(1);
    let sig_id = SignalTypeId::new(0);
    let t = 1_000_000_000u64;

    // Local: node 0 contributed 1.0 at t
    let mut local_snap = StateSnapshot::new();
    let mut local_state = CrdtSignalState::new(LAMBDA);
    local_state.on_signal(ShardId(0), 1.0, t);
    local_snap.add_signal_state(entity, sig_id, local_state);

    // Remote: node 1 contributed 2.0 at t
    let mut remote_snap = StateSnapshot::new();
    let mut remote_state = CrdtSignalState::new(LAMBDA);
    remote_state.on_signal(ShardId(1), 2.0, t);
    remote_snap.add_signal_state(entity, sig_id, remote_state);

    let plan = engine.plan(&local_snap, &remote_snap);
    assert_eq!(plan.signal_merges.len(), 1);
    assert_eq!(plan.hardneg_resolutions.len(), 0);

    // Merged score at t should be ~3.0 (1.0 + 2.0, both at same time)
    let merged_score = plan.signal_merges[0].merged_state.decay_score(t);
    assert!(
        (merged_score - 3.0).abs() < 1e-6,
        "merged score {merged_score} should be ~3.0"
    );
}

#[test]
fn plan_includes_single_sided_signal_state() {
    let (engine, _, _) = make_engine();
    let entity = EntityId::new(1);
    let sig_id = SignalTypeId::new(0);
    let t = 1_000_000_000u64;

    // Only local has state.
    let mut local_snap = StateSnapshot::new();
    let mut local_state = CrdtSignalState::new(LAMBDA);
    local_state.on_signal(ShardId(0), 5.0, t);
    local_snap.add_signal_state(entity, sig_id, local_state);

    let remote_snap = StateSnapshot::new(); // empty

    let plan = engine.plan(&local_snap, &remote_snap);
    assert_eq!(plan.signal_merges.len(), 1);
    let merged_score = plan.signal_merges[0].merged_state.decay_score(t);
    assert!(
        (merged_score - 5.0).abs() < 1e-6,
        "single-sided score {merged_score} should be ~5.0"
    );
}

#[test]
fn plan_hardneg_lww_resolution_hide_wins_higher_hlc() {
    let (engine, _, _) = make_engine();
    let user = EntityId::new(10);
    let item = EntityId::new(20);

    // Local: hide at t=1000
    let mut local_snap = StateSnapshot::new();
    let mut local_reg = LWWRegister::empty();
    local_reg.write(HardNegAction::Hide, ts(1000, 0, 0));
    local_snap.add_hardneg_register(user, item, local_reg);

    // Remote: unhide at t=500 (earlier)
    let mut remote_snap = StateSnapshot::new();
    let mut remote_reg = LWWRegister::empty();
    remote_reg.write(HardNegAction::Unhide, ts(500, 0, 1));
    remote_snap.add_hardneg_register(user, item, remote_reg);

    let plan = engine.plan(&local_snap, &remote_snap);
    assert_eq!(plan.hardneg_resolutions.len(), 1);
    assert_eq!(
        plan.hardneg_resolutions[0].action,
        Some(HardNegAction::Hide),
        "hide at t=1000 should beat unhide at t=500"
    );
}

#[test]
fn plan_hardneg_lww_resolution_unhide_wins_higher_hlc() {
    let (engine, _, _) = make_engine();
    let user = EntityId::new(10);
    let item = EntityId::new(20);

    // Local: hide at t=500
    let mut local_snap = StateSnapshot::new();
    let mut local_reg = LWWRegister::empty();
    local_reg.write(HardNegAction::Hide, ts(500, 0, 0));
    local_snap.add_hardneg_register(user, item, local_reg);

    // Remote: unhide at t=1000 (later)
    let mut remote_snap = StateSnapshot::new();
    let mut remote_reg = LWWRegister::empty();
    remote_reg.write(HardNegAction::Unhide, ts(1000, 0, 1));
    remote_snap.add_hardneg_register(user, item, remote_reg);

    let plan = engine.plan(&local_snap, &remote_snap);
    assert_eq!(plan.hardneg_resolutions.len(), 1);
    assert_eq!(
        plan.hardneg_resolutions[0].action,
        Some(HardNegAction::Unhide),
        "unhide at t=1000 should beat hide at t=500"
    );
}

// -- ReconciliationEngine::apply() --

#[test]
fn apply_signal_merge_updates_ledger() {
    use crate::schema::Timestamp;

    let (engine, ledger, _) = make_engine();
    let entity = EntityId::new(42);
    let sig_id = SignalTypeId::new(0); // "view" is alphabetically first (only signal)
    let t = Timestamp::now().as_nanos();

    // Build a merged state with two nodes using a recent timestamp.
    let mut merged = CrdtSignalState::new(LAMBDA);
    merged.on_signal(ShardId(0), 3.0, t);
    merged.on_signal(ShardId(1), 7.0, t);

    let plan = MergePlan {
        signal_merges: vec![SignalMergeOp {
            entity_id: entity,
            signal_type_id: sig_id,
            merged_state: merged,
        }],
        hardneg_resolutions: vec![],
    };

    engine.apply(&plan).unwrap();

    // Verify the ledger entry was created and has a positive score.
    let entry = ledger.entries().get(&(entity, sig_id));
    assert!(
        entry.is_some(),
        "ledger should have an entry for the entity"
    );
    let score = entry.unwrap().hot.stored_score(0);
    assert!(score > 0.0, "stored score {score} should be positive");
}

#[test]
fn apply_hardneg_hide_adds_to_index() {
    let (engine, _, hard_neg) = make_engine();
    let user = EntityId::new(10);
    let item = EntityId::new(20);

    let plan = MergePlan {
        signal_merges: vec![],
        hardneg_resolutions: vec![HardNegResolutionOp {
            user_id: user,
            item_id: item,
            action: Some(HardNegAction::Hide),
        }],
    };

    engine.apply(&plan).unwrap();
    assert!(
        hard_neg.is_negative(10, 20),
        "item 20 should be hidden for user 10"
    );
}

#[test]
fn apply_hardneg_unhide_removes_from_index() {
    let (engine, _, hard_neg) = make_engine();

    // Pre-populate a hard negative.
    hard_neg.add(10, 20);
    assert!(hard_neg.is_negative(10, 20));

    let plan = MergePlan {
        signal_merges: vec![],
        hardneg_resolutions: vec![HardNegResolutionOp {
            user_id: EntityId::new(10),
            item_id: EntityId::new(20),
            action: Some(HardNegAction::Unhide),
        }],
    };

    engine.apply(&plan).unwrap();
    assert!(
        !hard_neg.is_negative(10, 20),
        "item 20 should no longer be hidden for user 10"
    );
}

#[test]
fn apply_is_idempotent() {
    use crate::schema::Timestamp;

    let (engine, ledger, hard_neg) = make_engine();
    let entity = EntityId::new(1);
    let sig_id = SignalTypeId::new(0);
    let t = Timestamp::now().as_nanos();

    let mut merged = CrdtSignalState::new(LAMBDA);
    merged.on_signal(ShardId(0), 5.0, t);

    let plan = MergePlan {
        signal_merges: vec![SignalMergeOp {
            entity_id: entity,
            signal_type_id: sig_id,
            merged_state: merged,
        }],
        hardneg_resolutions: vec![HardNegResolutionOp {
            user_id: EntityId::new(10),
            item_id: EntityId::new(20),
            action: Some(HardNegAction::Hide),
        }],
    };

    // Apply twice.
    engine.apply(&plan).unwrap();
    let score_first = ledger
        .entries()
        .get(&(entity, sig_id))
        .unwrap()
        .hot
        .stored_score(0);

    engine.apply(&plan).unwrap();
    let score_second = ledger
        .entries()
        .get(&(entity, sig_id))
        .unwrap()
        .hot
        .stored_score(0);

    // Scores should be very close (not exactly equal because Timestamp::now()
    // advances between the two apply calls, causing slightly different decay).
    // But the key invariant is that the second apply does not double the score.
    let ratio = score_second / score_first;
    assert!(
        (ratio - 1.0).abs() < 0.01,
        "idempotent apply: ratio {ratio} should be ~1.0 (first={score_first}, second={score_second})"
    );

    // Hard-neg should still be set (idempotent add).
    assert!(hard_neg.is_negative(10, 20));
}

// -- CrdtSignalState::from_node_contribution --

#[test]
fn from_node_contribution_produces_correct_score() {
    let state = CrdtSignalState::from_node_contribution(ShardId(0), 10.0, 1_000_000_000, LAMBDA);
    // At the same timestamp, score should be exactly 10.0.
    let score = state.decay_score(1_000_000_000);
    assert!(
        (score - 10.0).abs() < 1e-10,
        "from_node_contribution score {score} should be 10.0"
    );
}

#[test]
fn from_node_contribution_merges_with_other_nodes() {
    let a = CrdtSignalState::from_node_contribution(ShardId(0), 3.0, 1_000_000_000, LAMBDA);
    let b = CrdtSignalState::from_node_contribution(ShardId(1), 7.0, 1_000_000_000, LAMBDA);
    let mut merged = a.clone();
    merged.merge(&b);
    let score = merged.decay_score(1_000_000_000);
    assert!(
        (score - 10.0).abs() < 1e-10,
        "merged score {score} should be 10.0"
    );
}
