//! Fallback chain: primary fails (retryable) → secondary fails →
//! tertiary succeeds. Asserts winner + chain_tried fields populate
//! correctly + the executor stops trying after first success.

use std::sync::Arc;

use async_trait::async_trait;
use nexo_plugin_voice::breaker::{BreakerRegistry, BreakerSettings};
use nexo_plugin_voice::error::{ReasonCode, Result, VoiceError};
use nexo_plugin_voice::fallback::{execute, FallbackConfig, FallbackStrategyKind, RetryPolicy};
use nexo_plugin_voice::providers::{
    AudioFormat, CostClass, SynthesizeOutput, SynthesizeRequest, TtsProvider,
};
use std::sync::atomic::{AtomicU32, Ordering};

struct CountingProvider {
    id_str: &'static str,
    response: Result<&'static [u8]>,
    calls: Arc<AtomicU32>,
}

impl CountingProvider {
    fn ok(id: &'static str, bytes: &'static [u8]) -> Self {
        Self {
            id_str: id,
            response: Ok(bytes),
            calls: Arc::new(AtomicU32::new(0)),
        }
    }
    fn fail(id: &'static str, code: ReasonCode) -> Self {
        Self {
            id_str: id,
            response: Err(VoiceError::Provider {
                provider: id,
                reason: "test fail".into(),
                code,
            }),
            calls: Arc::new(AtomicU32::new(0)),
        }
    }
    fn call_count(&self) -> u32 {
        self.calls.load(Ordering::SeqCst)
    }
}

#[async_trait]
impl TtsProvider for CountingProvider {
    fn id(&self) -> &'static str {
        self.id_str
    }
    fn label(&self) -> &str {
        self.id_str
    }
    fn cost_class(&self) -> CostClass {
        CostClass::Free
    }
    fn is_configured(&self) -> bool {
        true
    }
    async fn synthesize(&self, _req: SynthesizeRequest<'_>) -> Result<SynthesizeOutput> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        match &self.response {
            Ok(b) => Ok(SynthesizeOutput {
                audio_bytes: b.to_vec(),
                raw_mime: "audio/mpeg",
                latency_ms: 50,
                reason_code: ReasonCode::Success,
            }),
            Err(VoiceError::Provider {
                provider,
                reason,
                code,
            }) => Err(VoiceError::Provider {
                provider,
                reason: reason.clone(),
                code: *code,
            }),
            Err(_) => Err(VoiceError::NotSupported("counting")),
        }
    }
}

#[tokio::test]
async fn chain_stops_after_first_success() {
    let primary = Arc::new(CountingProvider::ok("edge", b"primary_audio"));
    let secondary = Arc::new(CountingProvider::ok("elevenlabs", b"secondary_audio"));
    let p1 = primary.clone();
    let p2 = secondary.clone();
    let providers: Vec<Arc<dyn TtsProvider>> = vec![primary, secondary];

    // max_attempts = 1 so tests assert single-attempt-per-provider
    // call counts. SDK-parity retry-with-backoff is exercised in
    // its own dedicated test file.
    let mut cfg = FallbackConfig::default();
    cfg.retry = RetryPolicy {
        max_attempts: 1,
        ..RetryPolicy::default()
    };
    cfg.strategy = FallbackStrategyKind::Chain;
    cfg.chain = vec!["edge".into(), "elevenlabs".into()];

    let req = SynthesizeRequest {
        body_ssml: "hola",
        voice_id: "es-MX-DaliaNeural",
        speed: None,
        pitch_pct: None,
        style: None,
        model_id: None,
        format: AudioFormat::Mp3_24k,
    };
    let breakers = BreakerRegistry::new(BreakerSettings::default());
    let outcome = execute(&providers, &cfg, &breakers, &req).await.unwrap();

    assert_eq!(outcome.winner, "edge");
    assert_eq!(outcome.chain_tried, vec!["edge"]);
    assert_eq!(p1.call_count(), 1);
    assert_eq!(p2.call_count(), 0, "secondary must NOT be tried");
}

#[tokio::test]
async fn chain_falls_through_to_winner() {
    let primary = Arc::new(CountingProvider::fail("edge", ReasonCode::EmptyAudio));
    let secondary = Arc::new(CountingProvider::fail("elevenlabs", ReasonCode::NetworkTimeout));
    let tertiary = Arc::new(CountingProvider::ok("openai_tts", b"tertiary_audio"));
    let p1 = primary.clone();
    let p2 = secondary.clone();
    let p3 = tertiary.clone();
    let providers: Vec<Arc<dyn TtsProvider>> = vec![primary, secondary, tertiary];

    // max_attempts = 1 so tests assert single-attempt-per-provider
    // call counts. SDK-parity retry-with-backoff is exercised in
    // its own dedicated test file.
    let mut cfg = FallbackConfig::default();
    cfg.retry = RetryPolicy {
        max_attempts: 1,
        ..RetryPolicy::default()
    };
    cfg.strategy = FallbackStrategyKind::Chain;
    cfg.chain = vec!["edge".into(), "elevenlabs".into(), "openai_tts".into()];

    let req = SynthesizeRequest {
        body_ssml: "hola",
        voice_id: "es-MX-DaliaNeural",
        speed: None,
        pitch_pct: None,
        style: None,
        model_id: None,
        format: AudioFormat::Mp3_24k,
    };
    let breakers = BreakerRegistry::new(BreakerSettings::default());
    let outcome = execute(&providers, &cfg, &breakers, &req).await.unwrap();

    assert_eq!(outcome.winner, "openai_tts");
    assert_eq!(outcome.chain_tried, vec!["edge", "elevenlabs", "openai_tts"]);
    assert_eq!(p1.call_count(), 1);
    assert_eq!(p2.call_count(), 1);
    assert_eq!(p3.call_count(), 1);
    assert_eq!(outcome.output.audio_bytes, b"tertiary_audio".to_vec());
}

#[tokio::test]
async fn chain_returns_last_error_when_every_provider_fails() {
    let providers: Vec<Arc<dyn TtsProvider>> = vec![
        Arc::new(CountingProvider::fail("edge", ReasonCode::EmptyAudio)),
        Arc::new(CountingProvider::fail(
            "elevenlabs",
            ReasonCode::NetworkTimeout,
        )),
    ];
    // max_attempts = 1 so tests assert single-attempt-per-provider
    // call counts. SDK-parity retry-with-backoff is exercised in
    // its own dedicated test file.
    let mut cfg = FallbackConfig::default();
    cfg.retry = RetryPolicy {
        max_attempts: 1,
        ..RetryPolicy::default()
    };
    cfg.strategy = FallbackStrategyKind::Chain;
    cfg.chain = vec!["edge".into(), "elevenlabs".into()];

    let req = SynthesizeRequest {
        body_ssml: "hola",
        voice_id: "es-MX-DaliaNeural",
        speed: None,
        pitch_pct: None,
        style: None,
        model_id: None,
        format: AudioFormat::Mp3_24k,
    };
    let breakers = BreakerRegistry::new(BreakerSettings::default());
    let err = execute(&providers, &cfg, &breakers, &req).await.unwrap_err();
    assert_eq!(err.reason_code(), ReasonCode::NetworkTimeout);
}

#[tokio::test]
async fn chain_short_circuits_on_non_retryable_error() {
    let primary = Arc::new(CountingProvider::fail("edge", ReasonCode::Unauthorized));
    let secondary = Arc::new(CountingProvider::ok("elevenlabs", b"never_reached"));
    let p2 = secondary.clone();
    let providers: Vec<Arc<dyn TtsProvider>> = vec![primary, secondary];

    // max_attempts = 1 so tests assert single-attempt-per-provider
    // call counts. SDK-parity retry-with-backoff is exercised in
    // its own dedicated test file.
    let mut cfg = FallbackConfig::default();
    cfg.retry = RetryPolicy {
        max_attempts: 1,
        ..RetryPolicy::default()
    };
    cfg.strategy = FallbackStrategyKind::Chain;
    cfg.chain = vec!["edge".into(), "elevenlabs".into()];

    let req = SynthesizeRequest {
        body_ssml: "hola",
        voice_id: "es-MX-DaliaNeural",
        speed: None,
        pitch_pct: None,
        style: None,
        model_id: None,
        format: AudioFormat::Mp3_24k,
    };
    let breakers = BreakerRegistry::new(BreakerSettings::default());
    let err = execute(&providers, &cfg, &breakers, &req).await.unwrap_err();
    assert_eq!(err.reason_code(), ReasonCode::Unauthorized);
    assert_eq!(
        p2.call_count(),
        0,
        "non-retryable error must short-circuit chain"
    );
}
