use super::*;

const K: usize = 11;
const M: usize = 5;

/// Build a `QueryBatch` from raw FASTA text, going through the same
/// `Rope` + `parse_chunk` path `process_chunk` uses — avoids hand-building a
/// `normalized` `Rope`, which is an implementation detail of `obiread`.
///
/// `obikseq`'s global K/M params are thread-local under `test-utils` (see
/// `obikseq::params`), so setting them here is per-test-thread and does not
/// need coordination with other tests.
fn batch_from_fasta(fasta: &str, k: usize, n_partitions: usize) -> QueryBatch {
    obikseq::set_k(k);
    obikseq::set_m(M);
    let mut rope = Rope::new(Some("text/fasta"));
    rope.push(fasta.as_bytes().to_vec());
    let records = parse_chunk(&rope, k);
    QueryBatch::from_records(records, k, 6, 0.7, n_partitions)
}

fn total_occurrences(batch: &QueryBatch) -> u64 {
    batch.n_kmers.iter().map(|&n| n as u64).sum()
}

fn total_unique_kmers(batch: &QueryBatch) -> u64 {
    batch.by_partition.iter().map(|m| m.len() as u64).sum()
}

// A 60 bp sequence, arbitrary but fixed — no attempt is made to prove it is
// free of internal k=11 repeats; the tests below only rely on inequalities
// that hold regardless (see each test's comment).
const SEQ: &str = "CATTAGCGTACCTGATCAGGTTACAGCTTAGGCATCCAGTTGACCATGACTGGACTTAGC";

#[test]
fn single_sequence_yields_plausible_kmer_counts() {
    // A single record can still contain internal repeats (SEQ isn't
    // guaranteed repeat-free at k=11) — this only checks the batch is
    // internally consistent, not a specific dedup ratio. The cross-record
    // tests below make the actual, unconditional dedup claims.
    let fasta = format!(">r1\n{SEQ}\n");
    let batch = batch_from_fasta(&fasta, K, 1);

    assert_eq!(batch.ids, vec!["r1".to_string()]);
    let occurrences = total_occurrences(&batch);
    let unique = total_unique_kmers(&batch);
    assert!(occurrences > 0, "sequence should yield at least one k-mer");
    assert!(unique > 0 && unique <= occurrences);
}

#[test]
fn duplicated_sequence_across_records_deduplicates() {
    // Two records with byte-identical sequences: every k-mer in record 1
    // exactly duplicates one in record 0, so unique kmers <= n_kmers[0],
    // strictly less than the summed occurrences (2 * n_kmers[0]) as long as
    // the sequence yields at least one k-mer. This holds regardless of
    // whether SEQ has internal repeats.
    let fasta = format!(">r1\n{SEQ}\n>r2\n{SEQ}\n");
    let batch = batch_from_fasta(&fasta, K, 1);

    assert_eq!(batch.ids.len(), 2);
    let occurrences = total_occurrences(&batch);
    let unique = total_unique_kmers(&batch);

    assert!(batch.n_kmers[0] > 0);
    assert_eq!(occurrences, batch.n_kmers[0] as u64 + batch.n_kmers[1] as u64);
    assert!(
        unique <= batch.n_kmers[0] as u64,
        "identical sequences must not produce more unique k-mers than one copy has"
    );
    assert!(
        unique < occurrences,
        "k-mer-level dedup must collapse at least the cross-record duplication"
    );
}

#[test]
fn duplicated_sequence_broadcasts_to_both_seq_indices() {
    // Stronger than the ratio check above: pick any k-mer that hit in both
    // records and confirm its occurrence list actually references both
    // seq_idx 0 and seq_idx 1 — this is the specific new capability (dedup
    // reaching across records/superkmers), not just a smaller unique count.
    let fasta = format!(">r1\n{SEQ}\n>r2\n{SEQ}\n");
    let batch = batch_from_fasta(&fasta, K, 1);

    let shared = batch.by_partition[0]
        .values()
        .find(|descs| descs.iter().any(|d| d.seq_idx == 0) && descs.iter().any(|d| d.seq_idx == 1));

    assert!(
        shared.is_some(),
        "expected at least one k-mer shared between the two identical records"
    );
}

#[test]
fn empty_records_yield_empty_batch() {
    let batch = batch_from_fasta("", K, 1);
    assert!(batch.ids.is_empty());
    assert_eq!(total_occurrences(&batch), 0);
    assert_eq!(total_unique_kmers(&batch), 0);
}

#[test]
fn partition_routing_is_a_pure_function_of_the_kmer() {
    // With n_partitions=4, every occurrence of a given k-mer must land in
    // the same partition bucket as every other occurrence of that k-mer
    // (partition routing is derived from the minimizer, shared by
    // definition among instances of the same k-mer's containing superkmer
    // in this test's single-sequence-pair setup).
    let fasta = format!(">r1\n{SEQ}\n>r2\n{SEQ}\n");
    let batch = batch_from_fasta(&fasta, K, 4);

    let total_unique: u64 = batch.by_partition.iter().map(|m| m.len() as u64).sum();
    assert!(total_unique > 0);

    // No k-mer key appears in more than one partition's map.
    let mut seen: std::collections::HashSet<CanonicalKmer> = std::collections::HashSet::new();
    for map in &batch.by_partition {
        for kmer in map.keys() {
            assert!(seen.insert(*kmer), "k-mer routed to more than one partition");
        }
    }
}

// ── sparse_findere_for_genome vs. a dense reference implementation ──────────
//
// No property-testing crate (proptest/quickcheck) is a workspace dependency
// (checked before writing this — not adding one for a single test module,
// per this project's dependency-approval rule). A tiny deterministic xorshift
// PRNG, std-only, stands in for one.

/// Faithful reimplementation of the pre-phase-5 dense sliding-window scan —
/// the algorithm `sparse_findere_for_genome` replaced — used here only as a
/// correctness oracle, not in production code. Operates on one genome's
/// hits across possibly many sequences, exactly like the sparse version.
fn dense_reference_findere(
    hits: &[(u32, u32, u32)],
    seq_lens: &[usize],
    z: usize,
    presence: bool,
    threshold: u32,
) -> Vec<(u32, u32, u32)> {
    let mut by_seq: Vec<Vec<u32>> = seq_lens.iter().map(|&n| vec![0u32; n]).collect();
    for &(seq, pos, val) in hits {
        by_seq[seq as usize][pos as usize] = val;
    }

    let mut confirmed = Vec::new();
    for (seq_idx, values) in by_seq.iter().enumerate() {
        let n = values.len();
        let mut dq: std::collections::VecDeque<(usize, u32)> = std::collections::VecDeque::new();
        for i in 0..n {
            let v_i = values[i];
            while dq.front().map_or(false, |&(f, _)| f + z <= i) {
                dq.pop_front();
            }
            while dq.back().map_or(false, |&(_, v)| v >= v_i) {
                dq.pop_back();
            }
            dq.push_back((i, v_i));
            if i + 1 >= z {
                let win_min = dq.front().unwrap().1;
                if win_min > 0 {
                    let pos_out = (i + 1 - z) as u32;
                    let c = if presence { u32::from(win_min >= threshold) } else { win_min };
                    confirmed.push((seq_idx as u32, pos_out, c));
                }
            }
        }
    }
    confirmed
}

/// Minimal std-only xorshift64 PRNG — deterministic, seedable, no dependency.
struct Xorshift64(u64);
impl Xorshift64 {
    fn next(&mut self) -> u64 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        self.0
    }
    fn range(&mut self, n: u32) -> u32 {
        (self.next() % n as u64) as u32
    }
}

#[test]
fn sparse_findere_matches_dense_reference_on_random_inputs() {
    let mut rng = Xorshift64(0x5eed_5eed_5eed_5eedu64);

    for case in 0..200 {
        let n_seqs = 1 + rng.range(4) as usize;
        let seq_lens: Vec<usize> = (0..n_seqs).map(|_| 1 + rng.range(30) as usize).collect();
        let z = 1 + rng.range(4) as usize;
        let presence = rng.range(2) == 0;
        let threshold = 1 + rng.range(3);

        // Sparse density varies across cases, including edge cases (empty,
        // fully dense) — deliberately not uniform, to stress both few-hits
        // and many-overlapping-runs scenarios.
        let density = rng.range(101);
        let mut hits: Vec<(u32, u32, u32)> = Vec::new();
        for (seq_idx, &len) in seq_lens.iter().enumerate() {
            for pos in 0..len {
                if rng.range(100) < density {
                    let val = 1 + rng.range(5); // never 0 — matches QueryHit::Value's invariant
                    hits.push((seq_idx as u32, pos as u32, val));
                }
            }
        }

        let mut sparse_input = hits.clone();
        let (mut sparse_result, _, _) =
            sparse_findere_for_genome(&mut sparse_input, z, presence, threshold);
        let mut dense_result = dense_reference_findere(&hits, &seq_lens, z, presence, threshold);

        sparse_result.sort_unstable();
        dense_result.sort_unstable();

        assert_eq!(
            sparse_result, dense_result,
            "case {case}: n_seqs={n_seqs} seq_lens={seq_lens:?} z={z} presence={presence} \
             threshold={threshold} density={density} hits={hits:?}"
        );
    }
}
