//! ModelOverridesPolicy enforcement: voice_set / voice_unset from
//! LLM-sourced calls must be rejected when allow_toggle = false.

use std::sync::Arc;

use nexo_plugin_voice::breaker::{BreakerRegistry, BreakerSettings};
use nexo_plugin_voice::cache::AudioCache;
use nexo_plugin_voice::fallback::FallbackConfig;
use nexo_plugin_voice::policy::{ModelOverridesPolicy, PolicyDimension};
use nexo_plugin_voice::providers::TtsProvider;
use nexo_plugin_voice::store::VoiceModeStore;
use nexo_plugin_voice::tools::{dispatch, VoiceToolContext};

async fn fresh_ctx(policy: ModelOverridesPolicy) -> VoiceToolContext {
    use sqlx::sqlite::SqliteConnectOptions;
    use std::str::FromStr;
    let opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
    let pool = sqlx::sqlite::SqlitePoolOptions::new()
        .max_connections(1)
        .connect_with(opts)
        .await
        .unwrap();
    let store = VoiceModeStore::open_with_pool(pool).await.unwrap();
    let providers: Vec<Arc<dyn TtsProvider>> = vec![];
    VoiceToolContext {
        store,
        providers,
        fallback: FallbackConfig::default(),
        breakers: BreakerRegistry::new(BreakerSettings::default()),
        policy,
        cache: AudioCache::new(0, std::time::Duration::from_secs(60)),
        max_text_length: 1500,
    }
}

#[tokio::test]
async fn voice_set_from_llm_rejected_when_policy_denies_toggle() {
    let mut p = ModelOverridesPolicy::default();
    p.allow_toggle = false;
    let ctx = fresh_ctx(p).await;
    let err = dispatch(
        &ctx,
        "voice_set",
        serde_json::json!({
            "context": { "conversation_key": "ana:s:1", "source": "llm" },
            "voice_id": "es-MX-DaliaNeural"
        }),
    )
    .await
    .unwrap_err();
    let msg = format!("{err}");
    assert!(msg.contains("policy.allow_toggle=false"));
}

#[tokio::test]
async fn voice_set_from_ui_bypasses_policy_toggle_gate() {
    let mut p = ModelOverridesPolicy::default();
    p.allow_toggle = false;
    let ctx = fresh_ctx(p).await;
    let out = dispatch(
        &ctx,
        "voice_set",
        serde_json::json!({
            "context": { "conversation_key": "ana:s:1", "source": "ui" },
            "voice_id": "es-MX-DaliaNeural"
        }),
    )
    .await
    .unwrap();
    assert_eq!(out["ok"], true);
}

#[tokio::test]
async fn voice_unset_from_llm_rejected_when_policy_denies_toggle() {
    let mut p = ModelOverridesPolicy::default();
    p.allow_toggle = false;
    let ctx = fresh_ctx(p).await;
    let err = dispatch(
        &ctx,
        "voice_unset",
        serde_json::json!({
            "context": { "conversation_key": "ana:s:1", "source": "llm" }
        }),
    )
    .await
    .unwrap_err();
    assert!(format!("{err}").contains("policy.allow_toggle=false"));
}

#[tokio::test]
async fn voice_set_rejects_unknown_voice() {
    let ctx = fresh_ctx(ModelOverridesPolicy::default()).await;
    let err = dispatch(
        &ctx,
        "voice_set",
        serde_json::json!({
            "context": { "conversation_key": "ana:s:1", "source": "ui" },
            "voice_id": "xx-YY-Bogus"
        }),
    )
    .await
    .unwrap_err();
    assert!(format!("{err}").contains("xx-YY-Bogus"));
}

#[tokio::test]
async fn voice_set_rejects_speed_out_of_range() {
    let ctx = fresh_ctx(ModelOverridesPolicy::default()).await;
    let err = dispatch(
        &ctx,
        "voice_set",
        serde_json::json!({
            "context": { "conversation_key": "ana:s:1", "source": "ui" },
            "voice_id": "es-MX-DaliaNeural",
            "speed": 2.5
        }),
    )
    .await
    .unwrap_err();
    assert!(format!("{err}").contains("speed"));
}

#[test]
fn policy_master_switch_denies_everything() {
    let mut p = ModelOverridesPolicy::default();
    p.enabled = false;
    assert!(!p.dimension_allowed(PolicyDimension::Voice));
    assert!(!p.dimension_allowed(PolicyDimension::Toggle));
}
