//! Integration tests for `transforms::recommendations` (PR-B5).
//!
//! These pin three guarantees the Rust proxy depends on at startup:
//!
//! 1. A well-formed `recommendations.toml` parses into a populated
//!    [`RecommendationStore`] with byte-for-byte the same fields the
//!    Python `headroom.cli.toin_publish` CLI emits.
//! 2. A missing file degrades to an empty store with no panic — the
//!    proxy must boot even if the publish pipeline is broken.
//! 3. A malformed file likewise degrades to an empty store, and the
//!    error is surfaced via `tracing::warn!` rather than swallowed.

use std::fs;
use std::path::{Path, PathBuf};

use headroom_core::transforms::recommendations::{
    AuthMode, RecommendationStore, RecommendationsError,
};

/// Minimal tempdir helper. Project convention is to avoid a
/// dev-dependency on `tempfile` (see `crates/headroom-parity` for the
/// matching helper). Cleanup happens on drop.
struct TempDir(PathBuf);

impl TempDir {
    fn path(&self) -> &Path {
        &self.0
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

fn tempdir() -> TempDir {
    use std::time::{SystemTime, UNIX_EPOCH};
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let p = std::env::temp_dir().join(format!(
        "headroom-recommendations-{nanos}-{:?}",
        std::thread::current().id()
    ));
    fs::create_dir_all(&p).unwrap();
    TempDir(p)
}

/// The exact schema emitted by the Python publish CLI. Keeping this
/// inline in tests prevents accidental schema drift on either side.
const VALID_TOML: &str = r#"
[[recommendation]]
auth_mode = "payg"
model_family = "claude-3-5"
structure_hash = "deadbeef00112233"
strategy_hint = "smart_crusher"
confidence = 0.87
observations = 142

[[recommendation]]
auth_mode = "oauth"
model_family = "gpt-4o"
structure_hash = "cafebabe44556677"
strategy_hint = "log_compressor"
confidence = 0.42
observations = 60
"#;

#[test]
fn loads_valid_toml() {
    let dir = tempdir();
    let path = dir.path().join("recommendations.toml");
    fs::write(&path, VALID_TOML).expect("write");

    let store = RecommendationStore::from_file(&path).expect("parses");
    assert_eq!(store.len(), 2);

    let payg = store
        .lookup(AuthMode::Payg, "claude-3-5", "deadbeef00112233")
        .expect("payg row");
    assert_eq!(payg.strategy_hint, "smart_crusher");
    assert!((payg.confidence - 0.87).abs() < 1e-9);
    assert_eq!(payg.observations, 142);

    let oauth = store
        .lookup(AuthMode::OAuth, "gpt-4o", "cafebabe44556677")
        .expect("oauth row");
    assert_eq!(oauth.strategy_hint, "log_compressor");
    assert_eq!(oauth.observations, 60);
}

#[test]
fn missing_file_yields_empty_recommendations() {
    let dir = tempdir();
    let path = dir.path().join("does_not_exist.toml");

    // `from_file` surfaces Missing as a typed error.
    let err = RecommendationStore::from_file(&path).unwrap_err();
    assert!(matches!(err, RecommendationsError::Missing(_)));

    // `load_or_empty` is the production entry point and degrades
    // gracefully — no panic, empty store.
    let store = RecommendationStore::load_or_empty(&path);
    assert!(store.is_empty());
    assert!(store.lookup(AuthMode::Payg, "claude-3-5", "any").is_none());
}

#[test]
fn malformed_toml_logs_and_yields_empty() {
    let dir = tempdir();
    let path = dir.path().join("recommendations.toml");
    // Real-world breakage: missing closing brace + unquoted value.
    fs::write(&path, "this is [[ definitely not valid\nrubbish = \n").expect("write");

    // The typed loader returns a Parse error.
    let err = RecommendationStore::from_file(&path).unwrap_err();
    assert!(
        matches!(err, RecommendationsError::Parse(_)),
        "expected Parse, got {err:?}"
    );

    // The production loader returns an empty store — proxy still
    // boots; ops alert on the structured `tracing::warn!` event.
    let store = RecommendationStore::load_or_empty(&path);
    assert!(store.is_empty());
}

#[test]
fn empty_recommendation_array_is_valid() {
    // A publish run with no eligible slices writes a header-only file.
    // It must parse cleanly to an empty store, not surface as an error.
    let dir = tempdir();
    let path = dir.path().join("recommendations.toml");
    fs::write(&path, "# Auto-generated by toin_publish\n").expect("write");

    let store = RecommendationStore::from_file(&path).expect("parses");
    assert!(store.is_empty());
}
