//! `Config` construction for tests.
//!
//! Collapses the ~13 lookalike `test_config_*` / `build_config` helpers
//! scattered across the integration tests into one fluent builder.
//! Every builder call is a minimal override on top of
//! `Config::test_default()`; the builder automatically derives the
//! `api_key → slug` index map from the collected users.

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};

use doctate_server::config::{Config, User};
use doctate_server::settings::Settings;

/// Make a (test) `IONOS_API_KEY` visible before the static backend catalog
/// in `analyze::backend` reads it. Idempotent — only the first call writes
/// the env var; later calls are a no-op. Tests that exercise paths gated
/// by `analyze::backend::any_backend_available()` should call this (the
/// builder does it automatically when `with_llm` is invoked).
///
/// SAFETY: in Edition 2024, `std::env::set_var` is `unsafe` because it
/// races with concurrent `getenv` on POSIX. The `OnceLock` guarantees
/// that the write happens at most once, before any test thread has had
/// a chance to read the env via `analyze::backend::backends()`. As long
/// as `ensure_test_llm_env()` is called in `TestConfig::new()` (i.e. at
/// the top of every test setup), no other test thread observes the
/// env-var being mutated mid-flight.
pub fn ensure_test_llm_env() {
    static ONCE: OnceLock<()> = OnceLock::new();
    ONCE.get_or_init(|| {
        // SAFETY: see function docstring — single-threaded init under OnceLock.
        unsafe {
            std::env::set_var("IONOS_API_KEY", "test-key");
        }
    });
}

/// Unique tempdir path for a given label. `process::id()` + UUID v4
/// keeps parallel `cargo test` runs from colliding; the label lets
/// humans identify which test owns which leftover directory when
/// cleanup fails.
pub fn unique_tmpdir(label: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "doctate-test-{label}-{}-{}",
        std::process::id(),
        uuid::Uuid::new_v4()
    ))
}

/// Fluent builder for `Arc<Config>`. All methods take `self` by value
/// and return `Self` so calls chain naturally:
///
/// ```ignore
/// let cfg = TestConfig::new()
///     .with_user(test_user("dr_a"))
///     .with_llm(mock.uri())
///     .build();
/// ```
pub struct TestConfig {
    data_path: Option<PathBuf>,
    users: Vec<User>,
    llm_url: Option<String>,
    llm_api_key: Option<String>,
    llm_model: Option<String>,
    whisper_url: Option<String>,
    whisper_timeout_seconds: Option<u64>,
    ollama_url: Option<String>,
    label: &'static str,
}

impl TestConfig {
    /// Start a fresh builder. `label` is used only to name the tempdir
    /// when no explicit `data_path` is set via `with_data_path`.
    pub fn new() -> Self {
        // Pre-seed the test API key so any code path that touches the
        // backend catalog observes a non-empty `IONOS_API_KEY`.
        ensure_test_llm_env();
        Self {
            data_path: None,
            users: Vec::new(),
            llm_url: None,
            llm_api_key: None,
            llm_model: None,
            whisper_url: None,
            whisper_timeout_seconds: None,
            ollama_url: None,
            label: "common",
        }
    }

    /// Label used when auto-generating the tempdir name.
    pub fn with_label(mut self, label: &'static str) -> Self {
        self.label = label;
        self
    }

    /// Override the data path. By default `build()` generates a unique
    /// tempdir under `std::env::temp_dir()`.
    pub fn with_data_path(mut self, path: PathBuf) -> Self {
        self.data_path = Some(path);
        self
    }

    /// Add a single user. Can be called repeatedly.
    pub fn with_user(mut self, user: User) -> Self {
        self.users.push(user);
        self
    }

    /// Replace the user list outright.
    pub fn with_users(mut self, users: Vec<User>) -> Self {
        self.users = users;
        self
    }

    /// Configure a hosted-provider LLM with the common test defaults
    /// (`api_key = "test-key"`, `model = "test-model"`). For Ollama-style
    /// endpoints that expect no `Authorization` header, use
    /// [`with_llm_explicit`] and pass an empty api_key.
    pub fn with_llm(mut self, url: impl Into<String>) -> Self {
        self.llm_url = Some(url.into());
        self.llm_api_key.get_or_insert_with(|| "test-key".into());
        self.llm_model.get_or_insert_with(|| "test-model".into());
        self
    }

    /// Configure the LLM with explicit values. `api_key` may be empty
    /// to exercise the Ollama-style no-auth path.
    pub fn with_llm_explicit(
        mut self,
        url: impl Into<String>,
        api_key: impl Into<String>,
        model: impl Into<String>,
    ) -> Self {
        self.llm_url = Some(url.into());
        self.llm_api_key = Some(api_key.into());
        self.llm_model = Some(model.into());
        self
    }

    /// Whisper ASR mock URL + timeout.
    pub fn with_whisper(mut self, url: impl Into<String>, timeout_seconds: u64) -> Self {
        self.whisper_url = Some(url.into());
        self.whisper_timeout_seconds = Some(timeout_seconds);
        self
    }

    /// Ollama (oneliner generator) mock URL.
    pub fn with_ollama(mut self, url: impl Into<String>) -> Self {
        self.ollama_url = Some(url.into());
        self
    }

    /// Materialize the config alone. Bucket-B overrides (`with_llm`,
    /// `with_whisper`, `with_ollama`) are silently dropped — call
    /// [`build_pair`] when those need to take effect.
    pub fn build(self) -> Arc<Config> {
        self.build_pair().0
    }

    /// Materialize both `Config` and `Settings`. Use when a test exercises
    /// LLM, Whisper or Ollama paths and needs the builder-set values to
    /// reach the worker.
    pub fn build_pair(self) -> (Arc<Config>, Arc<Settings>) {
        let data_path = self.data_path.unwrap_or_else(|| unique_tmpdir(self.label));
        let api_keys: HashMap<String, String> = self
            .users
            .iter()
            .map(|u| (u.api_key.clone(), u.slug.clone()))
            .collect();

        let config = Arc::new(Config {
            data_path,
            users: self.users,
            api_keys,
            ..Config::test_default()
        });

        let mut settings = Settings::default();
        // LLM-related fields used to live on `Settings`; with the multi-backend
        // refactor they are now owned by `analyze::backend`. The `with_llm*`
        // builder methods are kept for source-compat but their stored URL/
        // api_key/model values no longer flow into `settings`. Tests that
        // rely on a working LLM mock must inject backends through other
        // means (currently: those tests are #[ignore]'d, see the test
        // attribute comment).
        let _ = (self.llm_url, self.llm_api_key, self.llm_model);
        if let Some(v) = self.whisper_url {
            settings.whisper.url = v;
        }
        if let Some(v) = self.whisper_timeout_seconds {
            settings.whisper.timeout_seconds = v;
        }
        if let Some(v) = self.ollama_url {
            settings.ollama.url = v;
        }
        (config, Arc::new(settings))
    }
}

impl Default for TestConfig {
    fn default() -> Self {
        Self::new()
    }
}
