//! Tests for the declarative [`EmbedderProfile`] / [`EmbedderStrategy`] (LOT C,
//! étage 2 vectoriel): serde camelCase round-trip, the validated `new`
//! constructor, and the dependency-free `none()` default.

use domain::{DomainError, EmbedderProfile, EmbedderStrategy};

fn roundtrip<T>(value: &T) -> T
where
    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
    let json = serde_json::to_string(value).expect("serialize");
    let back: T = serde_json::from_str(&json).expect("deserialize");
    assert_eq!(&back, value, "round-trip mismatch via {json}");
    back
}

// ---------------------------------------------------------------------------
// EmbedderStrategy: camelCase wire form (`localOnnx` / `localServer` / `api` /
// `none`).
// ---------------------------------------------------------------------------

#[test]
fn strategy_serializes_camel_case() {
    assert_eq!(
        serde_json::to_string(&EmbedderStrategy::LocalOnnx).unwrap(),
        "\"localOnnx\""
    );
    assert_eq!(
        serde_json::to_string(&EmbedderStrategy::LocalServer).unwrap(),
        "\"localServer\""
    );
    assert_eq!(
        serde_json::to_string(&EmbedderStrategy::Api).unwrap(),
        "\"api\""
    );
    assert_eq!(
        serde_json::to_string(&EmbedderStrategy::None).unwrap(),
        "\"none\""
    );
}

#[test]
fn strategy_deserializes_camel_case() {
    assert_eq!(
        serde_json::from_str::<EmbedderStrategy>("\"localOnnx\"").unwrap(),
        EmbedderStrategy::LocalOnnx
    );
    assert_eq!(
        serde_json::from_str::<EmbedderStrategy>("\"localServer\"").unwrap(),
        EmbedderStrategy::LocalServer
    );
    assert_eq!(
        serde_json::from_str::<EmbedderStrategy>("\"api\"").unwrap(),
        EmbedderStrategy::Api
    );
    assert_eq!(
        serde_json::from_str::<EmbedderStrategy>("\"none\"").unwrap(),
        EmbedderStrategy::None
    );
}

#[test]
fn strategy_roundtrips() {
    for s in [
        EmbedderStrategy::LocalOnnx,
        EmbedderStrategy::LocalServer,
        EmbedderStrategy::Api,
        EmbedderStrategy::None,
    ] {
        roundtrip(&s);
    }
}

// ---------------------------------------------------------------------------
// EmbedderProfile: camelCase fields and optional-field skipping.
// ---------------------------------------------------------------------------

#[test]
fn profile_roundtrips_with_all_fields() {
    let p = EmbedderProfile::new(
        "local-onnx-minilm",
        "Local MiniLM",
        EmbedderStrategy::LocalOnnx,
        Some("all-MiniLM-L6-v2".to_owned()),
        Some("http://localhost:1234".to_owned()),
        Some("OPENAI_API_KEY".to_owned()),
        384,
    )
    .unwrap();
    let back = roundtrip(&p);
    assert_eq!(back.dimension, 384);
    assert_eq!(back.strategy, EmbedderStrategy::LocalOnnx);
}

#[test]
fn profile_uses_camel_case_keys_and_skips_none_options() {
    let p = EmbedderProfile::new(
        "id-1",
        "Name",
        EmbedderStrategy::Api,
        None,
        Some("https://api.example/embed".to_owned()),
        Some("MY_KEY".to_owned()),
        16,
    )
    .unwrap();
    let json = serde_json::to_string(&p).unwrap();
    assert!(
        json.contains("\"apiKeyEnv\":\"MY_KEY\""),
        "camelCase apiKeyEnv: {json}"
    );
    assert!(
        json.contains("\"strategy\":\"api\""),
        "camelCase strategy: {json}"
    );
    // `model` is None ⇒ skipped from the wire form.
    assert!(
        !json.contains("\"model\""),
        "None model must be skipped: {json}"
    );
}

#[test]
fn profile_deserializes_from_camel_case_document() {
    let json = r#"{
        "id": "srv",
        "name": "Server",
        "strategy": "localServer",
        "endpoint": "http://127.0.0.1:9000",
        "dimension": 512
    }"#;
    let p: EmbedderProfile = serde_json::from_str(json).unwrap();
    assert_eq!(p.id, "srv");
    assert_eq!(p.strategy, EmbedderStrategy::LocalServer);
    assert_eq!(p.endpoint.as_deref(), Some("http://127.0.0.1:9000"));
    assert_eq!(p.dimension, 512);
    assert!(p.model.is_none());
    assert!(p.api_key_env.is_none());
}

// ---------------------------------------------------------------------------
// none(): the dependency-free default.
// ---------------------------------------------------------------------------

#[test]
fn none_profile_has_none_strategy() {
    let p = EmbedderProfile::none();
    assert_eq!(p.strategy, EmbedderStrategy::None);
    assert_eq!(p.id, "none");
    assert!(
        p.dimension > 0,
        "even the none profile keeps a non-zero dimension"
    );
    // And it round-trips like any other profile.
    roundtrip(&p);
}

// ---------------------------------------------------------------------------
// new(): validation.
// ---------------------------------------------------------------------------

#[test]
fn new_rejects_empty_id() {
    assert!(matches!(
        EmbedderProfile::new("", "Name", EmbedderStrategy::None, None, None, None, 8),
        Err(DomainError::EmptyField {
            field: "embedder.id"
        })
    ));
}

#[test]
fn new_rejects_empty_name() {
    assert!(matches!(
        EmbedderProfile::new("id", "", EmbedderStrategy::None, None, None, None, 8),
        Err(DomainError::EmptyField {
            field: "embedder.name"
        })
    ));
}

#[test]
fn new_rejects_zero_dimension() {
    assert!(matches!(
        EmbedderProfile::new("id", "Name", EmbedderStrategy::None, None, None, None, 0),
        Err(DomainError::EmptyField {
            field: "embedder.dimension"
        })
    ));
}

#[test]
fn new_accepts_valid_minimal_profile() {
    let p =
        EmbedderProfile::new("id", "Name", EmbedderStrategy::None, None, None, None, 1).unwrap();
    assert_eq!(p.dimension, 1);
    assert_eq!(p.strategy, EmbedderStrategy::None);
}
