use pmocache::db::DB;
use serde_json::{json, Value};
use tempfile::TempDir;

/// Crée une DB temporaire pour les tests
fn create_test_db() -> (TempDir, DB) {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db");
    let db = DB::init(&db_path).unwrap();
    (temp_dir, db)
}

#[test]
fn test_db_init() {
    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db");
    let db = DB::init(&db_path);
    assert!(db.is_ok());
    assert!(db_path.exists());
}

#[test]
fn test_add_and_get() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_123";
    let id = Some("test_id");
    let collection = Some("test_collection");

    // Ajouter une entrée
    let result = db.add(pk, id, collection);
    assert!(result.is_ok());

    // Récupérer l'entrée
    let entry = db.get(pk, false);
    assert!(entry.is_ok());

    let entry = entry.unwrap();
    assert_eq!(entry.pk, pk);
    assert_eq!(entry.id.as_deref(), id);
    assert_eq!(entry.collection.as_deref(), collection);
    assert_eq!(entry.hits, 0);
}

#[test]
fn test_add_with_metadata() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_456";
    let metadata = json!({
        "title": "Test Track",
        "artist": "Test Artist",
        "duration": 180,
        "bitrate": 320
    });

    // Ajouter avec métadonnées
    let result = db.add_with_metadata(pk, None, None, Some(&metadata));
    assert!(result.is_ok());

    // Récupérer l'entrée avec métadonnées
    let entry = db.get(pk, true).unwrap();
    assert_eq!(entry.pk, pk);
    assert!(entry.metadata.is_some());

    let stored_metadata = entry.metadata.unwrap();
    assert_eq!(stored_metadata["title"], "Test Track");
    assert_eq!(stored_metadata["artist"], "Test Artist");
    assert_eq!(stored_metadata["duration"], 180);
    assert_eq!(stored_metadata["bitrate"], 320);
}

#[test]
fn test_update_hit() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_789";
    db.add(pk, None, None).unwrap();

    // Récupérer l'entrée initiale
    let entry = db.get(pk, false).unwrap();
    let initial_hits = entry.hits;
    let initial_last_used = entry.last_used.clone();

    // Attendre un peu pour que le timestamp change
    std::thread::sleep(std::time::Duration::from_millis(10));

    // Mettre à jour le hit
    db.update_hit(pk).unwrap();

    // Vérifier que hits a augmenté et last_used a changé
    let entry = db.get(pk, false).unwrap();
    assert_eq!(entry.hits, initial_hits + 1);
    assert_ne!(entry.last_used, initial_last_used);
}

#[test]
fn test_delete() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_delete";
    db.add(pk, None, None).unwrap();

    // Vérifier que l'entrée existe
    assert!(db.get(pk, false).is_ok());

    // Supprimer l'entrée
    let result = db.delete(pk);
    assert!(result.is_ok());

    // Vérifier que l'entrée n'existe plus
    assert!(db.get(pk, false).is_err());
}

#[test]
fn test_get_by_collection() {
    let (_temp_dir, db) = create_test_db();

    let collection = "test_collection";

    // Ajouter plusieurs entrées dans la même collection
    db.add("pk1", None, Some(collection)).unwrap();
    db.add("pk2", None, Some(collection)).unwrap();
    db.add("pk3", None, Some("other_collection")).unwrap();

    // Récupérer les entrées de la collection
    let entries = db.get_by_collection(collection, false).unwrap();

    assert_eq!(entries.len(), 2);
    assert!(entries.iter().any(|e| e.pk == "pk1"));
    assert!(entries.iter().any(|e| e.pk == "pk2"));
    assert!(!entries.iter().any(|e| e.pk == "pk3"));
}

#[test]
fn test_delete_collection() {
    let (_temp_dir, db) = create_test_db();

    let collection = "test_collection_to_delete";

    db.add("pk1", None, Some(collection)).unwrap();
    db.add("pk2", None, Some(collection)).unwrap();
    db.add("pk3", None, Some("other_collection")).unwrap();

    // Supprimer la collection
    let result = db.delete_collection(collection);
    assert!(result.is_ok());

    // Vérifier que les entrées de la collection sont supprimées
    let entries = db.get_by_collection(collection, false).unwrap();
    assert_eq!(entries.len(), 0);

    // Vérifier que l'autre collection existe toujours
    assert!(db.get("pk3", false).is_ok());
}

#[test]
fn test_get_oldest() {
    let (_temp_dir, db) = create_test_db();

    // Ajouter plusieurs entrées avec des timestamps différents
    db.add("pk1", None, None).unwrap();
    std::thread::sleep(std::time::Duration::from_millis(10));

    db.add("pk2", None, None).unwrap();
    std::thread::sleep(std::time::Duration::from_millis(10));

    db.add("pk3", None, None).unwrap();

    // Mettre à jour le hit de pk1 pour le rendre plus récent
    std::thread::sleep(std::time::Duration::from_millis(10));
    db.update_hit("pk1").unwrap();

    // Récupérer les 2 plus anciennes entrées
    let oldest = db.get_oldest(2).unwrap();

    assert_eq!(oldest.len(), 2);
    // pk2 et pk3 devraient être les plus anciennes
    assert!(oldest.iter().any(|e| e.pk == "pk2"));
    assert!(oldest.iter().any(|e| e.pk == "pk3"));
}

#[test]
fn test_count() {
    let (_temp_dir, db) = create_test_db();

    assert_eq!(db.count().unwrap(), 0);

    db.add("pk1", None, None).unwrap();
    assert_eq!(db.count().unwrap(), 1);

    db.add("pk2", None, None).unwrap();
    assert_eq!(db.count().unwrap(), 2);

    db.delete("pk1").unwrap();
    assert_eq!(db.count().unwrap(), 1);
}

#[test]
fn test_purge() {
    let (_temp_dir, db) = create_test_db();

    db.add("pk1", None, None).unwrap();
    db.add("pk2", None, None).unwrap();
    db.add("pk3", None, None).unwrap();

    assert_eq!(db.count().unwrap(), 3);

    // Purger toutes les entrées
    let result = db.purge();
    assert!(result.is_ok());

    assert_eq!(db.count().unwrap(), 0);
}

#[test]
fn test_origin_url() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_url";
    let url = "https://example.com/test.flac";

    db.add(pk, None, None).unwrap();
    db.set_origin_url(pk, url).unwrap();

    let retrieved_url = db.get_origin_url(pk).unwrap();
    assert_eq!(retrieved_url, Some(url.to_string()));
}

#[test]
fn test_pk_collision_detection() {
    let (_temp_dir, db) = create_test_db();

    let pk = "collision_pk_123";
    let url1 = "https://example.com/file1.jpg";
    let url2 = "https://example.com/file2.jpg";

    // Ajouter le premier fichier avec le pk
    db.add(pk, None, None).unwrap();
    db.set_origin_url(pk, url1).unwrap();

    // Vérifier que l'URL est bien enregistrée
    let retrieved_url = db.get_origin_url(pk).unwrap();
    assert_eq!(retrieved_url, Some(url1.to_string()));

    // Tenter d'enregistrer une URL différente pour le même pk
    // Ceci devrait logger une erreur mais ne devrait pas échouer
    let result = db.set_origin_url(pk, url2);
    assert!(result.is_ok());

    // L'URL devrait être écrasée par la nouvelle (comportement actuel)
    let retrieved_url = db.get_origin_url(pk).unwrap();
    assert_eq!(retrieved_url, Some(url2.to_string()));
}

#[test]
fn test_get_pk_by_origin_url() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_456";
    let url = "https://example.com/cover.webp";

    // Ajouter une entrée avec URL
    db.add(pk, None, None).unwrap();
    db.set_origin_url(pk, url).unwrap();

    // Rechercher le pk par URL
    let found_pk = db.get_pk_by_origin_url(url).unwrap();
    assert_eq!(found_pk, Some(pk.to_string()));

    // Rechercher une URL qui n'existe pas
    let not_found = db
        .get_pk_by_origin_url("https://example.com/notfound.jpg")
        .unwrap();
    assert_eq!(not_found, None);
}

#[test]
fn test_get_from_id() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_by_id";
    let collection = "my_collection";
    let id = "my_unique_id";

    db.add(pk, Some(id), Some(collection)).unwrap();

    // Récupérer par (collection, id)
    let entry = db.get_from_id(collection, id, false).unwrap();
    assert_eq!(entry.pk, pk);
    assert_eq!(entry.id.as_deref(), Some(id));
    assert_eq!(entry.collection.as_deref(), Some(collection));
}

#[test]
fn test_does_collection_contain_id() {
    let (_temp_dir, db) = create_test_db();

    let collection = "my_collection";
    let id = "my_id";

    assert!(!db.does_collection_contain_id(collection, id));

    db.add("pk", Some(id), Some(collection)).unwrap();

    assert!(db.does_collection_contain_id(collection, id));
}

#[test]
fn test_get_pk_from_id() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_123";
    let collection = "my_collection";
    let id = "my_id";

    db.add(pk, Some(id), Some(collection)).unwrap();

    let retrieved_pk = db.get_pk_from_id(collection, id).unwrap();
    assert_eq!(retrieved_pk, pk);
}

#[test]
fn test_set_id() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk";
    db.add(pk, None, None).unwrap();

    // Définir l'id
    let new_id = "new_id";
    db.set_id(pk, new_id).unwrap();

    let entry = db.get(pk, false).unwrap();
    assert_eq!(entry.id.as_deref(), Some(new_id));
}

#[test]
fn test_metadata_types() {
    let (_temp_dir, db) = create_test_db();

    let pk = "test_pk_types";
    db.add(pk, None, None).unwrap();

    // Tester les différents types de métadonnées
    db.set_a_metadata(pk, "string_val", Value::String("test".to_string()))
        .unwrap();
    db.set_a_metadata(pk, "number_val", json!(42)).unwrap();
    db.set_a_metadata(pk, "bool_val", Value::Bool(true))
        .unwrap();
    db.set_a_metadata(pk, "null_val", Value::Null).unwrap();

    // Vérifier les valeurs
    assert_eq!(
        db.get_metadata_value(pk, "string_val").unwrap(),
        Some(Value::String("test".to_string()))
    );
    assert_eq!(
        db.get_metadata_value(pk, "number_val").unwrap(),
        Some(json!(42))
    );
    assert_eq!(
        db.get_metadata_value(pk, "bool_val").unwrap(),
        Some(Value::Bool(true))
    );
    assert_eq!(
        db.get_metadata_value(pk, "null_val").unwrap(),
        Some(Value::Null)
    );
}
