//! Golden route replay — classify and replay supported rows on compact `symforge`.
#![cfg(feature = "server")]

#[path = "support/stel_surface_env.rs"]
mod stel_surface_env;

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

use symforge::live_index::LiveIndex;
use symforge::protocol::SymForgeServer;
use symforge::stel::{self, GoldenRouteRow};

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn corpus_path(relative: &str) -> PathBuf {
    repo_root().join(relative)
}

fn corpus_available(relative: &str, marker: &str) -> bool {
    corpus_path(relative).join(marker).is_file()
}

fn all_replay_corpora_available() -> bool {
    corpus_available(stel::S4_REPLAY_CORPUS, "src/lib.rs")
        && corpus_available("tests/fixtures/phase0-corpus/records-python", "records.py")
        && corpus_available("tests/fixtures/phase0-corpus/is-plain-obj-ts", "index.js")
        && corpus_available("tests/fixtures/compression_ratio/rust", "service.rs")
}

fn corpus_available_for_row(row: &GoldenRouteRow) -> bool {
    let corpus = stel::corpus_for_row_id(&row.id);
    let marker = stel::corpus_marker_for_row_id(&row.id);
    corpus_available(corpus, marker)
}

fn golden_fixture_path() -> PathBuf {
    repo_root().join(stel::GOLDEN_ROUTES_FIXTURE)
}

fn tool_result_text(result: &serde_json::Value) -> &str {
    result["content"][0]["text"]
        .as_str()
        .expect("symforge result must contain text content")
}

fn server_for_corpus(relative: &str, project: &str) -> SymForgeServer {
    let root = corpus_path(relative);
    let shared = LiveIndex::load(&root).unwrap_or_else(|error| {
        panic!("index {}: {error}", root.display());
    });
    SymForgeServer::new(
        shared,
        project.to_string(),
        std::sync::Arc::new(parking_lot::Mutex::new(
            symforge::watcher::WatcherInfo::default(),
        )),
        Some(root),
        None,
    )
}

async fn replay_row(server: &SymForgeServer, row: &GoldenRouteRow) -> String {
    let request = row.to_request();
    let params = serde_json::to_value(stel::SymforgeCallInput {
        request,
        probe_legacy_tool: None,
        probe_legacy_args: None,
    })
    .expect("symforge params serialize");
    let result = server
        .dispatch_tool_result_for_tests("symforge", params)
        .await
        .expect("symforge dispatch");
    let serialized = serde_json::to_value(&result).expect("serialize CallToolResult");
    tool_result_text(&serialized).to_string()
}

#[test]
fn golden_corpus_classification_accounts_for_all_rows() {
    let rows = stel::load_golden_rows(&golden_fixture_path()).expect("golden fixture");
    let classification = stel::classify_golden_corpus(&rows);
    assert_eq!(classification.row_count(), rows.len());
    assert!(
        classification.deferred_planner_mismatch.is_empty(),
        "planner mismatches must be empty or listed explicitly in deferred_planner_mismatch_ids_are_stable"
    );
    for id in stel::S4_EXIT_ROW_IDS {
        assert!(
            classification
                .supported_serve
                .iter()
                .any(|row_id| row_id == id),
            "S4 minimum subset must remain supported: {id}"
        );
    }
}

#[test]
fn s4_exit_rows_align_with_planner_routing() {
    let rows = stel::load_golden_rows(&golden_fixture_path()).expect("golden fixture");
    let mut mismatches = Vec::new();
    for row in stel::s4_exit_rows(&rows) {
        let request = stel::request_for_golden_row(row);
        let plan = stel::build_plan(&request);
        if row.must_call.first().map(String::as_str) != Some(plan.steps[0].tool.as_str()) {
            mismatches.push(format!(
                "{}: golden `{}` vs planner `{}` for {:?}",
                row.id,
                row.must_call.first().map(String::as_str).unwrap_or("?"),
                plan.steps[0].tool,
                row.query
            ));
        }
    }
    assert!(
        mismatches.is_empty(),
        "S4 exit rows must match L1 planner routing:\n{}",
        mismatches.join("\n")
    );
}

#[tokio::test]
async fn s4_minimum_subset_replays_on_compact_symforge() {
    if !all_replay_corpora_available() {
        eprintln!(
            "skip s4_minimum_subset_replays: clone phase0 corpora per tests/fixtures/phase0-corpus/README.md"
        );
        return;
    }

    let _guard = stel_surface_env::COMPACT_ENV_LOCK.lock().await;
    let _surface = stel_surface_env::set_symforge_surface("compact");
    // Force the FULL trust envelope: the replay validators assert the full
    // contract (`── stel ──`, `decision: serve|bypass`, `ledger:`), which the
    // live serve path generates only in full mode.
    let _full = stel_surface_env::force_full_stel_envelope();

    let rows = stel::load_golden_rows(&golden_fixture_path()).expect("load golden fixture");
    let exit_rows = stel::s4_exit_rows(&rows);
    replay_serve_rows_grouped_by_corpus(&exit_rows).await;
}

#[tokio::test]
async fn supported_serve_rows_replay_with_envelope_and_ledger() {
    if !all_replay_corpora_available() {
        eprintln!("skip supported_serve_rows_replay: missing corpora");
        return;
    }

    let _guard = stel_surface_env::COMPACT_ENV_LOCK.lock().await;
    let _surface = stel_surface_env::set_symforge_surface("compact");
    // Force the FULL trust envelope: the replay validators assert the full
    // contract (`── stel ──`, `decision: serve|bypass`, `ledger:`), which the
    // live serve path generates only in full mode.
    let _full = stel_surface_env::force_full_stel_envelope();

    let rows = stel::load_golden_rows(&golden_fixture_path()).expect("load golden fixture");
    let serve_rows: Vec<_> = stel::supported_serve_rows(&rows)
        .into_iter()
        .filter(|row| corpus_available_for_row(row))
        .collect();
    assert!(
        serve_rows.len() >= stel::S4_EXIT_ROW_IDS.len(),
        "supported serve replay must be broader than the S4 minimum subset"
    );
    replay_serve_rows_grouped_by_corpus(&serve_rows).await;
}

#[tokio::test]
async fn supported_pff_rows_bypass_without_legacy_execution() {
    if !all_replay_corpora_available() {
        eprintln!("skip supported_pff_rows_bypass: missing corpora");
        return;
    }

    let _guard = stel_surface_env::COMPACT_ENV_LOCK.lock().await;
    let _surface = stel_surface_env::set_symforge_surface("compact");
    // Force the FULL trust envelope: the replay validators assert the full
    // contract (`── stel ──`, `decision: serve|bypass`, `ledger:`), which the
    // live serve path generates only in full mode.
    let _full = stel_surface_env::force_full_stel_envelope();

    let rows = stel::load_golden_rows(&golden_fixture_path()).expect("load golden fixture");
    let pff_rows: Vec<_> = stel::supported_pff_rows(&rows)
        .into_iter()
        .filter(|row| corpus_available_for_row(row))
        .collect();
    assert_eq!(pff_rows.len(), 4, "all four P-FF golden rows must replay");

    let mut by_corpus: BTreeMap<&str, Vec<&GoldenRouteRow>> = BTreeMap::new();
    for row in pff_rows {
        by_corpus
            .entry(stel::corpus_for_row_id(&row.id))
            .or_default()
            .push(row);
    }

    let mut failures = Vec::new();
    for (corpus, corpus_rows) in by_corpus {
        let project = Path::new(corpus)
            .file_name()
            .and_then(|s| s.to_str())
            .unwrap_or("replay");
        let server = server_for_corpus(corpus, project);
        for row in corpus_rows {
            let output = replay_row(&server, row).await;
            let validation = stel::validate_pff_replay_output(row, &output);
            if !validation.passed {
                failures.push(format!(
                    "{}: {}",
                    validation.row_id,
                    validation.errors.join("; ")
                ));
            }
        }
    }

    assert!(
        failures.is_empty(),
        "P-FF golden replay failures:\n{}",
        failures.join("\n")
    );
}

async fn replay_serve_rows_grouped_by_corpus(rows: &[&GoldenRouteRow]) {
    let mut by_corpus: BTreeMap<&str, Vec<&GoldenRouteRow>> = BTreeMap::new();
    for row in rows {
        by_corpus
            .entry(stel::corpus_for_row_id(&row.id))
            .or_default()
            .push(row);
    }

    let mut failures = Vec::new();
    for (corpus, corpus_rows) in by_corpus {
        let project = Path::new(corpus)
            .file_name()
            .and_then(|s| s.to_str())
            .unwrap_or("replay");
        let server = server_for_corpus(corpus, project);
        for row in corpus_rows {
            let output = replay_row(&server, row).await;
            let validation = stel::validate_serve_replay_output(row, &output);
            if !validation.passed {
                let tail: String = output
                    .chars()
                    .rev()
                    .take(400)
                    .collect::<String>()
                    .chars()
                    .rev()
                    .collect();
                failures.push(format!(
                    "{}: {}\n--- output tail ---\n{tail}",
                    validation.row_id,
                    validation.errors.join("; "),
                ));
            }
        }
    }

    assert!(
        failures.is_empty(),
        "serve golden replay failures:\n{}",
        failures.join("\n\n")
    );
}

// ===========================================================================
// T035 (feature 013 US2) — routing/policy/golden-replay is byte-exact across
// deferred/accumulating/tuned states (calibration changes ESTIMATES, not
// DECISIONS), and calibration writes never bump frecency. Proves FR-007 / SC-004
// and the estimate-only contract (Principle V/VII).
// ===========================================================================

/// The L2 DECISION for every golden row is identical whether tuning is None,
/// matching the deferred/accumulating scenario where no tuning is in force.
/// `evaluate_plan_tuned(.., None)` MUST equal `evaluate_plan` byte-for-byte, so
/// the calibration machinery cannot perturb routing/policy when no validated
/// tuning exists (the golden-replay topology).
#[test]
fn golden_row_decisions_are_byte_exact_with_no_tuning_in_force() {
    use symforge::stel::controller::{evaluate_plan, evaluate_plan_tuned};

    let rows = stel::load_golden_rows(&golden_fixture_path()).expect("golden fixture");
    for row in &rows {
        let request = stel::request_for_golden_row(row);
        let plan = stel::build_plan(&request);

        let baseline = evaluate_plan(&request, &plan);
        let with_machinery = evaluate_plan_tuned(&request, &plan, None, None);

        // Serialize both decisions and compare byte-for-byte: the calibration
        // code path with `tuned = None` is the identity of the pre-013 decision.
        let baseline_json = serde_json::to_string(&baseline).expect("serialize baseline");
        let machinery_json = serde_json::to_string(&with_machinery).expect("serialize machinery");
        assert_eq!(
            baseline_json, machinery_json,
            "row {}: calibration machinery (tuned=None) must not change the decision",
            row.id
        );

        // And the routing/policy expectation the golden corpus pins is unchanged.
        assert_eq!(
            with_machinery.decision, row.expected_decision,
            "row {}: golden expected decision must hold under the calibration path",
            row.id
        );
    }
}

/// Persisting accepted tuned constants (the calibration write) must NOT bump
/// discovery/search frecency — calibration is estimate-only and has no business
/// touching the ranking subsystem (Principle V).
#[test]
fn calibration_write_does_not_bump_frecency() {
    use symforge::live_index::frecency::FrecencyStore;
    use symforge::stel::ledger_store::{
        CURRENT_ESTIMATOR_VERSION, StelLedgerStore, TunedEstimateConstants,
    };

    let frecency = FrecencyStore::open_in_memory().expect("open frecency store");
    let before = frecency.last_10_bumps().expect("frecency snapshot before");
    assert!(before.is_empty(), "fresh frecency store has no bumps");

    // Store + reload an active tuning set (the audited gated-action write). The
    // STEL ledger store has no dependency on frecency, so this must not touch
    // discovery ranking at all.
    let store = StelLedgerStore::open_in_memory("sess-calib-frecency").expect("ledger store");
    let tuned = TunedEstimateConstants {
        response_correction_factor: 2.0,
        estimator_version: CURRENT_ESTIMATOR_VERSION.to_string(),
        sample_size: 60,
        error_before: 300.0,
        error_after: 12.0,
        tuned_at_ms: 1,
    };
    store.store_active_tuning(&tuned).expect("store tuning");
    let _ = store
        .load_active_tuning(CURRENT_ESTIMATOR_VERSION)
        .expect("load tuning");

    let after = frecency.last_10_bumps().expect("frecency snapshot after");
    assert_eq!(
        before, after,
        "calibration writes must NOT bump discovery/search frecency"
    );

    // Control: a genuine bump DOES change the snapshot (assertion is not vacuous).
    frecency
        .bump(&[std::path::PathBuf::from("src/lib.rs")], 1_718_000_000)
        .expect("control bump");
    let bumped = frecency.last_10_bumps().expect("snapshot after bump");
    assert_ne!(
        after, bumped,
        "a genuine frecency bump must change the snapshot"
    );
}
