//! L5 tests for the embedder-configuration use cases (LOT C2).
//!
//! Ports are faked in-memory so the use cases run without any I/O:
//! - [`InMemoryEmbedderProfileStore`] — an [`EmbedderProfileStore`] backed by a
//!   `Vec` with the same upsert/delete-NotFound semantics as the real
//!   `FsEmbedderProfileStore`,
//! - [`FixedEnvInspector`] — an [`EmbedderEnvInspector`] returning a constant
//!   [`EmbedderEnvReport`] (best-effort port, never errors).

use std::sync::{Arc, Mutex};

use async_trait::async_trait;

use domain::ports::{EmbedderEnvInspector, EmbedderEnvReport, EmbedderProfileStore, StoreError};
use domain::profile::{EmbedderProfile, EmbedderStrategy};

use application::{
    DeleteEmbedderProfile, DeleteEmbedderProfileInput, DescribeEmbedderEngines,
    ListEmbedderProfiles, OnnxModelView, SaveEmbedderProfile, SaveEmbedderProfileInput,
};

// ---------------------------------------------------------------------------
// Fakes
// ---------------------------------------------------------------------------

/// In-memory [`EmbedderProfileStore`] mirroring the real store's semantics:
/// upsert-by-id (no duplicate), delete-absent ⇒ [`StoreError::NotFound`].
#[derive(Default, Clone)]
struct InMemoryEmbedderProfileStore(Arc<Mutex<Vec<EmbedderProfile>>>);

#[async_trait]
impl EmbedderProfileStore for InMemoryEmbedderProfileStore {
    async fn list(&self) -> Result<Vec<EmbedderProfile>, StoreError> {
        Ok(self.0.lock().unwrap().clone())
    }

    async fn save(&self, profile: &EmbedderProfile) -> Result<(), StoreError> {
        let mut v = self.0.lock().unwrap();
        if let Some(slot) = v.iter_mut().find(|p| p.id == profile.id) {
            *slot = profile.clone();
        } else {
            v.push(profile.clone());
        }
        Ok(())
    }

    async fn delete(&self, id: &str) -> Result<(), StoreError> {
        let mut v = self.0.lock().unwrap();
        let before = v.len();
        v.retain(|p| p.id != id);
        if v.len() == before {
            return Err(StoreError::NotFound);
        }
        Ok(())
    }
}

/// An [`EmbedderEnvInspector`] returning a fixed report (best-effort, infallible).
struct FixedEnvInspector(EmbedderEnvReport);

#[async_trait]
impl EmbedderEnvInspector for FixedEnvInspector {
    async fn inspect(&self) -> EmbedderEnvReport {
        self.0.clone()
    }
}

fn save_input(id: &str, name: &str, dimension: usize) -> SaveEmbedderProfileInput {
    SaveEmbedderProfileInput {
        id: id.to_owned(),
        name: name.to_owned(),
        strategy: EmbedderStrategy::LocalOnnx,
        model: Some("multilingual-e5-small".to_owned()),
        endpoint: None,
        api_key_env: None,
        dimension,
    }
}

// ---------------------------------------------------------------------------
// ListEmbedderProfiles / SaveEmbedderProfile
// ---------------------------------------------------------------------------

#[tokio::test]
async fn list_is_empty_then_contains_saved_profile() {
    let store = InMemoryEmbedderProfileStore::default();
    let list = ListEmbedderProfiles::new(Arc::new(store.clone()));
    let save = SaveEmbedderProfile::new(Arc::new(store.clone()));

    assert!(
        list.execute().await.unwrap().profiles.is_empty(),
        "no profile configured ⇒ empty list (default `none` posture)"
    );

    let saved = save
        .execute(save_input("local-onnx", "Local ONNX", 384))
        .await
        .unwrap();
    assert_eq!(saved.profile.id, "local-onnx");
    assert_eq!(saved.profile.dimension, 384);

    let listed = list.execute().await.unwrap().profiles;
    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0], saved.profile);
}

#[tokio::test]
async fn save_upserts_by_id_without_duplicating() {
    let store = InMemoryEmbedderProfileStore::default();
    let save = SaveEmbedderProfile::new(Arc::new(store.clone()));
    let list = ListEmbedderProfiles::new(Arc::new(store.clone()));

    save.execute(save_input("e", "before", 384)).await.unwrap();
    let updated = save.execute(save_input("e", "after", 768)).await.unwrap();

    let listed = list.execute().await.unwrap().profiles;
    assert_eq!(listed.len(), 1, "same id replaces, no duplicate");
    assert_eq!(listed[0], updated.profile);
    assert_eq!(listed[0].name, "after");
    assert_eq!(listed[0].dimension, 768);
}

#[tokio::test]
async fn save_invalid_dimension_is_invalid_and_writes_nothing() {
    let store = InMemoryEmbedderProfileStore::default();
    let save = SaveEmbedderProfile::new(Arc::new(store.clone()));
    let list = ListEmbedderProfiles::new(Arc::new(store.clone()));

    let err = save
        .execute(save_input("bad", "Bad", 0))
        .await
        .expect_err("dimension 0 must be rejected");
    assert_eq!(err.code(), "INVALID", "got {err:?}");

    assert!(
        list.execute().await.unwrap().profiles.is_empty(),
        "a rejected profile must not be persisted"
    );
}

#[tokio::test]
async fn save_empty_name_is_invalid_and_writes_nothing() {
    let store = InMemoryEmbedderProfileStore::default();
    let save = SaveEmbedderProfile::new(Arc::new(store.clone()));
    let list = ListEmbedderProfiles::new(Arc::new(store.clone()));

    let err = save
        .execute(save_input("id", "", 384))
        .await
        .expect_err("empty name must be rejected");
    assert_eq!(err.code(), "INVALID", "got {err:?}");

    assert!(
        list.execute().await.unwrap().profiles.is_empty(),
        "a rejected profile must not be persisted"
    );
}

// ---------------------------------------------------------------------------
// DeleteEmbedderProfile
// ---------------------------------------------------------------------------

#[tokio::test]
async fn delete_removes_existing_profile() {
    let store = InMemoryEmbedderProfileStore::default();
    let save = SaveEmbedderProfile::new(Arc::new(store.clone()));
    let delete = DeleteEmbedderProfile::new(Arc::new(store.clone()));
    let list = ListEmbedderProfiles::new(Arc::new(store.clone()));

    save.execute(save_input("a", "A", 384)).await.unwrap();
    save.execute(save_input("b", "B", 384)).await.unwrap();

    delete
        .execute(DeleteEmbedderProfileInput { id: "a".to_owned() })
        .await
        .unwrap();

    let listed = list.execute().await.unwrap().profiles;
    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0].id, "b");
}

#[tokio::test]
async fn delete_unknown_id_is_not_found() {
    let store = InMemoryEmbedderProfileStore::default();
    let delete = DeleteEmbedderProfile::new(Arc::new(store));

    let err = delete
        .execute(DeleteEmbedderProfileInput {
            id: "ghost".to_owned(),
        })
        .await
        .expect_err("deleting an unknown id errors");
    assert_eq!(
        err.code(),
        "NOT_FOUND",
        "StoreError::NotFound ⇒ AppError::NotFound, got {err:?}"
    );
}

// ---------------------------------------------------------------------------
// DescribeEmbedderEngines
// ---------------------------------------------------------------------------

fn onnx_view() -> OnnxModelView {
    OnnxModelView {
        id: "multilingual-e5-small".to_owned(),
        display_name: "Multilingual E5 Small".to_owned(),
        dimension: 384,
        approx_size_mb: 118,
        recommended: true,
    }
}

#[tokio::test]
async fn describe_engines_merges_catalogue_report_and_flags() {
    let report = EmbedderEnvReport {
        ollama_detected: true,
        onnx_cached_models: vec!["multilingual-e5-small".to_owned()],
    };
    let inspector = Arc::new(FixedEnvInspector(report));
    let describe = DescribeEmbedderEngines::new(
        inspector,
        vec![onnx_view()],
        /* vector_http_enabled */ true,
        /* vector_onnx_enabled */ false,
    );

    let view = describe.execute().await.unwrap();

    // Catalogue injected verbatim.
    assert_eq!(view.recommended_onnx, vec![onnx_view()]);
    // Report fields surfaced.
    assert!(view.ollama_detected);
    assert_eq!(view.onnx_cached_models, vec!["multilingual-e5-small"]);
    // Compiled-capability flags surfaced as injected.
    assert!(view.vector_http_enabled);
    assert!(!view.vector_onnx_enabled);
}

#[tokio::test]
async fn describe_engines_with_nothing_detected() {
    let inspector = Arc::new(FixedEnvInspector(EmbedderEnvReport {
        ollama_detected: false,
        onnx_cached_models: vec![],
    }));
    let describe = DescribeEmbedderEngines::new(inspector, vec![], false, true);

    let view = describe.execute().await.unwrap();

    assert!(view.recommended_onnx.is_empty());
    assert!(!view.ollama_detected);
    assert!(view.onnx_cached_models.is_empty());
    assert!(!view.vector_http_enabled);
    assert!(view.vector_onnx_enabled);
}
