// Server-only integration test: depends on a `#[cfg(feature = "server")]`
// module (protocol/daemon/cli/sidecar/watcher/analytics). Gating the whole
// file keeps `--no-default-features --features embed --all-targets` compiling.
#![cfg(feature = "server")]

//! Parity tests — locks in current `search_files` ranking behavior.
//!
//! These tests MUST pass against current `main` *before* the `RankSignal`
//! trait refactor lands. After the refactor, they must still pass
//! byte-identically — same tiers, same ordering, same ancillary lines. That
//! invariant is the refactor's acceptance bar (CONTEXT.md §Acceptance bar).
//!
//! ## Update workflow
//!
//! The golden strings below are generated by running this test with
//! `SYMFORGE_UPDATE_RANK_GOLDEN=1`. The run will fail with a diff-friendly
//! `println!` dump of the actual output for each query; the maintainer
//! pastes the dump into the `expected` column. Subsequent runs assert
//! byte-identical equality.
//!
//! Dumps include the full tool return string (envelope + ranked body) so
//! that *any* drift — reordering, tier re-labeling, evidence-line changes,
//! new hints — is caught.

use std::fs;
use std::path::PathBuf;
use std::sync::Arc;

use parking_lot::Mutex;
use serde_json::{Value, json};
use symforge::live_index::LiveIndex;
use symforge::protocol::SymForgeServer;
use symforge::watcher::WatcherInfo;
use tempfile::TempDir;

// ─── Fixture corpus ──────────────────────────────────────────────────────────
//
// A small but deliberately varied set of source-file paths chosen to exercise
// every ranking tier the current code distinguishes:
//
//   * Exact/suffix path hits        → Strong path matches
//   * Basename-only hits            → Basename matches
//   * Single-component path hits    → Loose path matches
//   * Prefix token hits             → Loose path matches
//
// Keep this list stable — reordering files here changes golden values.
const FIXTURE_FILES: &[(&str, &str)] = &[
    ("src/lib.rs", "pub fn lib_root() {}\n"),
    ("src/main.rs", "fn main() {}\n"),
    (
        "src/protocol/tools.rs",
        "pub fn tools_handler() {}\npub fn search_files() {}\n",
    ),
    ("src/protocol/format.rs", "pub fn format_path() {}\n"),
    ("src/protocol/mod.rs", "pub mod tools;\npub mod format;\n"),
    ("src/live_index/store.rs", "pub struct LiveIndex;\n"),
    ("src/live_index/search.rs", "pub fn search_index() {}\n"),
    ("src/live_index/mod.rs", "pub mod store;\npub mod search;\n"),
    (
        "tests/tools_integration.rs",
        "// tests for the tools module\n",
    ),
    (
        "tests/format_integration.rs",
        "// tests for the format module\n",
    ),
];

// ─── Queries + golden outputs ────────────────────────────────────────────────

struct GoldenCase {
    query: &'static str,
    current_file: Option<&'static str>,
    expected: &'static str,
}

const GOLDEN_CASES: &[GoldenCase] = &[
    // 1. Exact path hit (strong)
    GoldenCase {
        query: "src/protocol/tools.rs",
        current_file: None,
        expected: GOLDEN_EXACT_PATH_HIT,
    },
    // 2. Basename-only match surfaces in basename tier
    GoldenCase {
        query: "tools.rs",
        current_file: None,
        expected: GOLDEN_BASENAME_TOOLS_RS,
    },
    // 3. Prefix match — loose path matches by prefix.
    GoldenCase {
        query: "protocol",
        current_file: None,
        expected: GOLDEN_LOOSE_PROTOCOL,
    },
    // 4. Multi-component path query
    GoldenCase {
        query: "live_index/search.rs",
        current_file: None,
        expected: GOLDEN_MULTI_COMPONENT_LIVE_SEARCH,
    },
    // 5. current-file boost — ties broken by shared directory prefix.
    GoldenCase {
        query: "mod.rs",
        current_file: Some("src/protocol/tools.rs"),
        expected: GOLDEN_CURRENT_FILE_BOOST,
    },
    // 6. Glob — stable lexicographic fallback.
    GoldenCase {
        query: "src/protocol/*.rs",
        current_file: None,
        expected: GOLDEN_GLOB,
    },
    // 7. No match — deterministic message.
    GoldenCase {
        query: "definitely_not_in_fixture_xyzzy",
        current_file: None,
        expected: GOLDEN_NOT_FOUND,
    },
];

// ─── Fixture helpers ─────────────────────────────────────────────────────────

struct Fixture {
    _dir: TempDir,
    server: SymForgeServer,
}

impl Fixture {
    fn new() -> Self {
        let dir = tempfile::tempdir().expect("tempdir");
        let root: PathBuf = dir.path().to_path_buf();
        for (rel, content) in FIXTURE_FILES {
            let path = root.join(rel);
            if let Some(parent) = path.parent() {
                fs::create_dir_all(parent).expect("create parent");
            }
            fs::write(&path, content).expect("write fixture");
        }
        let shared = LiveIndex::load(&root).expect("LiveIndex::load");
        let watcher_info = Arc::new(Mutex::new(WatcherInfo::default()));
        let server = SymForgeServer::new(
            shared,
            "rank_signal_behavior_test".to_string(),
            watcher_info,
            Some(root),
            None,
        );
        Self { _dir: dir, server }
    }

    async fn search(&self, query: &str, current_file: Option<&str>) -> String {
        let mut params = json!({ "query": query, "limit": 20 });
        if let Some(cf) = current_file {
            params["current_file"] = Value::String(cf.to_string());
        }
        self.server
            .dispatch_tool_for_tests("search_files", params)
            .await
    }
}

// ─── Test driver ─────────────────────────────────────────────────────────────

#[tokio::test]
async fn search_files_rankings_match_golden() {
    let fx = Fixture::new();
    let update = std::env::var("SYMFORGE_UPDATE_RANK_GOLDEN").is_ok();

    let mut failures: Vec<String> = Vec::new();
    for case in GOLDEN_CASES {
        let actual = fx.search(case.query, case.current_file).await;
        if update {
            failures.push(format!(
                "── query: {:?} current_file={:?} ──\n{actual}\n",
                case.query, case.current_file
            ));
            continue;
        }
        if actual != case.expected {
            failures.push(format!(
                "── query: {:?} current_file={:?} ──\n\
                 --- EXPECTED ---\n{}\n--- ACTUAL ---\n{actual}\n",
                case.query, case.current_file, case.expected
            ));
        }
    }

    if !failures.is_empty() {
        let msg = if update {
            format!(
                "SYMFORGE_UPDATE_RANK_GOLDEN=1 — paste these into GOLDEN_* constants:\n\n{}",
                failures.join("\n")
            )
        } else {
            format!(
                "rank-signal parity drift — {} case(s) diverged:\n\n{}",
                failures.len(),
                failures.join("\n")
            )
        };
        panic!("{msg}");
    }
}

// ─── Golden constants ────────────────────────────────────────────────────────
//
// Populate by running `SYMFORGE_UPDATE_RANK_GOLDEN=1 cargo test --test
// rank_signal_behavior -- --test-threads=1 --nocapture`, then paste each
// block into the corresponding constant. Keep trailing-newline semantics
// exact — `search_files` returns strings without a trailing newline.

const GOLDEN_EXACT_PATH_HIT: &str = "\
Trust: constrained (tiered path relevance) | current index | parsed | full for current scope
Scope: ranked indexed file paths; filters: vendor filtered; personal tooling filtered
Evidence: paths `src/protocol/tools.rs`

1 matching file
── Strong path matches ──
  src/protocol/tools.rs  [0.80]";

const GOLDEN_BASENAME_TOOLS_RS: &str = "\
Trust: constrained (tiered path relevance) | current index | parsed | full for current scope
Scope: ranked indexed file paths; filters: vendor filtered; personal tooling filtered
Evidence: paths `src/protocol/tools.rs`

1 matching file
── Basename matches ──
  src/protocol/tools.rs  [0.90]";

const GOLDEN_LOOSE_PROTOCOL: &str = "\
Trust: heuristic (loose path relevance) | current index | parsed | full for current scope
Scope: ranked indexed file paths; filters: vendor filtered; personal tooling filtered
Evidence: paths `src/protocol/mod.rs`, `src/protocol/tools.rs`, `src/protocol/format.rs`

3 matching files
── Loose path matches ──
  src/protocol/mod.rs  [0.40]
  src/protocol/tools.rs  [0.40]
  src/protocol/format.rs  [0.40]";

const GOLDEN_MULTI_COMPONENT_LIVE_SEARCH: &str = "\
Trust: constrained (tiered path relevance) | current index | parsed | full for current scope
Scope: ranked indexed file paths; filters: vendor filtered; personal tooling filtered
Evidence: paths `src/live_index/search.rs`

1 matching file
── Strong path matches ──
  src/live_index/search.rs  [0.80]";

const GOLDEN_CURRENT_FILE_BOOST: &str = "\
Trust: constrained (tiered path relevance) | current index | parsed | full for current scope
Scope: ranked indexed file paths; current file boost `src/protocol/tools.rs`; filters: vendor filtered; personal tooling filtered
Evidence: paths `src/protocol/mod.rs`, `src/live_index/mod.rs`

2 matching files
── Basename matches ──
  src/protocol/mod.rs  [0.90]
  src/live_index/mod.rs  [0.90]";

const GOLDEN_GLOB: &str = "\
Trust: constrained (tiered path relevance) | current index | parsed | full for current scope
Scope: ranked indexed file paths; filters: vendor filtered; personal tooling filtered
Evidence: paths `src/protocol/format.rs`, `src/protocol/mod.rs`, `src/protocol/tools.rs`

3 matching files
── Strong path matches ──
  src/protocol/format.rs  [0.80]
  src/protocol/mod.rs  [0.80]
  src/protocol/tools.rs  [0.80]";

const GOLDEN_NOT_FOUND: &str = "\
No indexed source files matching 'definitely_not_in_fixture_xyzzy'

filters: vendor filtered; personal tooling filtered";
