//! Tests for persistent visual index.

#![allow(clippy::expect_used)]

use super::*;
use crate::visual_index::bk_tree::BkTreeVisualIndex;
use crate::visual_index::hamming::{hamming_distance, VisualIndex};
use std::io::Write;
use stemedb_core::types::{Hash, PHash};
use tempfile::TempDir;

fn make_phash(bytes: [u8; 8]) -> PHash {
    bytes
}

#[test]
fn test_hamming_distance_zero() {
    let a = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
    let b = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
    assert_eq!(hamming_distance(&a, &b), 0);
}

#[test]
fn test_hamming_distance_max() {
    let a = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
    let b = make_phash([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
    assert_eq!(hamming_distance(&a, &b), 64);
}

#[test]
fn test_hamming_distance_partial() {
    let a = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
    let b = make_phash([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
    assert_eq!(hamming_distance(&a, &b), 1);

    let c = make_phash([0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
    assert_eq!(hamming_distance(&a, &c), 8);
}

#[test]
fn test_create_index() {
    let index = BkTreeVisualIndex::new();
    assert!(index.is_empty().expect("is_empty"));
    assert_eq!(index.len().expect("len"), 0);
}

#[test]
fn test_insert_and_search_exact() {
    let index = BkTreeVisualIndex::new();

    let hash1: Hash = [1u8; 32];
    let phash1 = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);

    index.insert(&hash1, &phash1).expect("insert");
    assert_eq!(index.len().expect("len"), 1);

    // Exact match search
    let results = index.search(&phash1, 0).expect("search");
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].0, hash1);
    assert_eq!(results[0].1, 0);
}

#[test]
fn test_search_within_threshold() {
    let index = BkTreeVisualIndex::new();

    let hash1: Hash = [1u8; 32];
    let hash2: Hash = [2u8; 32];
    let hash3: Hash = [3u8; 32];

    // Insert three hashes with varying distances
    let phash1 = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // All zeros
    let phash2 = make_phash([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // 1 bit diff
    let phash3 = make_phash([0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // 16 bits diff

    index.insert(&hash1, &phash1).expect("insert 1");
    index.insert(&hash2, &phash2).expect("insert 2");
    index.insert(&hash3, &phash3).expect("insert 3");

    // Search with threshold 5 from all-zeros
    let results = index.search(&phash1, 5).expect("search");
    assert_eq!(results.len(), 2); // hash1 (0) and hash2 (1)
    assert_eq!(results[0].0, hash1); // Exact match first
    assert_eq!(results[0].1, 0);
    assert_eq!(results[1].0, hash2);
    assert_eq!(results[1].1, 1);

    // Search with threshold 20
    let results = index.search(&phash1, 20).expect("search");
    assert_eq!(results.len(), 3); // All three
}

#[test]
fn test_search_no_matches() {
    let index = BkTreeVisualIndex::new();

    let hash1: Hash = [1u8; 32];
    let phash1 = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);

    index.insert(&hash1, &phash1).expect("insert");

    // Search with very different hash and threshold 0
    let query = make_phash([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
    let results = index.search(&query, 0).expect("search");
    assert!(results.is_empty());
}

#[test]
fn test_search_empty_index() {
    let index = BkTreeVisualIndex::new();
    let query = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
    let results = index.search(&query, 10).expect("search");
    assert!(results.is_empty());
}

#[test]
fn test_idempotent_insert() {
    let index = BkTreeVisualIndex::new();

    let hash: Hash = [1u8; 32];
    let phash1 = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
    let phash2 = make_phash([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);

    index.insert(&hash, &phash1).expect("insert 1");
    index.insert(&hash, &phash1).expect("insert 2"); // Same
    index.insert(&hash, &phash2).expect("insert 3"); // Same hash, different phash (ignored)

    assert_eq!(index.len().expect("len"), 1);

    // Should still find by original phash
    let results = index.search(&phash1, 0).expect("search");
    assert_eq!(results.len(), 1);
}

#[test]
fn test_contains() {
    let index = BkTreeVisualIndex::new();

    let hash1: Hash = [1u8; 32];
    let hash2: Hash = [2u8; 32];
    let phash = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);

    assert!(!index.contains(&hash1));

    index.insert(&hash1, &phash).expect("insert");

    assert!(index.contains(&hash1));
    assert!(!index.contains(&hash2));
}

#[test]
fn test_results_sorted_by_distance() {
    let index = BkTreeVisualIndex::new();

    // Insert hashes at various distances from query
    let query = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);

    let hash1: Hash = [1u8; 32];
    let hash2: Hash = [2u8; 32];
    let hash3: Hash = [3u8; 32];

    // Different distances: 5, 2, 8 bits
    let phash1 = make_phash([0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // 5 bits
    let phash2 = make_phash([0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // 2 bits
    let phash3 = make_phash([0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // 8 bits

    index.insert(&hash1, &phash1).expect("insert");
    index.insert(&hash2, &phash2).expect("insert");
    index.insert(&hash3, &phash3).expect("insert");

    let results = index.search(&query, 10).expect("search");
    assert_eq!(results.len(), 3);

    // Should be sorted by distance: 2, 5, 8
    assert_eq!(results[0].1, 2);
    assert_eq!(results[1].1, 5);
    assert_eq!(results[2].1, 8);
}

#[test]
fn test_threshold_clamped_to_64() {
    let index = BkTreeVisualIndex::new();

    let hash: Hash = [1u8; 32];
    let phash = make_phash([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);

    index.insert(&hash, &phash).expect("insert");

    // Threshold > 64 should be clamped to 64
    let query = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
    let results = index.search(&query, 100).expect("search"); // 100 > 64

    // Should find the hash since 64 is max distance
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].1, 64);
}

#[test]
fn test_larger_scale() {
    let index = BkTreeVisualIndex::new();

    // Insert 1000 hashes
    for i in 0..1000u64 {
        let mut hash: Hash = [0u8; 32];
        hash[0..8].copy_from_slice(&i.to_le_bytes());

        // Create pseudo-random phash from i
        let phash = make_phash(i.to_le_bytes());

        index.insert(&hash, &phash).expect("insert");
    }

    assert_eq!(index.len().expect("len"), 1000);

    // Search for exact match of hash 500
    let query = make_phash(500u64.to_le_bytes());
    let results = index.search(&query, 0).expect("search");

    assert_eq!(results.len(), 1);
    let mut expected_hash: Hash = [0u8; 32];
    expected_hash[0..8].copy_from_slice(&500u64.to_le_bytes());
    assert_eq!(results[0].0, expected_hash);
}

#[test]
fn test_default_impl() {
    let index = BkTreeVisualIndex::default();
    assert!(index.is_empty().expect("is_empty"));
}

// ── Persistent Visual Index Tests ────────────────────────────────────

#[test]
fn test_create_persistent_index() {
    let tmp = TempDir::new().expect("create temp dir");
    let checkpoint_path = tmp.path().join("visual_index.checkpoint");

    let index = PersistentVisualIndex::new(checkpoint_path);
    assert!(index.is_empty().expect("is_empty"));
    assert_eq!(index.len().expect("len"), 0);
}

#[test]
fn test_persistent_insert_and_search() {
    let tmp = TempDir::new().expect("create temp dir");
    let checkpoint_path = tmp.path().join("visual_index.checkpoint");

    let index = PersistentVisualIndex::new(checkpoint_path);

    let hash1: Hash = [1u8; 32];
    let phash1 = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);

    index.insert(&hash1, &phash1).expect("insert");
    assert_eq!(index.len().expect("len"), 1);
    assert!(index.contains(&hash1));

    let results = index.search(&phash1, 0).expect("search");
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].0, hash1);
}

#[test]
fn test_checkpoint_roundtrip() {
    let tmp = TempDir::new().expect("create temp dir");
    let checkpoint_path = tmp.path().join("visual_index.checkpoint");

    // Create and populate index
    {
        let index = PersistentVisualIndex::new(checkpoint_path.clone());

        let hash1: Hash = [1u8; 32];
        let hash2: Hash = [2u8; 32];
        let phash1 = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
        let phash2 = make_phash([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);

        index.insert(&hash1, &phash1).expect("insert 1");
        index.insert(&hash2, &phash2).expect("insert 2");

        VisualIndex::checkpoint(&index).expect("checkpoint");
    }

    // Reopen and verify
    {
        let config = PersistentVisualIndexConfig {
            checkpoint_path: checkpoint_path.clone(),
            ..Default::default()
        };
        let index = PersistentVisualIndex::open(config).expect("open");

        assert_eq!(index.len().expect("len"), 2);

        let hash1: Hash = [1u8; 32];
        let hash2: Hash = [2u8; 32];
        assert!(index.contains(&hash1));
        assert!(index.contains(&hash2));

        // Search should work
        let phash1 = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
        let results = index.search(&phash1, 0).expect("search");
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].0, hash1);
    }
}

#[test]
fn test_crc_corruption_detected() {
    let tmp = TempDir::new().expect("create temp dir");
    let checkpoint_path = tmp.path().join("visual_index.checkpoint");

    // Create checkpoint
    {
        let index = PersistentVisualIndex::new(checkpoint_path.clone());
        let hash: Hash = [1u8; 32];
        let phash = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
        index.insert(&hash, &phash).expect("insert");
        VisualIndex::checkpoint(&index).expect("checkpoint");
    }

    // Corrupt the file
    {
        use std::fs::OpenOptions;
        use std::io::{Seek, SeekFrom};

        let mut file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(&checkpoint_path)
            .expect("open for corruption");

        // Corrupt byte at offset 20 (inside payload)
        file.seek(SeekFrom::Start(20)).expect("seek");
        file.write_all(&[0xFF]).expect("write corruption");
    }

    // Try to open - should fail
    let config = PersistentVisualIndexConfig { checkpoint_path, ..Default::default() };
    let result = PersistentVisualIndex::open(config);
    assert!(result.is_err());
}

#[test]
fn test_no_checkpoint_when_no_changes() {
    let tmp = TempDir::new().expect("create temp dir");
    let checkpoint_path = tmp.path().join("visual_index.checkpoint");

    let index = PersistentVisualIndex::new(checkpoint_path.clone());

    // Checkpoint with no changes should succeed but not create file
    VisualIndex::checkpoint(&index).expect("checkpoint empty");
    assert!(!checkpoint_path.exists());
}

#[test]
fn test_shutdown_writes_checkpoint() {
    let tmp = TempDir::new().expect("create temp dir");
    let checkpoint_path = tmp.path().join("visual_index.checkpoint");

    let index = PersistentVisualIndex::new(checkpoint_path.clone());

    let hash: Hash = [1u8; 32];
    let phash = make_phash([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);
    index.insert(&hash, &phash).expect("insert");

    index.shutdown().expect("shutdown");

    // File should exist
    assert!(checkpoint_path.exists());
}
