#![allow(clippy::panic)]

//! Regression tests for runtime cache budgets, bypass rules, and repository invalidation.

use super::*;

#[test]
fn runtime_cache_registry_classifies_snapshot_metadata_and_request_local_families() {
    let server = FriggMcpServer::new(fixture_config());

    let manifest = server.runtime_cache_policy(RuntimeCacheFamily::ValidatedManifestCandidate);
    assert_eq!(manifest.residency, RuntimeCacheResidency::ProcessWide);
    assert_eq!(
        manifest.reuse_class,
        RuntimeCacheReuseClass::SnapshotScopedReusable
    );
    assert_eq!(
        manifest.freshness_contract,
        RuntimeCacheFreshnessContract::RepositorySnapshot
    );
    assert!(manifest.dirty_root_bypass);
    assert_eq!(manifest.budget.max_entries, Some(128));

    let process_metadata = server.runtime_cache_policy(RuntimeCacheFamily::HeuristicReference);
    assert_eq!(
        process_metadata.residency,
        RuntimeCacheResidency::ProcessWide
    );
    assert_eq!(
        process_metadata.reuse_class,
        RuntimeCacheReuseClass::ProcessMetadata
    );
    assert_eq!(
        process_metadata.freshness_contract,
        RuntimeCacheFreshnessContract::RepositoryId
    );
    assert!(process_metadata.dirty_root_bypass);
    assert_eq!(process_metadata.budget.max_entries, Some(128));

    let request_local = server.runtime_cache_policy(RuntimeCacheFamily::SearcherProjectionStore);
    assert_eq!(request_local.residency, RuntimeCacheResidency::RequestLocal);
    assert_eq!(
        request_local.reuse_class,
        RuntimeCacheReuseClass::RequestLocalOnly
    );
    assert_eq!(
        request_local.freshness_contract,
        RuntimeCacheFreshnessContract::RequestLocal
    );
    assert_eq!(request_local.budget.max_entries, None);
    assert_eq!(request_local.budget.max_bytes, None);
}

#[test]
fn runtime_text_searchers_share_projection_store_service_across_requests() {
    let workspace_root = temp_workspace_root("runtime-shared-projection-store");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace src directory");
    fs::create_dir_all(workspace_root.join(".github/workflows"))
        .expect("failed to create workflow fixture directory");
    fs::write(
        workspace_root.join("Cargo.toml"),
        "[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
    )
    .expect("failed to write manifest fixture");
    fs::write(workspace_root.join("src/main.rs"), "fn main() {}\n")
        .expect("failed to write source fixture");
    fs::write(
        workspace_root.join(".github/workflows/ci.yml"),
        "name: ci\non: push\n",
    )
    .expect("failed to write workflow fixture");
    seed_manifest_snapshot(
        &workspace_root,
        "repo-001",
        "snapshot-001",
        &["Cargo.toml", "src/main.rs", ".github/workflows/ci.yml"],
    );

    let server = FriggMcpServer::new_with_runtime_options(
        FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
            .expect("workspace root must produce valid config"),
        false,
    );
    assert_eq!(
        server
            .runtime_state
            .searcher_projection_store_service
            .entrypoint_surface_cache_len(),
        0
    );

    let first_searcher = server.runtime_text_searcher(server.config.as_ref().clone());
    let repository = first_searcher
        .first_repository_candidate_universe()
        .expect("expected manifest-backed repository");
    let first = first_searcher
        .load_or_build_entrypoint_surface_projections_for_repository(&repository, "snapshot-001")
        .expect("first request should load entrypoint projections");
    assert!(
        !first.is_empty(),
        "projection fixture should decode surfaces"
    );
    assert_eq!(
        server
            .runtime_state
            .searcher_projection_store_service
            .entrypoint_surface_cache_len(),
        1
    );

    let second_searcher = server.runtime_text_searcher(server.config.as_ref().clone());
    let second_repository = second_searcher
        .first_repository_candidate_universe()
        .expect("expected manifest-backed repository");
    let second = second_searcher
        .load_or_build_entrypoint_surface_projections_for_repository(
            &second_repository,
            "snapshot-001",
        )
        .expect("second request should reuse entrypoint projections");
    assert_eq!(&*first, &*second);
    assert_eq!(
        server
            .runtime_state
            .searcher_projection_store_service
            .entrypoint_surface_cache_len(),
        1
    );

    let _ = fs::remove_dir_all(workspace_root);
}

#[tokio::test]
async fn read_file_and_explore_read_current_file_content() {
    let workspace_root = temp_workspace_root("file-content-direct-reads");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(
        workspace_root.join("src/lib.rs"),
        "pub fn alpha() {}\npub fn beta() {}\n",
    )
    .expect("failed to write source fixture");

    let server = FriggMcpServer::new(
        FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
            .expect("workspace root must produce valid config"),
    );
    let workspace = server
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");
    seed_manifest_snapshot(
        &workspace_root,
        &workspace.runtime_repository_id,
        "snapshot-001",
        &["src/lib.rs"],
    );
    server
        .adopt_workspace(&workspace, true)
        .expect("server should adopt known workspace");

    let read_params = crate::mcp::types::ReadFileParams {
        path: "src/lib.rs".to_owned(),
        repository_id: Some(workspace.repository_id.clone()),
        max_bytes: None,
        start_line: None,
        end_line: None,
        line_count: None,
        presentation_mode: Some(crate::mcp::types::ReadPresentationMode::Json),
        include_context_efficiency: None,
    };
    let first_read = server
        .read_file_impl(read_params.clone())
        .await
        .expect("first read_file call should succeed");
    assert!(first_read.content.contains("pub fn alpha"));

    let first_explore = server
        .explore_impl(crate::mcp::types::ExploreParams {
            path: "src/lib.rs".to_owned(),
            repository_id: Some(workspace.repository_id.clone()),
            operation: crate::mcp::types::ExploreOperation::Probe,
            query: Some("beta".to_owned()),
            pattern_type: Some(crate::mcp::types::SearchPatternType::Literal),
            anchor: None,
            context_lines: None,
            max_matches: Some(4),
            resume_from: None,
            presentation_mode: None,
            include_context_efficiency: None,
        })
        .await
        .expect("explore should read the source file");
    assert_eq!(first_explore.total_matches, 1);

    let second_read = server
        .read_file_impl(read_params)
        .await
        .expect("second read_file call should read the source file");
    assert_eq!(first_read.content, second_read.content);

    let _ = fs::remove_dir_all(workspace_root);
}

#[tokio::test]
async fn read_file_rejects_empty_file_start_line_outside_bounds() {
    let workspace_root = temp_workspace_root("read-file-empty-start-line-outside");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(workspace_root.join("src/empty.rs"), "")
        .expect("failed to write empty source fixture");

    let server = FriggMcpServer::new(
        FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
            .expect("workspace root must produce valid config"),
    );
    let workspace = server
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");
    server
        .adopt_workspace(&workspace, true)
        .expect("server should adopt known workspace");

    let error = server
        .read_file_impl(crate::mcp::types::ReadFileParams {
            path: "src/empty.rs".to_owned(),
            repository_id: Some(workspace.repository_id.clone()),
            max_bytes: None,
            start_line: Some(2),
            end_line: None,
            line_count: None,
            presentation_mode: Some(crate::mcp::types::ReadPresentationMode::Json),
            include_context_efficiency: None,
        })
        .await
        .expect_err("empty files should reject out-of-range start_line");

    assert_eq!(error.code, ErrorCode::INVALID_PARAMS);
    assert_eq!(error.message, "start_line is outside file bounds");
    assert_eq!(
        error
            .data
            .as_ref()
            .and_then(|value| value.get("total_lines")),
        Some(&Value::from(0))
    );

    let _ = fs::remove_dir_all(workspace_root);
}

#[tokio::test]
async fn explore_empty_file_scan_scope_stays_one_based() {
    let workspace_root = temp_workspace_root("explore-empty-scan-scope");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(workspace_root.join("src/empty.rs"), "")
        .expect("failed to write empty source fixture");

    let server = FriggMcpServer::new(
        FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
            .expect("workspace root must produce valid config"),
    );
    let workspace = server
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");
    server
        .adopt_workspace(&workspace, true)
        .expect("server should adopt known workspace");

    let response = server
        .explore_impl(crate::mcp::types::ExploreParams {
            path: "src/empty.rs".to_owned(),
            repository_id: Some(workspace.repository_id.clone()),
            operation: crate::mcp::types::ExploreOperation::Probe,
            query: Some("needle".to_owned()),
            pattern_type: Some(crate::mcp::types::SearchPatternType::Literal),
            anchor: None,
            context_lines: None,
            max_matches: Some(4),
            resume_from: None,
            presentation_mode: None,
            include_context_efficiency: None,
        })
        .await
        .expect("explore should scan empty files without zero-based coordinates");

    assert_eq!(response.total_lines, 0);
    assert_eq!(response.scan_scope.start_line, 1);
    assert_eq!(response.scan_scope.end_line, 0);

    let _ = fs::remove_dir_all(workspace_root);
}

#[tokio::test(flavor = "current_thread")]
async fn watch_leases_follow_session_adoption_counts() {
    let workspace_root = temp_workspace_root("watch-lease-counts");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(
        workspace_root.join("src/lib.rs"),
        "pub struct LeaseCount;\n",
    )
    .expect("failed to write workspace root fixture");

    let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
        .expect("workspace root must produce valid config");
    config.watch = WatchConfig {
        mode: WatchMode::On,
        debounce_ms: 25,
        retry_ms: 100,
        ..WatchConfig::default()
    };
    let runtime_task_registry = Arc::new(RwLock::new(RuntimeTaskRegistry::new()));
    let validated_manifest_candidate_cache =
        Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
    let server = FriggMcpServer::new_with_runtime(
        config.clone(),
        RuntimeProfile::StdioAttached,
        true,
        Arc::clone(&runtime_task_registry),
        Arc::clone(&validated_manifest_candidate_cache),
    );
    let runtime = Arc::new(
        maybe_start_watch_runtime(
            &config,
            RuntimeTransportKind::Stdio,
            runtime_task_registry,
            validated_manifest_candidate_cache,
            None,
        )
        .expect("watch runtime should start")
        .expect("watch runtime should be enabled"),
    );
    server.set_watch_runtime(Some(Arc::clone(&runtime)));
    let second_session = server.clone_for_new_session();
    let workspace = server
        .known_workspaces()
        .into_iter()
        .next()
        .expect("startup roots should register globally known workspaces");

    assert!(
        !runtime
            .lease_status(&workspace.runtime_repository_id)
            .active
    );

    server
        .adopt_workspace(&workspace, true)
        .expect("first session should adopt workspace");
    assert_eq!(
        runtime
            .lease_status(&workspace.runtime_repository_id)
            .lease_count,
        1
    );

    second_session
        .adopt_workspace(&workspace, true)
        .expect("second session should share the same watch lease");
    assert_eq!(
        runtime
            .lease_status(&workspace.runtime_repository_id)
            .lease_count,
        2
    );

    server
        .detach_workspace(&workspace.repository_id)
        .expect("first session should detach workspace");
    assert_eq!(
        runtime
            .lease_status(&workspace.runtime_repository_id)
            .lease_count,
        1
    );

    second_session
        .detach_workspace(&workspace.repository_id)
        .expect("second session should detach workspace");
    assert_eq!(
        runtime
            .lease_status(&workspace.runtime_repository_id)
            .lease_count,
        0
    );
    assert!(
        !runtime
            .lease_status(&workspace.runtime_repository_id)
            .active
    );

    let _ = fs::remove_dir_all(workspace_root);
}

#[test]
fn workspace_attach_invalidates_validated_manifest_candidate_cache() {
    let workspace_root = authorized_temp_workspace_root("attach-invalidates-manifest-cache");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(workspace_root.join("src/lib.rs"), "pub struct Cached;\n")
        .expect("failed to write workspace root fixture");
    let root = workspace_root
        .canonicalize()
        .expect("workspace root should canonicalize");
    let source_path = root.join("src/lib.rs");
    let metadata = fs::metadata(&source_path).expect("source path should have metadata");

    let cache = Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
    cache
        .write()
        .expect("validated manifest candidate cache should not be poisoned")
        .store_validated(
            &root,
            "snapshot-001",
            &[FileMetadataDigest {
                path: source_path,
                size_bytes: metadata.len(),
                mtime_ns: None,
            }],
        );
    assert!(
        cache
            .read()
            .expect("validated manifest candidate cache should not be poisoned")
            .has_entry_for_root(&root)
    );

    let server = FriggMcpServer::new_with_runtime(
        FriggConfig::from_optional_workspace_roots(Vec::new())
            .expect("empty serving config should be valid"),
        RuntimeProfile::StdioEphemeral,
        false,
        Arc::new(RwLock::new(RuntimeTaskRegistry::new())),
        Arc::clone(&cache),
    );

    let _ = server
        .attach_workspace_internal(&root, true, WorkspaceResolveMode::Direct)
        .expect("workspace attach should succeed");

    assert!(
        !cache
            .read()
            .expect("validated manifest candidate cache should not be poisoned")
            .has_entry_for_root(&root)
    );

    let _ = fs::remove_dir_all(workspace_root);
}

#[test]
fn workspace_attach_invalidates_only_attached_repository_navigation_caches() {
    let workspace_root =
        authorized_temp_workspace_root("attach-invalidates-only-attached-navigation-caches");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(workspace_root.join("src/lib.rs"), "pub struct Cached;\n")
        .expect("failed to write workspace root fixture");
    let root = workspace_root
        .canonicalize()
        .expect("workspace root should canonicalize");

    let server = FriggMcpServer::new_with_runtime(
        FriggConfig::from_optional_workspace_roots(Vec::new())
            .expect("empty serving config should be valid"),
        RuntimeProfile::StdioEphemeral,
        false,
        Arc::new(RwLock::new(RuntimeTaskRegistry::new())),
        Arc::new(RwLock::new(ValidatedManifestCandidateCache::default())),
    );

    let attached_repository_id = crate::domain::model::stable_repository_id_for_root(&root).0;
    server.cache_heuristic_references(
        HeuristicReferenceCacheKey {
            repository_id: attached_repository_id.clone(),
            symbol_id: "symbol-001".to_owned(),
            corpus_signature: "corpus-001".to_owned(),
            scip_signature: "scip-001".to_owned(),
        },
        Vec::new(),
        0,
        0,
        0,
        0,
    );
    server.cache_heuristic_references(
        HeuristicReferenceCacheKey {
            repository_id: "repo-002".to_owned(),
            symbol_id: "symbol-002".to_owned(),
            corpus_signature: "corpus-002".to_owned(),
            scip_signature: "scip-002".to_owned(),
        },
        Vec::new(),
        0,
        0,
        0,
        0,
    );

    let _ = server
        .attach_workspace_internal(&root, true, WorkspaceResolveMode::Direct)
        .expect("workspace attach should succeed");

    assert_eq!(
        server
            .cache_state
            .heuristic_reference_cache
            .read()
            .expect("heuristic reference cache should not be poisoned")
            .len(),
        1
    );
    assert!(
        server
            .cache_state
            .heuristic_reference_cache
            .read()
            .expect("heuristic reference cache should not be poisoned")
            .keys()
            .all(|key| key.repository_id == "repo-002"),
        "heuristic reference cache should retain only unaffected repository entries"
    );
    assert_eq!(
        server
            .runtime_cache_telemetry(RuntimeCacheFamily::HeuristicReference)
            .invalidations,
        1
    );

    let _ = fs::remove_dir_all(workspace_root);
}

#[tokio::test(flavor = "current_thread")]
async fn watch_notify_invalidates_live_server_navigation_caches() {
    let workspace_root = temp_workspace_root("watch-invalidates-live-navigation-caches");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(
        workspace_root.join("src/lib.rs"),
        "pub struct WatchCache;\n",
    )
    .expect("failed to write workspace root fixture");

    let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
        .expect("runtime gate tests should build a valid FriggConfig");
    config.watch = WatchConfig {
        mode: WatchMode::On,
        debounce_ms: 25,
        retry_ms: 100,
        ..WatchConfig::default()
    };

    let declared_repository = config
        .repositories()
        .into_iter()
        .next()
        .expect("watch cache test should define one repository");
    let declared_root = PathBuf::from(&declared_repository.root_path);
    let db_path = crate::storage::ensure_provenance_db_parent_dir(&declared_root)
        .expect("manifest storage path should work");
    Storage::new(&db_path)
        .initialize()
        .expect("manifest storage should initialize");
    crate::indexer::index_repository_with_runtime_config(
        &declared_repository.repository_id.0,
        &declared_root,
        &db_path,
        IndexMode::ChangedOnly,
        &SemanticRuntimeConfig::default(),
        &SemanticRuntimeCredentials::default(),
    )
    .expect("baseline changed-only index should succeed");

    let runtime_task_registry = Arc::new(RwLock::new(RuntimeTaskRegistry::new()));
    let validated_manifest_candidate_cache =
        Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
    let server = FriggMcpServer::new_with_runtime(
        config.clone(),
        RuntimeProfile::StdioAttached,
        true,
        Arc::clone(&runtime_task_registry),
        Arc::clone(&validated_manifest_candidate_cache),
    );
    let runtime = maybe_start_watch_runtime(
        &config,
        RuntimeTransportKind::Stdio,
        runtime_task_registry,
        validated_manifest_candidate_cache,
        Some(server.repository_cache_invalidation_callback()),
    )
    .expect("watch runtime should start")
    .expect("watch runtime should be enabled");
    tokio::time::sleep(Duration::from_millis(250)).await;

    let workspace = server
        .known_workspaces()
        .into_iter()
        .next()
        .expect("runtime server should expose the startup workspace");
    runtime
        .acquire_lease(&workspace)
        .expect("runtime should start watching an adopted workspace");
    server.cache_heuristic_references(
        HeuristicReferenceCacheKey {
            repository_id: workspace.repository_id.clone(),
            symbol_id: "WatchCache".to_owned(),
            corpus_signature: "corpus-001".to_owned(),
            scip_signature: "scip-001".to_owned(),
        },
        Vec::new(),
        0,
        0,
        0,
        0,
    );

    assert_eq!(
        server
            .cache_state
            .heuristic_reference_cache
            .read()
            .expect("heuristic reference cache should not be poisoned")
            .len(),
        1
    );

    fs::write(
        workspace_root.join("src/watch_cache.rs"),
        "pub fn watch_cache_dirty_event() {}\n",
    )
    .expect("creating a new source file should trigger notify backend");

    assert!(
        wait_for_heuristic_reference_cache_eviction(&server, Duration::from_secs(5)).await,
        "watch notify should evict live repository-scoped navigation caches"
    );
    assert!(
        server
            .runtime_cache_telemetry(RuntimeCacheFamily::HeuristicReference)
            .invalidations
            >= 1
    );

    drop(runtime);
    tokio::time::sleep(Duration::from_millis(25)).await;
    let _ = fs::remove_dir_all(workspace_root);
}

#[tokio::test]
async fn read_match_reads_from_cached_result_handle() {
    let workspace_root = temp_workspace_root("read-match-result-handle");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(
        workspace_root.join("src/lib.rs"),
        "pub fn before_marker() {}\npub fn middle_marker() {}\npub fn unique_marker_alpha() {}\n",
    )
    .expect("failed to write source fixture");

    let config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
        .expect("runtime gate tests should build a valid FriggConfig");
    let declared_repository = config
        .repositories()
        .into_iter()
        .next()
        .expect("read_match result-handle test should define one repository");
    let declared_root = PathBuf::from(&declared_repository.root_path);
    let db_path = crate::storage::ensure_provenance_db_parent_dir(&declared_root)
        .expect("storage path should resolve");
    Storage::new(&db_path)
        .initialize()
        .expect("storage should initialize");
    crate::indexer::index_repository_with_runtime_config(
        &declared_repository.repository_id.0,
        &declared_root,
        &db_path,
        IndexMode::ChangedOnly,
        &SemanticRuntimeConfig::default(),
        &SemanticRuntimeCredentials::default(),
    )
    .expect("baseline changed-only index should succeed");

    let server = FriggMcpServer::new(config);
    let workspace = server
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");
    server
        .adopt_workspace(&workspace, true)
        .expect("server should adopt known workspace");

    let search = server
        .search_text_impl(crate::mcp::types::SearchTextParams {
            query: "unique_marker_alpha".to_owned(),
            pattern_type: None,
            repository_id: Some(workspace.repository_id.clone()),
            path_regex: None,
            limit: None,
            context_lines: None,
            case_sensitive: None,
            ignore_case: None,
            word: None,
            files_with_matches: None,
            count_only: None,
            glob: None,
            exclude_glob: None,
            include_hidden: None,
            max_count_per_file: None,
            collapse_by_file: None,
            response_mode: None,
            include_context_efficiency: None,
        })
        .await
        .expect("search_text should succeed")
        .0;
    let result_handle = search
        .result_handle
        .expect("search_text should return a result handle");
    let match_id = search
        .matches
        .first()
        .and_then(|found| found.match_id.clone())
        .expect("search_text match should carry a match id");

    let read_match = server
        .read_match_impl(crate::mcp::types::ReadMatchParams {
            result_handle: result_handle.clone(),
            match_id: match_id.clone(),
            before: Some(0),
            after: Some(10),
            presentation_mode: None,
            include_context_efficiency: None,
        })
        .await
        .expect("read_match should succeed");

    assert_eq!(read_match.line, 3);
    assert_eq!(read_match.start_line, 3);
    assert_eq!(read_match.end_line, 3);

    let _ = fs::remove_dir_all(workspace_root);
}
