//! Tests for `add_sedimentree` — verifying that fragments and loose commits
//! survive the full pipeline: sign → store → in-memory tree → minimize →
//! fingerprint summary.

#![allow(
    clippy::cast_precision_loss,
    clippy::expect_used,
    clippy::indexing_slicing,
    clippy::map_unwrap_or,
    clippy::missing_const_for_fn,
    clippy::panic,
    clippy::redundant_closure_for_method_calls,
    clippy::unwrap_used,
    unused_variables
)]
//!
//! These tests reproduce the ingestion scenario where a client uploads
//! fragments + loose commits and the server must retain all of them.

use std::collections::BTreeSet;

use sedimentree_core::{
    blob::{Blob, BlobMeta},
    commit::CountLeadingZeroBytes,
    crypto::fingerprint::FingerprintSeed,
    fragment::{Fragment, checkpoint::Checkpoint},
    id::SedimentreeId,
    loose_commit::{LooseCommit, id::CommitId},
    sedimentree::Sedimentree,
};
use subduction_core::connection::test_utils::new_test_subduction;
use testresult::TestResult;

fn make_sed_id(seed: u8) -> SedimentreeId {
    SedimentreeId::new([seed; 32])
}

fn make_blob(seed: u8) -> Blob {
    let data: Vec<u8> = (0..64).map(|i| seed.wrapping_add(i)).collect();
    Blob::new(data)
}

/// Create a fragment with the given head depth and boundary digests.
fn make_fragment(
    sed_id: SedimentreeId,
    head_byte: u8,
    boundary_bytes: &[u8],
    checkpoint_bytes: &[u8],
    blob_seed: u8,
) -> (Fragment, Blob) {
    let head = CommitId::new({
        let mut h = [head_byte; 32];
        // Ensure head has depth > 0 (first byte == 0x00 for depth 1)
        h[0] = 0x00;
        h
    });
    let boundary: BTreeSet<CommitId> = boundary_bytes
        .iter()
        .map(|b| {
            let mut arr = [*b; 32];
            arr[0] = 0x00;
            arr[1] = 0x00; // depth 2 for boundary
            CommitId::new(arr)
        })
        .collect();
    let checkpoints: BTreeSet<Checkpoint> = checkpoint_bytes
        .iter()
        .map(|b| Checkpoint::new(CommitId::new([*b; 32])))
        .collect();
    let blob = make_blob(blob_seed);
    let fragment = Fragment::from_parts(sed_id, head, boundary, checkpoints, BlobMeta::new(&blob));
    (fragment, blob)
}

/// Create a loose commit with the given parents.
fn make_loose_commit(
    sed_id: SedimentreeId,
    parents: &[CommitId],
    blob_seed: u8,
) -> (LooseCommit, Blob) {
    let parent_set: BTreeSet<CommitId> = parents.iter().copied().collect();
    let blob = make_blob(blob_seed);
    let commit = LooseCommit::new(
        sed_id,
        CommitId::new([blob_seed; 32]),
        parent_set,
        BlobMeta::new(&blob),
    );
    (commit, blob)
}

/// After `add_sedimentree`, `get_commits` and `get_fragments` should return
/// all items that were added.
#[tokio::test]
async fn add_sedimentree_stores_all_items() -> TestResult {
    let (sd, _listener, _manager) = new_test_subduction();
    let sed_id = make_sed_id(0x10);

    // 2 fragments + 5 loose commits
    let (frag1, frag1_blob) = make_fragment(sed_id, 0x01, &[0xA0], &[], 1);
    let (frag2, frag2_blob) = make_fragment(sed_id, 0x02, &[0xB0], &[], 2);

    // Loose commits with parents pointing to fragment heads (correctly remapped)
    let frag1_head = frag1.head();
    let frag2_head = frag2.head();
    let (c1, c1_blob) = make_loose_commit(sed_id, &[frag1_head], 10);
    let (c2, c2_blob) = make_loose_commit(sed_id, &[frag1_head], 11);
    let (c3, c3_blob) = make_loose_commit(sed_id, &[frag2_head], 12);
    let (c4, c4_blob) = make_loose_commit(sed_id, &[c1.head()], 13);
    let (c5, c5_blob) = make_loose_commit(sed_id, &[c2.head(), c3.head()], 14);

    let fragments = vec![frag1.clone(), frag2.clone()];
    let commits = vec![c1.clone(), c2.clone(), c3.clone(), c4.clone(), c5.clone()];
    let blobs = vec![
        frag1_blob, frag2_blob, c1_blob, c2_blob, c3_blob, c4_blob, c5_blob,
    ];

    let sedimentree = Sedimentree::new(fragments.clone(), commits.clone());
    sd.add_sedimentree(sed_id, sedimentree, blobs).await?;

    // Check in-memory state
    let stored_commits = sd.get_commits(sed_id).await;
    let stored_fragments = sd.get_fragments(sed_id).await;

    assert!(
        stored_commits.is_some(),
        "commits should exist after add_sedimentree"
    );
    assert!(
        stored_fragments.is_some(),
        "fragments should exist after add_sedimentree"
    );

    let commit_count = stored_commits.as_ref().map(Vec::len).unwrap_or(0);
    let fragment_count = stored_fragments.as_ref().map(Vec::len).unwrap_or(0);

    assert_eq!(commit_count, 5, "all 5 loose commits should be stored");
    assert_eq!(fragment_count, 2, "both fragments should be stored");

    Ok(())
}

/// After `add_sedimentree`, the in-memory tree should survive `minimize`
/// without losing any items, PROVIDED that loose commit parents point to
/// fragment heads (not interior members).
#[tokio::test]
async fn add_sedimentree_survives_minimize() -> TestResult {
    let (sd, _listener, _manager) = new_test_subduction();
    let sed_id = make_sed_id(0x20);

    let (frag1, frag1_blob) = make_fragment(sed_id, 0x01, &[0xA0], &[], 1);
    let frag1_head = frag1.head();

    // Loose commits whose parents point to fragment HEAD (not interior members)
    let (c1, c1_blob) = make_loose_commit(sed_id, &[frag1_head], 10);
    let (c2, c2_blob) = make_loose_commit(sed_id, &[frag1_head], 11);
    let (c3, c3_blob) = make_loose_commit(sed_id, &[c1.head(), c2.head()], 12);

    let sedimentree = Sedimentree::new(vec![frag1.clone()], vec![c1, c2, c3]);
    let blobs = vec![frag1_blob, c1_blob, c2_blob, c3_blob];

    sd.add_sedimentree(sed_id, sedimentree, blobs).await?;

    // Verify all items survive in the in-memory tree.
    let commits = sd.get_commits(sed_id).await.unwrap_or_default();
    let fragments = sd.get_fragments(sed_id).await.unwrap_or_default();

    assert_eq!(commits.len(), 3, "all 3 loose commits should survive");
    assert_eq!(fragments.len(), 1, "fragment should survive");

    Ok(())
}

/// Loose commits whose parents point to interior fragment members
/// (checkpoints) survive `minimize` — they are _not_ pruned. The parent
/// remapping in `ingest_automerge` (which rewrites interior-member parents
/// to the fragment head) is a correctness optimization for topsort
/// ordering, not a requirement for `minimize` data retention.
#[test]
fn minimize_keeps_loose_commits_with_interior_parents() {
    let sed_id = make_sed_id(0x30);

    // Fragment with interior member at [0x50; 32]
    let interior_member = CommitId::new([0x50; 32]);

    let (frag1, _frag1_blob) = make_fragment(sed_id, 0x01, &[0xA0], &[0x50], 1);

    // Loose commit whose parent points to an interior member of the fragment
    let (c1, _c1_blob) = make_loose_commit(sed_id, &[interior_member], 10);

    let tree = Sedimentree::new(vec![frag1], vec![c1]);
    assert_eq!(
        tree.loose_commits().count(),
        1,
        "pre-minimize: 1 loose commit"
    );

    let minimized = tree.minimize(&CountLeadingZeroBytes);
    assert_eq!(
        minimized.loose_commits().count(),
        1,
        "minimize must retain loose commits even when parents are interior fragment members"
    );
}

/// The fingerprint summary should include all items from the in-memory tree.
/// This verifies the full flow: `add_sedimentree` → `fingerprint_summarize`.
#[tokio::test]
async fn fingerprint_summary_has_all_items_after_add_sedimentree() -> TestResult {
    let (sd, _listener, _manager) = new_test_subduction();
    let sed_id = make_sed_id(0x40);

    let (frag1, frag1_blob) = make_fragment(sed_id, 0x01, &[0xA0], &[], 1);
    let frag1_head = frag1.head();

    // 10 loose commits with parents pointing to fragment head
    let mut commits = Vec::new();
    let mut blobs = vec![frag1_blob];
    for i in 0..10u8 {
        let (c, b) = make_loose_commit(sed_id, &[frag1_head], 100 + i);
        commits.push(c);
        blobs.push(b);
    }

    let sedimentree = Sedimentree::new(vec![frag1], commits);

    // Check fingerprint summary BEFORE adding to subduction
    let seed = FingerprintSeed::new(123, 456);
    let summary = sedimentree.fingerprint_summarize(&seed);
    // 10 loose commits + 1 fragment head + 1 fragment boundary = 12 commit fingerprints.
    // Fragment head/boundary are included as coverage fingerprints to prevent
    // the remote from re-sending commits already inside our fragments.
    assert_eq!(
        summary.commit_fingerprints().len(),
        12,
        "pre-add: summary should have 10 commits + 2 coverage (fragment head + boundary)"
    );
    assert_eq!(
        summary.fragment_fingerprints().len(),
        1,
        "pre-add: summary should have 1 fragment"
    );

    // Now check after minimize
    let minimized = sedimentree.minimize(&CountLeadingZeroBytes);
    let minimized_summary = minimized.fingerprint_summarize(&seed);
    assert_eq!(
        minimized_summary.commit_fingerprints().len(),
        12,
        "post-minimize: summary should still have 10 commits + 2 coverage"
    );
    assert_eq!(
        minimized_summary.fragment_fingerprints().len(),
        1,
        "post-minimize: summary should still have 1 fragment"
    );

    Ok(())
}
