//! Shared test scaffolding for `kb-app` integration tests.
//!
//! Each test gets a fresh `TempDir` and a `Config` whose storage paths
//! all point inside it, so the user's real `data_dir` / `model_dir`
//! is never touched. The fixture workspace at
//! `tests/fixtures/workspace/` is *copied* into the temp dir for each
//! test so a write-side ingest can't trip on a read-only fixture
//! tree. The default lane (no `--ignored`) opts out of embeddings via
//! `provider = "none"` so AVX is not required.

#![allow(dead_code)]

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

use kebab_config::Config;
use tempfile::TempDir;

/// Test environment: owns a `TempDir` and exposes a `Config` whose
/// storage paths live inside it.
pub struct TestEnv {
    pub temp: TempDir,
    pub workspace_root: PathBuf,
    pub config: Config,
}

impl TestEnv {
    /// Build an env with embeddings disabled (lexical-only). Default
    /// lane — no AVX, no fastembed download.
    pub fn lexical_only() -> Self {
        let env = Self::new_inner();
        let mut e = env;
        e.config.models.embedding.provider = "none".to_string();
        e.config.models.embedding.dimensions = 0;
        e
    }

    /// Build an env with the default fastembed embedding provider.
    /// Used by AVX-gated `#[ignore]` tests.
    pub fn with_embeddings() -> Self {
        Self::new_inner()
    }

    fn new_inner() -> Self {
        let temp = tempfile::tempdir().expect("tempdir");
        let workspace_root = temp.path().join("workspace");
        copy_fixture_workspace(&workspace_root);

        let data_dir = temp.path().join("data");
        std::fs::create_dir_all(&data_dir).unwrap();
        let model_dir = temp.path().join("models");
        std::fs::create_dir_all(&model_dir).unwrap();

        let mut config = Config::defaults();
        config.workspace.root = Some(workspace_root.to_string_lossy().into_owned());
        // Drop the ".obsidian" / "node_modules" excludes — they bring
        // in nothing useful for fixtures and just hide debugging.
        config.workspace.exclude.clear();
        config.storage.data_dir = data_dir.to_string_lossy().into_owned();
        // Pin model_dir to the TempDir so a future fastembed-touching
        // test can't accidentally write to the user's `~/.local/share`.
        config.storage.model_dir = model_dir.to_string_lossy().into_owned();
        // Drop in a small chunk policy so the fixture's small files
        // emit at least a couple of chunks even with overlap_tokens
        // honored.
        config.ingest.chunking.target_tokens = 80;
        config.ingest.chunking.overlap_tokens = 20;

        Self {
            temp,
            workspace_root,
            config,
        }
    }

    pub fn scope(&self) -> kebab_core::SourceScope {
        kebab_core::SourceScope {
            root: self.workspace_root.clone(),
            exclude: self.config.workspace.exclude.clone(),
            ..Default::default()
        }
    }

    /// p9-fb-34 alias — tests added in fb-34 invoke `TestEnv::new()`
    /// per the plan; route to the existing lexical-only constructor
    /// so the lane stays AVX-free without churning all the existing
    /// callers.
    pub fn new() -> Self {
        Self::lexical_only()
    }

    /// p9-fb-34: open a fresh `App` against this env's config. Used
    /// by integration tests that need to call `App::search_with_opts`
    /// directly. Caller can invoke this multiple times to simulate
    /// re-opening the binary after a corpus revision bump.
    pub fn app(&self) -> kebab_app::App {
        kebab_app::App::open_with_config(self.config.clone()).expect("App::open_with_config")
    }
}

/// p9-fb-34: write `content` into the env's workspace at
/// `relative_path`, then run a full ingest so the document is
/// searchable. Mirrors the convenience helpers used by other
/// `TestEnv`-driven crates.
pub fn ingest_md(env: &TestEnv, relative_path: &str, content: &str) {
    let path = env.workspace_root.join(relative_path);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).expect("create parent dirs");
    }
    std::fs::write(&path, content).expect("write workspace file");
    kebab_app::ingest_with_config(env.config.clone(), env.scope(), true)
        .expect("ingest_with_config");
}

/// Test helper: build a `SearchQuery` for lexical mode at k=10. Used
/// by every kebab-app integration test that calls
/// `kebab_app::search_with_config`. Centralized here so a future
/// `SearchQuery` field bump only edits one site.
pub fn lexical_query(text: &str) -> kebab_core::SearchQuery {
    kebab_core::SearchQuery {
        text: text.to_string(),
        mode: kebab_core::SearchMode::Lexical,
        k: 10,
        filters: kebab_core::SearchFilters::default(),
    }
}

/// p9-fb-32: rewrite `documents.updated_at` for one workspace path
/// to `now - days_ago` (RFC3339 UTC). Used by staleness integration
/// tests to simulate aged-out docs without faking system time. Caller
/// is responsible for ingesting the doc *before* calling this — the
/// row must already exist.
pub fn backdate_document_updated_at(env: &TestEnv, workspace_path: &str, days_ago: i64) {
    let backdated = (time::OffsetDateTime::now_utc() - time::Duration::days(days_ago))
        .format(&time::format_description::well_known::Rfc3339)
        .expect("format backdated updated_at");
    let db_path = PathBuf::from(&env.config.storage.data_dir).join("kebab.sqlite");
    let conn = rusqlite::Connection::open(&db_path).expect("open kebab.sqlite");
    let updated = conn
        .execute(
            "UPDATE documents SET updated_at = ?1 WHERE workspace_path = ?2",
            rusqlite::params![backdated, workspace_path],
        )
        .expect("UPDATE documents.updated_at");
    assert_eq!(
        updated, 1,
        "backdate_document_updated_at: expected to update exactly 1 row for {workspace_path}, got {updated}"
    );
}

fn copy_fixture_workspace(dest: &Path) {
    let src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("workspace");
    copy_dir_recursive(&src, dest);
}

fn copy_dir_recursive(src: &Path, dest: &Path) {
    std::fs::create_dir_all(dest).unwrap();
    for entry in std::fs::read_dir(src).expect("read fixture dir") {
        let entry = entry.unwrap();
        let path = entry.path();
        let target = dest.join(entry.file_name());
        if path.is_dir() {
            copy_dir_recursive(&path, &target);
        } else {
            std::fs::copy(&path, &target).expect("copy fixture file");
        }
    }
}

pub mod mock_ocr;
