use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[cfg(test)]
use std::sync::Mutex;
use std::sync::{Arc, OnceLock};
use std::time::Duration;

use anyhow::Context as _;
use axum::extract::Request;
#[cfg(test)]
use axum::extract::State as AxumState;
use axum::http::{HeaderValue, header};
use axum::middleware::Next;
use axum::response::Response;
use axum::{Router, middleware};
use chrono::Duration as ChronoDuration;
use fabro_config::user::default_storage_dir;
use fabro_config::{RunLayer, ServerSettingsBuilder, Storage, envfile};
use fabro_db::DbPool;
use fabro_interview::Interviewer;
use fabro_model::catalog::{LlmCatalogSettings, ProviderCatalogSettings};
use fabro_model::{Catalog, ProviderId};
use fabro_sandbox::SandboxProviderRegistry;
use fabro_static::EnvVars;
use fabro_store::{ArtifactStore, Database, test_support as store_test_support};
use fabro_types::settings::ServerAuthMethod;
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::{AuthMethod, IdpIdentity, ServerSettings};
use fabro_vault::{SecretType, Vault};
use fabro_workflow::handler::HandlerRegistry;
use object_store::memory::InMemory as MemoryObjectStore;
use tokio::runtime::Builder as TokioRuntimeBuilder;
use tokio_util::sync::CancellationToken;
use ulid::Ulid;

use crate::automation_materializer::AutomationRunMaterializer;
pub use crate::automation_materializer::TestAutomationRunMaterializer;
use crate::interp::process_env_var;
use crate::jwt_auth::{AuthMode, ConfiguredAuth};
#[cfg(test)]
use crate::principal_middleware::{AuthContextSlot, RequestAuthContext};
use crate::server::{
    self, AppState, AppStateConfig, EnvLookup, RegistryFactoryOverride, ResolvedAppStateSettings,
    RouterOptions, build_app_state,
};
use crate::server_secrets::ServerSecrets;
#[cfg(test)]
use crate::worker_runtime::WorkerRuntime;
use crate::{auth, migrations};

pub const TEST_DEV_TOKEN: &str =
    "fabro_dev_abababababababababababababababababababababababababababababababab";
pub const TEST_SESSION_SECRET: &str =
    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const TEST_OPENAI_API_KEY: &str = "test-openai-api-key";
const FABRO_TEST_ASSUME_LLM_READY: &str = "FABRO_TEST_ASSUME_LLM_READY";

/// Supply enabled catalog providers to CLI fixture run materialization.
///
/// This is scoped to the `test-support` feature and an explicit child-process
/// flag. The CLI suite shares a server whose credential state can change
/// between tests, so the flag deliberately ignores that mutable state. It does
/// not register adapters or make model execution available.
pub(crate) fn test_run_materialization_provider_ids(
    catalog: &Catalog,
    ready_provider_ids: &[ProviderId],
) -> Vec<ProviderId> {
    let assume_ready = process_env_var(FABRO_TEST_ASSUME_LLM_READY)
        .is_some_and(|value| !matches!(value.as_str(), "" | "0" | "false" | "no"));
    if assume_ready {
        catalog.all_provider_ids().into_iter().collect()
    } else {
        ready_provider_ids.to_vec()
    }
}

pub fn default_test_server_settings() -> ServerSettings {
    ServerSettingsBuilder::from_toml(
        r#"
_version = 1

[server.auth]
methods = ["dev-token"]
"#,
    )
    .expect("default test server settings should resolve")
}

#[must_use]
pub struct TestAppStateBuilder {
    server_settings:              ServerSettings,
    manifest_run_defaults:        RunLayer,
    max_concurrent_runs:          usize,
    registry_factory_override:    Option<Box<RegistryFactoryOverride>>,
    sandbox_provider_registry:    Option<SandboxProviderRegistry>,
    store_bundle:                 Option<(Arc<Database>, ArtifactStore)>,
    vault_path:                   Option<PathBuf>,
    vault_entries:                Vec<(String, String)>,
    server_env_path:              Option<PathBuf>,
    active_config_path:           Option<PathBuf>,
    server_secret_env:            HashMap<String, String>,
    default_environment_provider: Option<EnvironmentProvider>,
    env_lookup:                   EnvLookup,
    llm_catalog_settings:         LlmCatalogSettings,
    automation_materializer:      Option<Arc<dyn AutomationRunMaterializer>>,
    #[cfg(test)]
    worker_runtime:               Option<Arc<dyn WorkerRuntime>>,
}

impl Default for TestAppStateBuilder {
    fn default() -> Self {
        Self {
            server_settings:              default_test_server_settings(),
            manifest_run_defaults:        RunLayer::default(),
            max_concurrent_runs:          5,
            registry_factory_override:    None,
            sandbox_provider_registry:    None,
            store_bundle:                 None,
            vault_path:                   None,
            vault_entries:                Vec::new(),
            server_env_path:              None,
            active_config_path:           None,
            server_secret_env:            HashMap::new(),
            default_environment_provider: Some(EnvironmentProvider::Docker),
            env_lookup:                   default_env_lookup(),
            llm_catalog_settings:         LlmCatalogSettings::default(),
            automation_materializer:      None,
            #[cfg(test)]
            worker_runtime:               None,
        }
    }
}

impl TestAppStateBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn runtime_settings(
        mut self,
        server_settings: ServerSettings,
        manifest_run_defaults: RunLayer,
    ) -> Self {
        self.server_settings = server_settings;
        self.manifest_run_defaults = manifest_run_defaults;
        self
    }

    pub fn max_concurrent_runs(mut self, max_concurrent_runs: usize) -> Self {
        self.max_concurrent_runs = max_concurrent_runs;
        self
    }

    pub fn registry_factory(
        mut self,
        registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry
        + Send
        + Sync
        + 'static,
    ) -> Self {
        self.registry_factory_override = Some(Box::new(registry_factory_override));
        self
    }

    pub fn sandbox_provider_registry(
        mut self,
        sandbox_provider_registry: SandboxProviderRegistry,
    ) -> Self {
        self.sandbox_provider_registry = Some(sandbox_provider_registry);
        self
    }

    pub fn env_lookup(
        mut self,
        env_lookup: impl Fn(&str) -> Option<String> + Send + Sync + 'static,
    ) -> Self {
        self.env_lookup = Arc::new(env_lookup);
        self
    }

    pub fn llm_catalog_settings(mut self, settings: LlmCatalogSettings) -> Self {
        self.llm_catalog_settings = settings;
        self
    }

    pub fn automation_materializer(mut self, materializer: TestAutomationRunMaterializer) -> Self {
        self.automation_materializer = Some(materializer.into_materializer());
        self
    }

    #[cfg(test)]
    pub(crate) fn worker_runtime(mut self, worker_runtime: Arc<dyn WorkerRuntime>) -> Self {
        self.worker_runtime = Some(worker_runtime);
        self
    }

    pub fn provider_base_url(
        mut self,
        provider: impl Into<String>,
        base_url: impl Into<String>,
    ) -> Self {
        self.llm_catalog_settings
            .providers
            .insert(provider.into(), ProviderCatalogSettings {
                base_url: Some(base_url.into()),
                ..ProviderCatalogSettings::default()
            });
        self
    }

    pub fn server_secret_env(mut self, server_secret_env: HashMap<String, String>) -> Self {
        self.server_secret_env = server_secret_env;
        self
    }

    pub fn default_environment_provider(mut self, provider: Option<EnvironmentProvider>) -> Self {
        self.default_environment_provider = provider;
        self
    }

    pub fn store_bundle(mut self, store: Arc<Database>, artifact_store: ArtifactStore) -> Self {
        self.store_bundle = Some((store, artifact_store));
        self
    }

    fn server_env_path(mut self, server_env_path: PathBuf) -> Self {
        self.server_env_path = Some(server_env_path);
        self
    }

    pub fn vault_path(mut self, vault_path: PathBuf) -> Self {
        self.vault_path = Some(vault_path);
        self
    }

    pub fn active_config_path(mut self, active_config_path: PathBuf) -> Self {
        self.active_config_path = Some(active_config_path);
        self
    }

    /// Pre-populate the vault file with optional integration secrets (token
    /// type) before [`build_app_state`] opens it.
    pub fn vault_entries<I, K, V>(mut self, entries: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<String>,
        V: Into<String>,
    {
        self.vault_entries
            .extend(entries.into_iter().map(|(k, v)| (k.into(), v.into())));
        self
    }

    pub fn build(self) -> Arc<AppState> {
        self.try_build().expect("test app state should build")
    }

    pub fn try_build(mut self) -> anyhow::Result<Arc<AppState>> {
        let (store, artifact_store) = self.store_bundle.unwrap_or_else(test_store_bundle);
        let vault_path = self.vault_path.unwrap_or_else(test_secret_store_path);
        self.server_settings = redirect_default_storage_root(self.server_settings, &vault_path)?;
        if !self.vault_entries.is_empty() {
            let mut vault = Vault::load(vault_path.clone()).expect("test vault should load");
            for (name, value) in &self.vault_entries {
                vault
                    .set(name, value, SecretType::Token, None)
                    .expect("test vault entry should persist");
            }
        }
        let server_env_path = self
            .server_env_path
            .unwrap_or_else(|| vault_path.with_file_name("server.env"));
        let active_config_path = self
            .active_config_path
            .unwrap_or_else(|| vault_path.with_file_name("settings.toml"));
        let db_pool = test_db_pool_for_vault_path_with_default_environment(
            &vault_path,
            self.default_environment_provider,
        )?;
        import_test_legacy_automations(
            db_pool.clone(),
            server::automation_dir_for_active_config(&active_config_path),
        )?;
        let preloaded_vault = test_secret_snapshot(db_pool.clone())?;
        build_app_state(AppStateConfig {
            resolved_settings: resolved_runtime_settings_for_tests(
                self.server_settings,
                self.manifest_run_defaults,
                self.llm_catalog_settings,
            ),
            registry_factory_override: self.registry_factory_override,
            max_concurrent_runs: self.max_concurrent_runs,
            store,
            artifact_store,
            db_pool,
            preloaded_vault,
            server_secrets: load_test_server_secrets(server_env_path, self.server_secret_env),
            env_lookup: self.env_lookup,
            github_api_base_url: None,
            active_config_path,
            http_client: Some(
                fabro_http::test_http_client().expect("test HTTP client should build"),
            ),
            sandbox_provider_registry: self.sandbox_provider_registry,
            shutdown: CancellationToken::new(),
            #[cfg(test)]
            worker_control_bus: None,
            #[cfg(test)]
            worker_runtime: self.worker_runtime,
            automation_materializer_override: self.automation_materializer,
        })
    }
}

#[expect(
    clippy::disallowed_methods,
    reason = "sync test builders may run inside Tokio; a dedicated thread avoids a nested runtime"
)]
pub(crate) fn test_secret_snapshot(pool: DbPool) -> anyhow::Result<Vault> {
    std::thread::spawn(move || {
        let runtime = TokioRuntimeBuilder::new_current_thread()
            .enable_all()
            .build()?;
        runtime
            .block_on(fabro_vault::SecretStore::new(pool).snapshot())
            .map(fabro_vault::SecretSnapshot::into_vault)
            .map_err(anyhow::Error::new)
    })
    .join()
    .expect("test secret snapshot thread should not panic")
}

pub fn llm_catalog_settings_with_provider_base_url(
    provider: impl Into<String>,
    base_url: impl Into<String>,
) -> LlmCatalogSettings {
    let mut settings = LlmCatalogSettings::default();
    settings
        .providers
        .insert(provider.into(), ProviderCatalogSettings {
            base_url: Some(base_url.into()),
            ..ProviderCatalogSettings::default()
        });
    settings
}

pub fn test_app_state() -> Arc<AppState> {
    ready_test_app_state_builder().build()
}

pub fn test_app_state_with_registry_factory(
    registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .registry_factory(registry_factory_override)
        .build()
}

pub fn test_app_state_with_settings_and_registry_factory(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .registry_factory(registry_factory_override)
        .build()
}

pub fn test_app_state_with_options_and_registry_factory(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
    registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .registry_factory(registry_factory_override)
        .build()
}

pub fn test_app_state_with_options(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .build()
}

fn ready_test_app_state_builder() -> TestAppStateBuilder {
    TestAppStateBuilder::new().vault_entries([(EnvVars::OPENAI_API_KEY, TEST_OPENAI_API_KEY)])
}

pub(crate) fn resolved_runtime_settings_for_tests(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    llm_catalog_settings: LlmCatalogSettings,
) -> ResolvedAppStateSettings {
    ResolvedAppStateSettings {
        server_settings,
        manifest_run_defaults,
        llm_catalog_settings,
    }
}

pub fn test_app_state_with_runtime_settings_and_registry_factory(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .registry_factory(registry_factory_override)
        .build()
}

pub fn test_app_state_with_runtime_settings_and_options_and_registry_factory(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
    registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .registry_factory(registry_factory_override)
        .build()
}

pub fn test_app_state_with_runtime_settings_and_options(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .build()
}

pub fn test_app_state_with_runtime_settings_and_env_lookup(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
    env_lookup: impl Fn(&str) -> Option<String> + Send + Sync + 'static,
) -> Arc<AppState> {
    TestAppStateBuilder::new()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .env_lookup(env_lookup)
        .build()
}

pub fn test_app_state_with_runtime_settings_and_env_lookup_and_server_secret_env(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
    env_lookup: impl Fn(&str) -> Option<String> + Send + Sync + 'static,
    server_secret_env: &HashMap<String, String>,
) -> Arc<AppState> {
    TestAppStateBuilder::new()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .env_lookup(env_lookup)
        .server_secret_env(server_secret_env.clone())
        .build()
}

pub fn test_app_state_with_env_lookup(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
    env_lookup: impl Fn(&str) -> Option<String> + Send + Sync + 'static,
) -> Arc<AppState> {
    TestAppStateBuilder::new()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .env_lookup(env_lookup)
        .build()
}

#[expect(
    clippy::disallowed_methods,
    reason = "test helper writes a fixture server.env with sync std::fs::write"
)]
pub fn test_app_state_with_runtime_settings_and_session_key(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    session_secret: Option<&str>,
) -> Arc<AppState> {
    let vault_path = test_secret_store_path();
    let server_env_path = vault_path
        .parent()
        .expect("test secrets path should have parent")
        .join("server.env");
    if let Some(session_secret) = session_secret {
        std::fs::write(
            &server_env_path,
            format!("SESSION_SECRET={session_secret}\n"),
        )
        .expect("test server env should be writable");
    }
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .vault_path(vault_path)
        .server_env_path(server_env_path)
        .build()
}

pub fn test_app_state_with_session_key(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    session_secret: Option<&str>,
) -> Arc<AppState> {
    test_app_state_with_runtime_settings_and_session_key(
        server_settings,
        manifest_run_defaults,
        session_secret,
    )
}

pub fn test_app_state_with_store(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
    store: Arc<Database>,
    artifact_store: ArtifactStore,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .store_bundle(store, artifact_store)
        .build()
}

pub fn test_store_bundle() -> (Arc<Database>, ArtifactStore) {
    let object_store: Arc<dyn object_store::ObjectStore> = Arc::new(MemoryObjectStore::new());
    let store = Arc::new(store_test_support::test_database(
        Arc::clone(&object_store),
        "",
        Duration::from_millis(1),
        None,
    ));
    let artifact_store = ArtifactStore::new(object_store, "artifacts");
    (store, artifact_store)
}

#[cfg(test)]
pub(crate) fn test_db_pool_for_vault_path(vault_path: &Path) -> anyhow::Result<DbPool> {
    test_db_pool_for_vault_path_with_default_environment(
        vault_path,
        Some(EnvironmentProvider::Docker),
    )
}

pub(crate) fn test_db_pool_for_vault_path_with_default_environment(
    vault_path: &Path,
    default_environment_provider: Option<EnvironmentProvider>,
) -> anyhow::Result<DbPool> {
    test_db_pool(
        sqlite_path_for_vault_path(vault_path),
        vault_path.to_path_buf(),
        default_environment_provider,
    )
}

fn sqlite_path_for_vault_path(vault_path: &Path) -> PathBuf {
    vault_path
        .parent()
        .unwrap_or_else(|| Path::new("."))
        .join("db")
        .join("fabro.sqlite3")
}

pub async fn test_environment_from_storage_dir(
    storage_dir: &Path,
    id: &str,
) -> anyhow::Result<Option<fabro_environment::Environment>> {
    let database = fabro_db::Database::connect(Storage::new(storage_dir).sqlite_path()).await?;
    let store = fabro_environment::EnvironmentStore::load(database.clone_pool(), false).await?;
    let id = fabro_environment::EnvironmentId::new(id)?;
    Ok(store.get(&id))
}

#[expect(
    clippy::disallowed_methods,
    reason = "sync test builders may be called inside async tests; a short-lived OS thread avoids nested Tokio runtimes"
)]
fn test_db_pool(
    path: PathBuf,
    vault_path: PathBuf,
    default_environment_provider: Option<EnvironmentProvider>,
) -> anyhow::Result<DbPool> {
    std::thread::spawn(move || {
        migrations::migrate_legacy_vault_file(&vault_path)?;
        let runtime = TokioRuntimeBuilder::new_current_thread()
            .enable_all()
            .build()?;
        runtime.block_on(async move {
            let database = fabro_db::Database::connect(&path).await?;
            database.migrate().await?;
            fabro_vault::import_legacy_json_once(database.pool(), vault_path).await?;
            if let Some(provider) = default_environment_provider {
                fabro_environment::seed_default_environment(database.pool(), provider).await?;
            }
            Ok(database.clone_pool())
        })
    })
    .join()
    .expect("test database setup thread should not panic")
}

#[expect(
    clippy::disallowed_methods,
    reason = "sync test builders may run inside async tests; a short-lived OS thread avoids nested Tokio runtimes"
)]
fn import_test_legacy_automations(pool: DbPool, source_dir: PathBuf) -> anyhow::Result<()> {
    std::thread::spawn(move || {
        let runtime = TokioRuntimeBuilder::new_current_thread()
            .enable_all()
            .build()?;
        runtime
            .block_on(fabro_automation::import_legacy_directory_once(
                &pool, source_dir,
            ))
            .map(|_| ())
            .map_err(anyhow::Error::new)
    })
    .join()
    .expect("test automation import thread should not panic")
}

pub fn test_app_state_with_store_and_runtime_settings(
    server_settings: ServerSettings,
    manifest_run_defaults: RunLayer,
    max_concurrent_runs: usize,
    store: Arc<Database>,
    artifact_store: ArtifactStore,
) -> Arc<AppState> {
    ready_test_app_state_builder()
        .runtime_settings(server_settings, manifest_run_defaults)
        .max_concurrent_runs(max_concurrent_runs)
        .store_bundle(store, artifact_store)
        .build()
}

pub(crate) fn default_env_lookup() -> EnvLookup {
    Arc::new(process_env_var)
}

pub(crate) fn load_test_server_secrets(
    path: PathBuf,
    env: HashMap<String, String>,
) -> ServerSecrets {
    let mut env = env;
    let file_has_session_secret = envfile::read_env_file(&path)
        .ok()
        .is_some_and(|entries| entries.contains_key(EnvVars::SESSION_SECRET));
    if !env.contains_key(EnvVars::SESSION_SECRET) && !file_has_session_secret {
        env.insert(
            EnvVars::SESSION_SECRET.to_string(),
            "server-test-session-key-0123456789".to_string(),
        );
    }
    ServerSecrets::load(path, env).expect("test server secrets should load")
}

pub fn test_secret_store_path() -> PathBuf {
    let dir = std::env::temp_dir().join(format!("fabro-test-{}", Ulid::new()));
    std::fs::create_dir_all(&dir).expect("test temp dir should be creatable");
    dir.join("secrets.json")
}

/// Keeps tests off the developer's real `~/.fabro/storage`.
///
/// Settings built for tests usually omit `[server.storage] root`, which
/// resolves to the production default. Handlers that walk that tree — `df`,
/// `system/resources`, `prune` — then read whatever runs and scratch
/// directories the machine happens to have, making tests slow and
/// machine-dependent, and letting run-creating tests write there.
///
/// Only settings still carrying the production default are redirected; a test
/// that chose its own root keeps it. The redirect goes through
/// [`ServerSettings::with_storage_override`] so the derived local object-store
/// roots move with it instead of pointing back at the real storage tree.
fn redirect_default_storage_root(
    settings: ServerSettings,
    vault_path: &Path,
) -> anyhow::Result<ServerSettings> {
    if Path::new(&settings.server.storage.root) != default_storage_dir() {
        return Ok(settings);
    }
    let root = vault_path.with_file_name("storage");
    std::fs::create_dir_all(&root)
        .with_context(|| format!("creating test storage root at {}", root.display()))?;
    Ok(settings.with_storage_override(&root))
}

#[must_use]
pub fn test_auth_mode() -> AuthMode {
    AuthMode::Enabled(ConfiguredAuth {
        methods:    vec![ServerAuthMethod::DevToken, ServerAuthMethod::Github],
        dev_token:  Some(TEST_DEV_TOKEN.to_string()),
        jwt_key:    Some(
            auth::derive_jwt_key(TEST_SESSION_SECRET.as_bytes())
                .expect("test jwt signing key should derive"),
        ),
        jwt_issuer: Some("https://fabro.test".to_string()),
    })
}

pub fn build_test_router(state: Arc<AppState>) -> Router {
    with_test_user(server::build_router(state, test_auth_mode()))
}

pub fn build_test_router_with_options(state: Arc<AppState>, options: RouterOptions) -> Router {
    with_test_user(server::build_router_with_options(
        state,
        &test_auth_mode(),
        options,
    ))
}

pub fn with_test_user(router: Router) -> Router {
    router.layer(middleware::from_fn(inject_test_user_bearer))
}

async fn inject_test_user_bearer(mut req: Request, next: Next) -> Response {
    if req.uri().path().starts_with("/api/") && !req.headers().contains_key(header::AUTHORIZATION) {
        static BEARER: OnceLock<HeaderValue> = OnceLock::new();
        let bearer = BEARER.get_or_init(|| {
            HeaderValue::from_str(&format!("Bearer {}", issue_test_user_token()))
                .expect("test JWT bearer header is valid")
        });
        req.headers_mut()
            .insert(header::AUTHORIZATION, bearer.clone());
    }
    next.run(req).await
}

fn issue_test_user_token() -> String {
    let key = auth::derive_jwt_key(TEST_SESSION_SECRET.as_bytes())
        .expect("test jwt signing key should derive");
    auth::issue(
        &key,
        "https://fabro.test",
        &auth::JwtSubject {
            identity:    IdpIdentity::new("fabro:dev", "dev")
                .expect("test identity should be valid"),
            login:       "dev".to_string(),
            name:        "Dev Token".to_string(),
            email:       "dev@fabro.local".to_string(),
            avatar_url:  String::new(),
            user_url:    String::new(),
            auth_method: AuthMethod::DevToken,
        },
        ChronoDuration::days(3650),
    )
}

#[cfg(test)]
pub(crate) async fn capture_auth_context(
    AxumState(captured): AxumState<Arc<Mutex<Vec<RequestAuthContext>>>>,
    mut req: Request,
    next: Next,
) -> Response {
    let slot = AuthContextSlot::initial();
    req.extensions_mut().insert(slot.clone());
    let response = next.run(req).await;
    captured
        .lock()
        .expect("captured auth contexts lock poisoned")
        .push(slot.snapshot());
    response
}

#[cfg(test)]
mod tests {
    use fabro_types::settings::ObjectStoreSettings;

    use super::*;

    fn local_store_root(store: &ObjectStoreSettings) -> &Path {
        let ObjectStoreSettings::Local { root } = store else {
            panic!("test server settings should use a local object store");
        };
        Path::new(root)
    }

    #[test]
    fn default_storage_redirect_updates_derived_local_store_roots() {
        let temp_dir = tempfile::tempdir().expect("test temp dir should be created");
        let vault_path = temp_dir.path().join("secrets.json");

        let settings = redirect_default_storage_root(default_test_server_settings(), &vault_path)
            .expect("default test storage root should redirect");
        let storage_root = temp_dir.path().join("storage");

        assert_eq!(Path::new(&settings.server.storage.root), storage_root);
        assert_eq!(
            local_store_root(&settings.server.artifacts.store),
            storage_root.join("objects/artifacts")
        );
        assert_eq!(
            local_store_root(&settings.server.slatedb.store),
            storage_root.join("objects/slatedb")
        );
    }

    #[test]
    fn default_storage_redirect_preserves_explicit_storage_root() {
        let temp_dir = tempfile::tempdir().expect("test temp dir should be created");
        let vault_path = temp_dir.path().join("secrets.json");
        let expected =
            default_test_server_settings().with_storage_override(&temp_dir.path().join("custom"));

        let settings = redirect_default_storage_root(expected.clone(), &vault_path)
            .expect("explicit test storage root should be preserved");

        assert_eq!(settings, expected);
    }

    #[test]
    #[expect(
        clippy::disallowed_methods,
        reason = "synchronous fixture setup creates a blocking file before exercising the helper"
    )]
    fn default_storage_redirect_preserves_directory_creation_error_chain() {
        let temp_dir = tempfile::tempdir().expect("test temp dir should be created");
        let vault_path = temp_dir.path().join("secrets.json");
        std::fs::write(temp_dir.path().join("storage"), "not a directory")
            .expect("blocking storage path should be created");

        let error = redirect_default_storage_root(default_test_server_settings(), &vault_path)
            .expect_err("storage redirect should reject a file at the directory path");

        assert!(error.to_string().contains("creating test storage root at"));
        assert!(
            error.chain().count() >= 2,
            "filesystem error should remain in the source chain"
        );
    }
}
