// SPDX-License-Identifier: MIT

use std::collections::HashMap;
use std::sync::Mutex;

use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::{
    CodeAction, CodeActionParams, CodeActionProviderCapability, CodeActionResponse, CodeLens,
    CodeLensOptions, CodeLensParams, ColorInformation, ColorPresentation, ColorPresentationParams,
    ColorProviderCapability, CompletionOptions, CompletionParams, CompletionResponse, Diagnostic,
    DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
    DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams,
    DidOpenTextDocumentParams, DocumentColorParams, DocumentFormattingParams, DocumentLink,
    DocumentLinkOptions, DocumentLinkParams, DocumentOnTypeFormattingOptions,
    DocumentOnTypeFormattingParams, DocumentRangeFormattingParams, DocumentSymbolParams,
    DocumentSymbolResponse, FileSystemWatcher, FoldingRange, FoldingRangeParams, GlobPattern,
    GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, InitializeParams,
    InitializeResult, InitializedParams, Location, NumberOrString, OneOf, Position,
    PrepareRenameResponse, Range, ReferenceParams, Registration, RenameOptions, RenameParams,
    SelectionRange, SelectionRangeParams, SelectionRangeProviderCapability, SemanticTokens,
    SemanticTokensFullOptions, SemanticTokensOptions, SemanticTokensParams, SemanticTokensResult,
    SemanticTokensServerCapabilities, ServerCapabilities, TextDocumentSyncCapability,
    TextDocumentSyncKind, TextEdit, Url, WatchKind, WorkDoneProgressOptions, WorkspaceEdit,
};
use tower_lsp::{Client, LanguageServer};

use crate::document_store::DocumentStore;
use crate::parser;
use crate::schema::{self, ParseContext, SchemaCache, SchemaStoreCatalog};

/// YAML specification version for formatting and diagnostic purposes.
///
/// Note: the underlying parser (rlsp-yaml-parser) always parses as YAML 1.2
/// regardless of this setting.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum YamlVersion {
    /// YAML 1.1 (allows implicit boolean values such as `yes`/`no`).
    V1_1,
    /// YAML 1.2 (strict; recommended for Kubernetes and modern tooling).
    V1_2,
}

/// Workspace settings received via LSP initialization options or
/// `workspace/didChangeConfiguration`.
#[derive(Debug, Default, Clone, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Settings {
    /// Custom YAML tag names to treat as valid (suppresses unknown-tag diagnostics).
    pub custom_tags: Vec<String>,
    /// Whether to enforce alphabetical key ordering in mappings.
    pub key_ordering: bool,
    /// Maps schema URL → glob pattern (upstream yaml-language-server convention).
    pub schemas: HashMap<String, String>,
    /// Kubernetes cluster version for schema resolution (e.g. `"1.29.0"`).
    /// Defaults to `"master"` (tracks latest schemas) when absent.
    pub kubernetes_version: Option<String>,
    /// Enable `SchemaStore` automatic schema association. Defaults to `true` when absent.
    pub schema_store: Option<bool>,
    /// Maximum line width for the full-document formatter. Defaults to 80.
    pub format_print_width: Option<usize>,
    /// Prefer single-quoted strings. Defaults to false.
    pub format_single_quote: Option<bool>,
    /// Preserve the source quote style of scalars. Defaults to false.
    pub format_preserve_quotes: Option<bool>,
    /// HTTP proxy URL for schema fetching (e.g. `"http://proxy.corp:8080"`).
    /// When absent, no proxy is used.
    pub http_proxy: Option<String>,
    /// Enable color decorators (color picker) for color values. Defaults to `true` when absent.
    pub color_decorators: Option<bool>,
    /// Enable `format` keyword validation. Defaults to `true` when absent.
    pub format_validation: Option<bool>,
    /// YAML specification version for formatting and diagnostics (`"1.1"` or
    /// `"1.2"`). Defaults to `"1.2"` when absent.
    pub yaml_version: Option<String>,
    /// Enable diagnostic validation. Defaults to `true` when absent.
    pub validate: Option<bool>,
    /// Enable hover responses. Defaults to `true` when absent.
    pub hover: Option<bool>,
    /// Enable completion responses. Defaults to `true` when absent.
    pub completion: Option<bool>,
    /// Maximum number of items returned by document symbols and folding ranges.
    /// Defaults to 5000 when absent.
    pub max_items_computed: Option<usize>,
    /// Controls severity of flow-style diagnostics: `"off"` disables them,
    /// `"warning"` (default) emits warnings, `"error"` emits errors.
    pub flow_style: Option<String>,
    /// Controls severity of duplicate-key diagnostics: `"off"` disables them,
    /// `"warning"` emits warnings, `"error"` (default) emits errors.
    pub duplicate_keys: Option<String>,
    /// When `true`, the formatter converts all flow collections to block style.
    /// Defaults to `false` when absent.
    pub format_enforce_block_style: Option<bool>,
    /// When `true`, the formatter removes duplicate mapping keys before
    /// rendering, keeping the last occurrence. Defaults to `false` when absent.
    pub format_remove_duplicate_keys: Option<bool>,
    /// Add spaces inside flow braces: `{ a: 1 }` vs `{a: 1}`. Defaults to `true` when absent.
    pub format_bracket_spacing: Option<bool>,
    /// Indent block sequences that are values of mapping keys. Defaults to `true` when absent.
    pub format_indent_sequences: Option<bool>,
}

/// Default Kubernetes version used when `kubernetesVersion` is not configured.
const DEFAULT_KUBERNETES_VERSION: &str = "master";

// Lock acquisition order — always acquire in this sequence to prevent deadlock.
// Every handler that needs multiple locks must follow this order and must fully
// release each guard (let it drop) before acquiring the next:
//
//   1. document_store
//   2. schema_associations
//   3. schema_cache
//   4. diagnostics
//   5. settings
//
// schemastore_catalog is independent — it is never held concurrently with any
// of the above locks.
//
// No std::sync::Mutex guard may be held across an .await point. Extract the
// needed data as owned values, drop the guard, then call async operations.
/// The LSP server backend holding shared state for all document handlers.
pub struct Backend {
    client: Client,
    document_store: Mutex<DocumentStore>,
    /// Maps document URI to the schema URL associated with that document.
    schema_associations: Mutex<HashMap<Url, String>>,
    /// In-memory schema cache shared across all documents.
    schema_cache: Mutex<SchemaCache>,
    diagnostics: Mutex<HashMap<Url, Vec<Diagnostic>>>,
    settings: Mutex<Settings>,
    /// Lazily-fetched `SchemaStore` catalog, cached for the session.
    schemastore_catalog: Mutex<Option<SchemaStoreCatalog>>,
}

impl Backend {
    /// Create a new backend with the given LSP client handle.
    #[must_use]
    pub fn new(client: Client) -> Self {
        Self {
            client,
            document_store: Mutex::new(DocumentStore::new()),
            schema_associations: Mutex::new(HashMap::new()),
            schema_cache: Mutex::new(SchemaCache::new()),
            diagnostics: Mutex::new(HashMap::new()),
            settings: Mutex::new(Settings::default()),
            schemastore_catalog: Mutex::new(None),
        }
    }

    pub(crate) fn get_custom_tags(&self) -> Vec<String> {
        self.settings
            .lock()
            .ok()
            .map(|s| s.custom_tags.clone())
            .unwrap_or_default()
    }

    pub(crate) fn get_key_ordering(&self) -> bool {
        self.settings.lock().ok().is_some_and(|s| s.key_ordering)
    }

    pub(crate) fn get_schema_associations(&self) -> Vec<crate::schema::SchemaAssociation> {
        self.settings
            .lock()
            .ok()
            .map(|s| {
                s.schemas
                    .iter()
                    .map(|(url, pattern)| crate::schema::SchemaAssociation {
                        pattern: pattern.clone(),
                        url: url.clone(),
                    })
                    .collect()
            })
            .unwrap_or_default()
    }

    pub(crate) fn get_kubernetes_version(&self) -> String {
        self.settings
            .lock()
            .ok()
            .and_then(|s| s.kubernetes_version.clone())
            .unwrap_or_else(|| DEFAULT_KUBERNETES_VERSION.to_string())
    }

    /// Return `true` if `SchemaStore` automatic association is enabled.
    ///
    /// Defaults to `true` when `schemaStore` is absent from settings.
    pub(crate) fn get_schema_store_enabled(&self) -> bool {
        self.settings
            .lock()
            .ok()
            .is_none_or(|s| s.schema_store.unwrap_or(true))
    }

    /// Return the configured HTTP proxy URL, or `None` if not set.
    pub(crate) fn get_http_proxy(&self) -> Option<String> {
        self.settings.lock().ok().and_then(|s| s.http_proxy.clone())
    }

    /// Return `true` if `format` keyword validation is enabled.
    ///
    /// Defaults to `true` when `formatValidation` is absent from settings.
    pub(crate) fn get_format_validation(&self) -> bool {
        self.settings
            .lock()
            .ok()
            .is_none_or(|s| s.format_validation.unwrap_or(true))
    }

    /// Return the YAML version to use for `text`.
    ///
    /// Priority: modeline (`$yamlVersion=`) > workspace setting > default (1.2).
    /// Invalid version strings (neither `"1.1"` nor `"1.2"`) are treated as
    /// absent — the next fallback in the chain is used.
    pub(crate) fn get_yaml_version(&self, text: &str) -> YamlVersion {
        let parse_version = |s: &str| {
            if s == "1.1" {
                YamlVersion::V1_1
            } else {
                YamlVersion::V1_2
            }
        };
        if let Some(v) = schema::extract_yaml_version(text) {
            return parse_version(&v);
        }
        self.settings
            .lock()
            .ok()
            .and_then(|s| s.yaml_version.clone())
            .map_or(YamlVersion::V1_2, |v| parse_version(&v))
    }

    /// Return the cached `SchemaStore` catalog, fetching it on first call.
    ///
    /// On fetch failure, logs a warning and returns `None` — `SchemaStore`
    /// is non-fatal: the server continues without it.
    ///
    /// Pattern: check cache under lock → drop lock → fetch if miss →
    /// re-acquire lock → insert. No lock is held across the blocking fetch.
    async fn get_or_fetch_schemastore_catalog(&self) -> Option<SchemaStoreCatalog> {
        // Check cache without holding the lock across spawn_blocking.
        let cached = self
            .schemastore_catalog
            .lock()
            .ok()
            .and_then(|guard| guard.clone());

        if cached.is_some() {
            return cached;
        }

        // Cache miss — fetch without holding any lock.
        let proxy = self.get_http_proxy();
        let fetched = tokio::task::spawn_blocking(move || {
            crate::schema::fetch_schemastore_catalog(proxy.as_deref())
        })
        .await;

        match fetched {
            Ok(Ok(catalog)) => {
                if let Ok(mut guard) = self.schemastore_catalog.lock() {
                    *guard = Some(catalog.clone());
                }
                Some(catalog)
            }
            Ok(Err(e)) => {
                self.client
                    .log_message(
                        tower_lsp::lsp_types::MessageType::WARNING,
                        format!("SchemaStore catalog fetch failed: {e}"),
                    )
                    .await;
                None
            }
            Err(_) => None,
        }
    }

    /// Return the raw text of the document at `uri`, or `None` if not open.
    pub fn get_document_text(&self, uri: &str) -> Option<String> {
        let parsed = Url::parse(uri).ok()?;
        let store = self.document_store.lock().ok()?;
        store.get(&parsed).map(str::to_string)
    }

    /// Return the most recently published diagnostics for `uri`, or `None`.
    pub fn get_diagnostics(&self, uri: &str) -> Option<Vec<Diagnostic>> {
        let parsed = Url::parse(uri).ok()?;
        let diags = self.diagnostics.lock().ok()?;
        diags.get(&parsed).cloned()
    }

    /// Pre-populate the schema cache so integration tests can exercise schema
    /// validation without a network fetch.
    ///
    /// Use a `$schema=<schema_url>` modeline in the test document so that
    /// `process_schema` picks up the URL, normalises it, and finds the schema
    /// already in the cache — skipping the network call entirely.
    /// `schema_url` must be a valid `https://` URL (the same value used in the
    /// modeline), since `validate_and_normalize_url` only permits http/https.
    #[doc(hidden)]
    pub fn seed_schema_cache(&self, schema_url: &str, schema: crate::schema::JsonSchema) {
        if let Ok(mut cache) = self.schema_cache.lock() {
            cache.insert(schema_url.to_string(), serde_json::Value::Null, schema);
        }
    }

    /// Normalize `schema_url`, record the association, fetch/cache the schema,
    /// and append schema-validation diagnostics to `diagnostics`.
    ///
    /// No Mutex guard is held across any `.await` point.
    async fn process_schema(
        &self,
        uri: &Url,
        schema_url: &str,
        diagnostics: &mut Vec<Diagnostic>,
        documents: &[rlsp_yaml_parser::node::Document<rlsp_yaml_parser::Span>],
        yaml_version: YamlVersion,
    ) {
        let normalised = crate::schema::validate_and_normalize_url(schema_url).ok();

        if let Some(url) = normalised {
            // Record the association (lock → insert → drop).
            if let Ok(mut assoc) = self.schema_associations.lock() {
                assoc.insert(uri.clone(), url.clone());
            }

            // Check cache without holding the lock across spawn_blocking.
            let cached = self
                .schema_cache
                .lock()
                .ok()
                .and_then(|cache| cache.get(&url).cloned());

            let schema = if let Some(s) = cached {
                // Already resolved when first fetched — no further ref resolution needed.
                Some(s)
            } else {
                // Take the cache out of the Mutex so it can be moved into
                // spawn_blocking (which requires 'static).  Replace with an
                // empty cache, then put the enriched cache back after the fetch.
                let mut taken_cache = self
                    .schema_cache
                    .lock()
                    .ok()
                    .map(|mut g| std::mem::take(&mut *g))
                    .unwrap_or_default();

                let url_clone = url.clone();
                let proxy = self.get_http_proxy();
                let join_result = tokio::task::spawn_blocking(move || {
                    let mut ctx = ParseContext::new(&mut taken_cache, proxy.as_deref());
                    let result = crate::schema::fetch_schema_raw(
                        &url_clone,
                        proxy.as_deref(),
                        Some(&mut ctx),
                    );
                    (result, taken_cache)
                })
                .await;

                let Ok((fetch_result, returned_cache)) = join_result else {
                    return;
                };

                // Merge the returned cache back into the live cache.  Use insert()
                // for each entry so the first-write-wins invariant is preserved:
                // any URL inserted by a concurrent process_schema call during the
                // spawn_blocking window is not overwritten.
                if let Ok(mut guard) = self.schema_cache.lock() {
                    for (key, (value, schema)) in returned_cache.into_inner() {
                        guard.insert(key, value, schema);
                    }
                }

                let fetched: Option<(serde_json::Value, crate::schema::JsonSchema)> =
                    fetch_result.ok();

                if let Some((ref v, ref s)) = fetched
                    && let Ok(mut cache) = self.schema_cache.lock()
                {
                    cache.insert(url, v.clone(), s.clone());
                }
                fetched.map(|(_, s)| s)
            };

            if let Some(s) = schema {
                let format_validation = self.get_format_validation();
                diagnostics.extend(crate::schema_validation::validate_schema(
                    documents,
                    &s,
                    format_validation,
                    yaml_version,
                ));
            }
        }
    }

    /// Route schema validation for a document, trying modeline → workspace
    /// associations → Kubernetes auto-detection → `SchemaStore` in that order.
    ///
    /// Lock ordering: `schema_associations` → `schema_cache` (`document_store`
    /// is not held here; diagnostics is accumulated by the caller).
    /// No Mutex guard is held across any `.await` point.
    async fn run_schema_validation(
        &self,
        uri: &Url,
        text: &str,
        documents: &[rlsp_yaml_parser::node::Document<rlsp_yaml_parser::Span>],
        diagnostics: &mut Vec<Diagnostic>,
        yaml_version: YamlVersion,
    ) {
        if let Some(schema_url) = crate::schema::extract_schema_url(text) {
            if schema_url.eq_ignore_ascii_case("none") {
                // $schema=none disables schema processing for this document.
                // Clear any previous association so stale schema info is not
                // carried over from a prior save.
                if let Ok(mut assoc) = self.schema_associations.lock() {
                    assoc.remove(uri);
                }
                // Fall through: non-schema validators already ran above.
            } else {
                self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
                    .await;
            }
            return;
        }
        // No modeline — check workspace associations, then auto-detection.
        // get_schema_associations(), get_kubernetes_version(), and
        // get_schema_store_enabled() each acquire and release the settings
        // lock before any schema_associations or schema_cache lock is taken.
        let associations = self.get_schema_associations();
        let k8s_version = self.get_kubernetes_version();
        let schema_store_enabled = self.get_schema_store_enabled();
        let filename = uri.path();
        if let Some(schema_url) = crate::schema::match_schema_by_filename(filename, &associations) {
            self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
                .await;
        } else if let Some((api_version, kind)) =
            crate::schema::detect_kubernetes_resource(documents)
        {
            // Third fallback: Kubernetes auto-detection.
            let schema_url =
                crate::schema::kubernetes_schema_url(&api_version, &kind, &k8s_version);
            self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
                .await;
        } else if schema_store_enabled {
            // Fourth fallback: SchemaStore catalog.
            // get_or_fetch_schemastore_catalog() acquires and releases
            // schemastore_catalog lock without holding any other lock.
            if let Some(catalog) = self.get_or_fetch_schemastore_catalog().await {
                if let Some(schema_url) = crate::schema::match_schemastore(filename, &catalog) {
                    self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
                        .await;
                }
            }
        }
    }

    async fn parse_and_publish(&self, uri: Url, text: &str) {
        if !self.get_validate() {
            if let Ok(mut diags) = self.diagnostics.lock() {
                diags.insert(uri.clone(), Vec::new());
            }
            self.client.publish_diagnostics(uri, Vec::new(), None).await;
            return;
        }

        let result = parser::parse_yaml(text);
        let mut diagnostics = result.diagnostics.clone();

        // Run validators and combine diagnostics
        diagnostics.extend(crate::validation::validators::validate_unused_anchors(
            &result.documents,
        ));
        let validation_settings = {
            let settings = self.settings.lock().ok();
            settings
                .as_ref()
                .map_or_else(crate::validation::ValidationSettings::default, |s| {
                    crate::validation::ValidationSettings::from_settings(s)
                })
        };
        diagnostics.extend(crate::validation::validators::validate_flow_style(
            &result.documents,
            &validation_settings,
        ));
        diagnostics.extend(crate::validation::validators::validate_duplicate_keys(
            &result.documents,
            &validation_settings,
        ));

        // Custom tag validation: merge workspace settings tags with per-document modeline tags.
        // get_custom_tags() and get_key_ordering() acquire and release the settings lock before
        // any other lock below.
        let key_ordering = self.get_key_ordering();
        if key_ordering {
            diagnostics.extend(crate::validation::validators::validate_key_ordering(
                &result.documents,
            ));
        }

        // Parse settings tags and modeline tags into CustomTag entries.
        // Modeline wins on name collision: insert settings first, then let modeline overwrite.
        let mut tag_map: std::collections::HashMap<
            String,
            crate::validation::validators::CustomTag,
        > = self
            .get_custom_tags()
            .into_iter()
            .map(|s| {
                let ct = crate::validation::validators::parse_custom_tag(&s);
                (ct.name.clone(), ct)
            })
            .collect();
        for s in crate::schema::extract_custom_tags(text) {
            let ct = crate::validation::validators::parse_custom_tag(&s);
            tag_map.insert(ct.name.clone(), ct);
        }
        let allowed_tags: Vec<crate::validation::validators::CustomTag> =
            tag_map.into_values().collect();
        diagnostics.extend(crate::validation::validators::validate_custom_tags(
            &result.documents,
            &allowed_tags,
        ));

        // YAML 1.1 compatibility: only warn when the document is being treated
        // as YAML 1.2 (the default). Users who opt into 1.1 semantics via
        // `yamlVersion: "1.1"` or the `# $yamlVersion=1.1` modeline already
        // expect 1.1 behaviour and should not receive these diagnostics.
        let yaml_version = self.get_yaml_version(text);
        if yaml_version == YamlVersion::V1_2 {
            diagnostics.extend(crate::validation::validators::validate_yaml11_compat(
                &result.documents,
            ));
        }

        // Schema validation — see run_schema_validation for routing logic.
        self.run_schema_validation(
            &uri,
            text,
            &result.documents,
            &mut diagnostics,
            yaml_version,
        )
        .await;

        // Filter out suppressed diagnostics before storing and publishing.
        let suppressions = crate::validation::suppression::build_suppression_map(text);
        diagnostics.retain(|diag| {
            let code = diag.code.as_ref().and_then(|c| match c {
                NumberOrString::String(s) => Some(s.as_str()),
                NumberOrString::Number(_) => None,
            });
            !suppressions.is_suppressed(diag.range.start.line, code.unwrap_or(""))
        });

        if let Ok(mut diags) = self.diagnostics.lock() {
            diags.insert(uri.clone(), diagnostics.clone());
        }

        self.client
            .publish_diagnostics(uri, diagnostics, None)
            .await;
    }

    /// Return the static server capabilities advertised during initialization.
    #[must_use]
    pub fn capabilities() -> ServerCapabilities {
        ServerCapabilities {
            text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
            hover_provider: Some(tower_lsp::lsp_types::HoverProviderCapability::Simple(true)),
            document_symbol_provider: Some(OneOf::Left(true)),
            completion_provider: Some(CompletionOptions {
                resolve_provider: Some(false),
                ..CompletionOptions::default()
            }),
            definition_provider: Some(OneOf::Left(true)),
            references_provider: Some(OneOf::Left(true)),
            folding_range_provider: Some(
                tower_lsp::lsp_types::FoldingRangeProviderCapability::Simple(true),
            ),
            rename_provider: Some(OneOf::Right(RenameOptions {
                prepare_provider: Some(true),
                work_done_progress_options: WorkDoneProgressOptions::default(),
            })),
            document_link_provider: Some(DocumentLinkOptions {
                resolve_provider: Some(false),
                work_done_progress_options: WorkDoneProgressOptions::default(),
            }),
            selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
            code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
            code_lens_provider: Some(CodeLensOptions {
                resolve_provider: Some(false),
            }),
            document_formatting_provider: Some(OneOf::Left(true)),
            document_range_formatting_provider: Some(OneOf::Left(true)),
            document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
                first_trigger_character: "\n".to_string(),
                more_trigger_character: None,
            }),
            semantic_tokens_provider: Some(
                SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
                    legend: crate::analysis::semantic_tokens::legend(),
                    full: Some(SemanticTokensFullOptions::Bool(true)),
                    ..SemanticTokensOptions::default()
                }),
            ),
            color_provider: Some(ColorProviderCapability::Simple(true)),
            ..ServerCapabilities::default()
        }
    }

    /// Return `true` if color decorators are enabled (default: `true`).
    pub(crate) fn get_color_decorators_enabled(&self) -> bool {
        self.settings
            .lock()
            .ok()
            .is_none_or(|s| s.color_decorators.unwrap_or(true))
    }

    /// Return `true` if diagnostic validation is enabled (default: `true`).
    pub(crate) fn get_validate(&self) -> bool {
        self.settings
            .lock()
            .ok()
            .is_none_or(|s| s.validate.unwrap_or(true))
    }

    /// Return `true` if hover responses are enabled (default: `true`).
    pub(crate) fn get_hover_enabled(&self) -> bool {
        self.settings
            .lock()
            .ok()
            .is_none_or(|s| s.hover.unwrap_or(true))
    }

    /// Return `true` if completion responses are enabled (default: `true`).
    pub(crate) fn get_completion_enabled(&self) -> bool {
        self.settings
            .lock()
            .ok()
            .is_none_or(|s| s.completion.unwrap_or(true))
    }

    /// Return the maximum number of items computed for document symbols and
    /// folding ranges. Defaults to 5000 when absent.
    pub(crate) fn get_max_items_computed(&self) -> usize {
        self.settings
            .lock()
            .ok()
            .and_then(|s| s.max_items_computed)
            .unwrap_or(5000)
    }
}

#[tower_lsp::async_trait]
impl LanguageServer for Backend {
    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
        let settings = params
            .initialization_options
            .and_then(|v| serde_json::from_value::<Settings>(v).ok())
            .unwrap_or_default();
        if let Ok(mut s) = self.settings.lock() {
            *s = settings;
        }
        Ok(InitializeResult {
            capabilities: Self::capabilities(),
            ..InitializeResult::default()
        })
    }

    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
        if let Ok(settings) = serde_json::from_value::<Settings>(params.settings)
            && let Ok(mut s) = self.settings.lock()
        {
            *s = settings;
        }
    }

    async fn initialized(&self, _: InitializedParams) {
        let watchers = vec![
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/*.yaml".to_string()),
                kind: Some(WatchKind::all()),
            },
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/*.yml".to_string()),
                kind: Some(WatchKind::all()),
            },
        ];
        let registration = Registration {
            id: "yaml-file-watcher".to_string(),
            method: "workspace/didChangeWatchedFiles".to_string(),
            register_options: serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
                watchers,
            })
            .ok(),
        };
        let client = self.client.clone();
        tokio::spawn(async move {
            let _ = client.register_capability(vec![registration]).await;
        });
    }

    async fn shutdown(&self) -> Result<()> {
        Ok(())
    }

    async fn did_change_watched_files(&self, _params: DidChangeWatchedFilesParams) {
        // Collect open documents, releasing the lock before any async work.
        let docs: Vec<(Url, String)> = if let Ok(store) = self.document_store.lock() {
            store.all_documents()
        } else {
            return;
        };
        for (uri, text) in docs {
            self.parse_and_publish(uri, &text).await;
        }
    }

    async fn did_open(&self, params: DidOpenTextDocumentParams) {
        let uri = params.text_document.uri;
        let text = params.text_document.text;

        if let Ok(mut store) = self.document_store.lock() {
            store.open(uri.clone(), text.clone());
        }

        self.parse_and_publish(uri, &text).await;
    }

    async fn did_change(&self, params: DidChangeTextDocumentParams) {
        let uri = params.text_document.uri;
        if let Some(change) = params.content_changes.into_iter().last() {
            if let Ok(mut store) = self.document_store.lock() {
                store.change(&uri, change.text.clone());
            }

            self.parse_and_publish(uri, &change.text).await;
        }
    }

    async fn did_close(&self, params: DidCloseTextDocumentParams) {
        let uri = params.text_document.uri;

        if let Ok(mut store) = self.document_store.lock() {
            store.close(&uri);
        }

        if let Ok(mut diags) = self.diagnostics.lock() {
            diags.remove(&uri);
        }

        self.client.publish_diagnostics(uri, Vec::new(), None).await;
    }

    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
        if !self.get_hover_enabled() {
            return Ok(None);
        }

        let uri = params.text_document_position_params.text_document.uri;
        let position = params.text_document_position_params.position;

        let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
            let text_present = store.get(&uri).is_some();
            let docs = store.get_documents(&uri).cloned();
            (docs, text_present)
        } else {
            return Ok(None);
        };

        if !text_present {
            return Ok(None);
        }

        let Some(docs) = docs else {
            return Ok(None);
        };

        // Lock ordering: document_store → schema_associations → schema_cache
        let schema_url = self
            .schema_associations
            .lock()
            .ok()
            .and_then(|assoc| assoc.get(&uri).cloned());

        let schema = schema_url.and_then(|url| {
            self.schema_cache
                .lock()
                .ok()
                .and_then(|cache| cache.get(&url).cloned())
        });

        Ok(crate::hover::hover_at(&docs, position, schema.as_ref()))
    }

    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
        if !self.get_completion_enabled() {
            return Ok(None);
        }

        let uri = params.text_document_position.text_document.uri;
        let position = params.text_document_position.position;

        // Lock ordering: document_store → schema_associations → schema_cache
        let docs = if let Ok(store) = self.document_store.lock() {
            let text = store.get(&uri);
            if text.is_none() {
                return Ok(None);
            }
            store.get_documents(&uri).cloned()
        } else {
            return Ok(None);
        };

        // Retrieve the schema URL for this document (if any).
        let schema_url = self
            .schema_associations
            .lock()
            .ok()
            .and_then(|assoc| assoc.get(&uri).cloned());

        // Retrieve the cached schema (if any) — no lock held across await.
        let schema = schema_url.and_then(|url| {
            self.schema_cache
                .lock()
                .ok()
                .and_then(|cache| cache.get(&url).cloned())
        });

        let items = crate::completion::complete_at(
            docs.as_deref().unwrap_or(&[]),
            position,
            schema.as_ref(),
        );
        if items.is_empty() {
            return Ok(None);
        }

        Ok(Some(CompletionResponse::Array(items)))
    }

    async fn goto_definition(
        &self,
        params: GotoDefinitionParams,
    ) -> Result<Option<GotoDefinitionResponse>> {
        let uri = params.text_document_position_params.text_document.uri;
        let position = params.text_document_position_params.position;

        let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
            let text_present = store.get(&uri).is_some();
            let docs = store.get_documents(&uri).cloned();
            (docs, text_present)
        } else {
            return Ok(None);
        };

        if !text_present {
            return Ok(None);
        }

        let Some(docs) = docs else {
            return Ok(None);
        };

        let location = crate::navigation::references::goto_definition(&docs, &uri, position);
        Ok(location.map(GotoDefinitionResponse::Scalar))
    }

    async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
        let uri = params.text_document_position.text_document.uri;
        let position = params.text_document_position.position;
        let include_declaration = params.context.include_declaration;

        let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
            let text_present = store.get(&uri).is_some();
            let docs = store.get_documents(&uri).cloned();
            (docs, text_present)
        } else {
            return Ok(None);
        };

        if !text_present {
            return Ok(None);
        }

        let Some(docs) = docs else {
            return Ok(None);
        };

        let locations = crate::navigation::references::find_references(
            &docs,
            &uri,
            position,
            include_declaration,
        );
        if locations.is_empty() {
            return Ok(None);
        }

        Ok(Some(locations))
    }

    async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
        let uri = params.text_document.uri;

        let (text, docs) = if let Ok(store) = self.document_store.lock() {
            let text = store.get(&uri).map(str::to_string);
            let docs = store.get_documents(&uri).cloned();
            (text, docs)
        } else {
            return Ok(None);
        };

        let Some(text) = text else {
            return Ok(None);
        };

        let mut ranges =
            crate::analysis::folding::folding_ranges(docs.as_deref().unwrap_or(&[]), &text);
        let limit = self.get_max_items_computed();
        ranges.truncate(limit);

        if ranges.is_empty() {
            return Ok(None);
        }

        Ok(Some(ranges))
    }

    async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
        let uri = params.text_document.uri;

        let docs = if let Ok(store) = self.document_store.lock() {
            if store.get(&uri).is_none() {
                return Ok(None);
            }
            store.get_documents(&uri).cloned()
        } else {
            return Ok(None);
        };

        let links = crate::decorators::document_links::find_document_links(
            docs.as_deref().unwrap_or(&[]),
            Some(&uri),
        );
        if links.is_empty() {
            return Ok(None);
        }

        Ok(Some(links))
    }

    async fn selection_range(
        &self,
        params: SelectionRangeParams,
    ) -> Result<Option<Vec<SelectionRange>>> {
        let uri = params.text_document.uri;

        let docs = if let Ok(store) = self.document_store.lock() {
            if store.get(&uri).is_none() {
                return Ok(None);
            }
            store.get_documents(&uri).cloned()
        } else {
            return Ok(None);
        };

        let result = crate::analysis::selection::selection_ranges(
            docs.as_deref().unwrap_or(&[]),
            &params.positions,
        );
        if result.is_empty() {
            return Ok(None);
        }

        Ok(Some(result))
    }

    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
        let uri = params.text_document.uri;
        let range = params.range;

        let text = if let Ok(store) = self.document_store.lock() {
            store.get(&uri).map(str::to_string)
        } else {
            return Ok(None);
        };

        let Some(text) = text else {
            return Ok(None);
        };

        let diagnostics = self.get_diagnostics(uri.as_str()).unwrap_or_default();

        let yaml_version = self.get_yaml_version(&text);
        let settings = self.settings.lock().ok();
        let format_options = crate::editing::formatter::YamlFormatOptions {
            print_width: settings
                .as_ref()
                .and_then(|s| s.format_print_width)
                .unwrap_or(80),
            tab_width: 2,
            single_quote: settings
                .as_ref()
                .and_then(|s| s.format_single_quote)
                .unwrap_or(false),
            preserve_quotes: settings
                .as_ref()
                .and_then(|s| s.format_preserve_quotes)
                .unwrap_or(false),
            bracket_spacing: settings
                .as_ref()
                .and_then(|s| s.format_bracket_spacing)
                .unwrap_or(true),
            yaml_version,
            format_enforce_block_style: settings
                .as_ref()
                .and_then(|s| s.format_enforce_block_style)
                .unwrap_or(false),
            format_remove_duplicate_keys: settings
                .as_ref()
                .and_then(|s| s.format_remove_duplicate_keys)
                .unwrap_or(false),
            format_indent_sequences: settings
                .as_ref()
                .and_then(|s| s.format_indent_sequences)
                .unwrap_or(true),
        };
        drop(settings);

        let docs = crate::parser::parse_yaml(&text).documents;
        let actions = crate::editing::code_actions::code_actions(
            &docs,
            &text,
            range,
            &diagnostics,
            &uri,
            &format_options,
        );
        if actions.is_empty() {
            return Ok(None);
        }

        Ok(Some(actions.into_iter().map(CodeAction::into).collect()))
    }

    async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
        let uri = params.text_document.uri;

        // Lock ordering: schema_associations → schema_cache
        let schema_url = self
            .schema_associations
            .lock()
            .ok()
            .and_then(|assoc| assoc.get(&uri).cloned());

        let Some(url) = schema_url else {
            return Ok(None);
        };

        let schema = self
            .schema_cache
            .lock()
            .ok()
            .and_then(|cache| cache.get(&url).cloned());

        let lenses = crate::decorators::code_lens::code_lenses(&url, schema.as_ref());
        if lenses.is_empty() {
            return Ok(None);
        }

        Ok(Some(lenses))
    }

    async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
        let uri = params.text_document.uri;

        let text = if let Ok(store) = self.document_store.lock() {
            store.get(&uri).map(str::to_string)
        } else {
            return Ok(None);
        };

        let Some(text) = text else {
            return Ok(None);
        };

        // Tab settings come from LSP params (editor override); other settings from workspace.
        let tab_size = params.options.tab_size as usize;
        let yaml_version = self.get_yaml_version(&text);
        let settings = self.settings.lock().ok();
        let options = crate::editing::formatter::YamlFormatOptions {
            print_width: settings
                .as_ref()
                .and_then(|s| s.format_print_width)
                .unwrap_or(80),
            tab_width: tab_size,
            single_quote: settings
                .as_ref()
                .and_then(|s| s.format_single_quote)
                .unwrap_or(false),
            preserve_quotes: settings
                .as_ref()
                .and_then(|s| s.format_preserve_quotes)
                .unwrap_or(false),
            bracket_spacing: settings
                .as_ref()
                .and_then(|s| s.format_bracket_spacing)
                .unwrap_or(true),
            yaml_version,
            format_enforce_block_style: settings
                .as_ref()
                .and_then(|s| s.format_enforce_block_style)
                .unwrap_or(false),
            format_remove_duplicate_keys: settings
                .as_ref()
                .and_then(|s| s.format_remove_duplicate_keys)
                .unwrap_or(false),
            format_indent_sequences: settings
                .as_ref()
                .and_then(|s| s.format_indent_sequences)
                .unwrap_or(true),
        };
        drop(settings);

        let formatted = crate::editing::formatter::format_yaml(&text, &options);

        // No changes — return None.
        if formatted == text {
            return Ok(None);
        }

        // Replace the entire document with a single TextEdit spanning from (0,0)
        // to the end of the last line. For documents ending with a newline the
        // end position is (line_count, 0); otherwise it is (last_line_idx, last_col).
        let lines: Vec<&str> = text.lines().collect();
        let end = if text.ends_with('\n') {
            Position {
                line: u32::try_from(lines.len()).unwrap_or(u32::MAX),
                character: 0,
            }
        } else {
            let last_col = lines.last().map_or(0, |l| l.len());
            Position {
                line: u32::try_from(lines.len().saturating_sub(1)).unwrap_or(u32::MAX),
                character: u32::try_from(last_col).unwrap_or(u32::MAX),
            }
        };

        Ok(Some(vec![TextEdit {
            range: Range {
                start: Position {
                    line: 0,
                    character: 0,
                },
                end,
            },
            new_text: formatted,
        }]))
    }

    async fn range_formatting(
        &self,
        params: DocumentRangeFormattingParams,
    ) -> Result<Option<Vec<TextEdit>>> {
        let uri = params.text_document.uri;
        let requested = params.range;

        let text = if let Ok(store) = self.document_store.lock() {
            store.get(&uri).map(str::to_string)
        } else {
            return Ok(None);
        };

        let Some(text) = text else {
            return Ok(None);
        };

        let tab_size = params.options.tab_size as usize;
        let yaml_version = self.get_yaml_version(&text);
        let settings = self.settings.lock().ok();
        let options = crate::editing::formatter::YamlFormatOptions {
            print_width: settings
                .as_ref()
                .and_then(|s| s.format_print_width)
                .unwrap_or(80),
            tab_width: tab_size,
            single_quote: settings
                .as_ref()
                .and_then(|s| s.format_single_quote)
                .unwrap_or(false),
            preserve_quotes: settings
                .as_ref()
                .and_then(|s| s.format_preserve_quotes)
                .unwrap_or(false),
            bracket_spacing: settings
                .as_ref()
                .and_then(|s| s.format_bracket_spacing)
                .unwrap_or(true),
            yaml_version,
            format_enforce_block_style: settings
                .as_ref()
                .and_then(|s| s.format_enforce_block_style)
                .unwrap_or(false),
            format_remove_duplicate_keys: settings
                .as_ref()
                .and_then(|s| s.format_remove_duplicate_keys)
                .unwrap_or(false),
            format_indent_sequences: settings
                .as_ref()
                .and_then(|s| s.format_indent_sequences)
                .unwrap_or(true),
        };
        drop(settings);

        let formatted = crate::editing::formatter::format_yaml(&text, &options);

        // Extract the lines within the requested range from both the original
        // and the formatted output. The range end is exclusive at character 0
        // of the next line when the editor selects whole lines.
        let orig_lines: Vec<&str> = text.lines().collect();
        let fmt_lines: Vec<&str> = formatted.lines().collect();

        let start_line = requested.start.line as usize;
        let end_line = requested.end.line as usize;

        // Clamp to the actual number of lines in both versions.
        let orig_end = end_line.min(orig_lines.len().saturating_sub(1));
        let fmt_end = end_line.min(fmt_lines.len().saturating_sub(1));

        // Collect the range slice from each version for comparison.
        let orig_slice = orig_lines
            .get(start_line..=orig_end)
            .unwrap_or_default()
            .join("\n");
        let fmt_slice = fmt_lines
            .get(start_line..=fmt_end)
            .unwrap_or_default()
            .join("\n");

        // No change in the requested range — nothing to do.
        if orig_slice == fmt_slice {
            return Ok(None);
        }

        // Build the replacement text. Append a trailing newline so the edit
        // replaces whole lines (including the terminator of the last line).
        let new_text = format!("{fmt_slice}\n");

        // The edit range spans from the start of start_line to the start of
        // the line after end_line (i.e. character 0 of end_line + 1), which
        // replaces the lines in their entirety.
        let edit_start = Position {
            line: u32::try_from(start_line).unwrap_or(u32::MAX),
            character: 0,
        };
        let edit_end = Position {
            line: u32::try_from(end_line + 1).unwrap_or(u32::MAX),
            character: 0,
        };

        Ok(Some(vec![TextEdit {
            range: Range {
                start: edit_start,
                end: edit_end,
            },
            new_text,
        }]))
    }

    async fn on_type_formatting(
        &self,
        params: DocumentOnTypeFormattingParams,
    ) -> Result<Option<Vec<TextEdit>>> {
        let uri = params.text_document_position.text_document.uri;
        let position = params.text_document_position.position;
        let ch = &params.ch;
        let tab_size = params.options.tab_size;

        let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
            let text_present = store.get(&uri).is_some();
            let docs = store.get_documents(&uri).cloned();
            (docs, text_present)
        } else {
            return Ok(None);
        };

        if !text_present {
            return Ok(None);
        }

        let edits = crate::editing::on_type_formatting::format_on_type(
            docs.as_deref().unwrap_or(&[]),
            position,
            ch,
            tab_size,
        );
        if edits.is_empty() {
            return Ok(None);
        }

        Ok(Some(edits))
    }

    async fn semantic_tokens_full(
        &self,
        params: SemanticTokensParams,
    ) -> Result<Option<SemanticTokensResult>> {
        let uri = params.text_document.uri;
        let (text, docs) = if let Ok(store) = self.document_store.lock() {
            let text = store.get(&uri).map(str::to_string);
            let docs = store.get_documents(&uri).cloned();
            (text, docs)
        } else {
            return Ok(None);
        };
        let Some(text) = text else {
            return Ok(None);
        };
        let tokens = crate::analysis::semantic_tokens::semantic_tokens(
            docs.as_deref().unwrap_or(&[]),
            &text,
        );
        if tokens.is_empty() {
            return Ok(None);
        }
        Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
            result_id: None,
            data: tokens,
        })))
    }

    async fn document_symbol(
        &self,
        params: DocumentSymbolParams,
    ) -> Result<Option<DocumentSymbolResponse>> {
        let uri = params.text_document.uri;

        let (text, docs) = if let Ok(store) = self.document_store.lock() {
            let text = store.get(&uri).map(str::to_string);
            let docs = store.get_documents(&uri).cloned();
            (text, docs)
        } else {
            return Ok(None);
        };

        let Some(_text) = text else {
            return Ok(None);
        };

        let mut symbols =
            crate::analysis::symbols::document_symbols(docs.as_deref().unwrap_or(&[]));
        let limit = self.get_max_items_computed();
        symbols.truncate(limit);

        if symbols.is_empty() {
            return Ok(None);
        }

        Ok(Some(DocumentSymbolResponse::Nested(symbols)))
    }

    async fn prepare_rename(
        &self,
        params: tower_lsp::lsp_types::TextDocumentPositionParams,
    ) -> Result<Option<PrepareRenameResponse>> {
        let uri = params.text_document.uri;
        let position = params.position;

        let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
            let text_present = store.get(&uri).is_some();
            let docs = store.get_documents(&uri).cloned();
            (docs, text_present)
        } else {
            return Ok(None);
        };

        if !text_present {
            return Ok(None);
        }

        let Some(docs) = docs else {
            return Ok(None);
        };

        let range = crate::navigation::rename::prepare_rename(&docs, position);
        Ok(range.map(PrepareRenameResponse::Range))
    }

    async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
        let uri = params.text_document_position.text_document.uri;
        let position = params.text_document_position.position;
        let new_name = params.new_name;

        let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
            let text_present = store.get(&uri).is_some();
            let docs = store.get_documents(&uri).cloned();
            (docs, text_present)
        } else {
            return Ok(None);
        };

        if !text_present {
            return Ok(None);
        }

        let Some(docs) = docs else {
            return Ok(None);
        };

        Ok(crate::navigation::rename::rename(
            &docs, &uri, position, &new_name,
        ))
    }

    async fn document_color(&self, params: DocumentColorParams) -> Result<Vec<ColorInformation>> {
        if !self.get_color_decorators_enabled() {
            return Ok(Vec::new());
        }

        let uri = params.text_document.uri;
        let text = if let Ok(store) = self.document_store.lock() {
            store.get(&uri).map(str::to_string)
        } else {
            return Ok(Vec::new());
        };

        let Some(text) = text else {
            return Ok(Vec::new());
        };

        let docs = crate::parser::parse_yaml(&text).documents;
        let colors = crate::decorators::color::find_colors(&docs)
            .into_iter()
            .map(|m| ColorInformation {
                range: m.range,
                color: m.color,
            })
            .collect();

        Ok(colors)
    }

    async fn color_presentation(
        &self,
        params: ColorPresentationParams,
    ) -> Result<Vec<ColorPresentation>> {
        Ok(crate::decorators::color::color_presentations(params.color))
    }
}

#[cfg(test)]
#[expect(
    clippy::indexing_slicing,
    clippy::expect_used,
    clippy::unwrap_used,
    clippy::panic,
    reason = "test code"
)]
mod tests {
    use super::*;
    use tower_lsp::lsp_types::{
        FormattingOptions, HoverProviderCapability, OneOf, PartialResultParams,
        TextDocumentIdentifier, TextDocumentSyncCapability, TextDocumentSyncKind,
        WorkDoneProgressParams,
    };

    #[test]
    fn should_advertise_full_text_document_sync() {
        let caps = Backend::capabilities();

        assert_eq!(
            caps.text_document_sync,
            Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL))
        );
    }

    #[test]
    fn should_advertise_hover_provider() {
        let caps = Backend::capabilities();

        assert_eq!(
            caps.hover_provider,
            Some(HoverProviderCapability::Simple(true))
        );
    }

    #[test]
    fn should_advertise_document_symbol_provider() {
        let caps = Backend::capabilities();

        assert!(matches!(
            caps.document_symbol_provider,
            Some(OneOf::Left(true))
        ));
    }

    #[test]
    fn should_advertise_completion_provider() {
        let caps = Backend::capabilities();

        assert!(
            caps.completion_provider.is_some(),
            "capabilities should include completion_provider"
        );
    }

    #[test]
    fn should_advertise_definition_provider() {
        let caps = Backend::capabilities();

        assert!(
            caps.definition_provider.is_some(),
            "capabilities should include definition_provider"
        );
    }

    #[test]
    fn should_advertise_references_provider() {
        let caps = Backend::capabilities();

        assert!(
            caps.references_provider.is_some(),
            "capabilities should include references_provider"
        );
    }

    #[test]
    fn should_advertise_folding_range_provider() {
        let caps = Backend::capabilities();

        assert!(
            caps.folding_range_provider.is_some(),
            "capabilities should include folding_range_provider"
        );
    }

    #[test]
    fn should_advertise_selection_range_provider() {
        let caps = Backend::capabilities();

        assert!(
            caps.selection_range_provider.is_some(),
            "capabilities should include selection_range_provider"
        );
    }

    // Test 35
    #[test]
    fn should_advertise_rename_provider_with_prepare_support() {
        let caps = Backend::capabilities();

        assert!(
            caps.rename_provider.is_some(),
            "capabilities should include rename_provider"
        );

        if let Some(OneOf::Right(rename_opts)) = caps.rename_provider {
            assert_eq!(
                rename_opts.prepare_provider,
                Some(true),
                "rename_provider should have prepare_provider set to true"
            );
        } else {
            panic!("rename_provider should be RenameOptions with prepare_provider");
        }
    }

    #[test]
    fn should_advertise_document_link_provider() {
        let caps = Backend::capabilities();

        assert!(
            caps.document_link_provider.is_some(),
            "capabilities should include document_link_provider"
        );
    }

    // ---- Settings deserialization ----

    #[test]
    fn settings_deserializes_custom_tags_from_json() {
        let json = serde_json::json!({"customTags": ["!include", "!ref"]});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.custom_tags, vec!["!include", "!ref"]);
    }

    #[test]
    fn settings_defaults_to_empty_custom_tags_when_field_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.custom_tags.is_empty());
    }

    #[test]
    fn settings_accepts_empty_custom_tags_array() {
        let json = serde_json::json!({"customTags": []});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.custom_tags.is_empty());
    }

    #[test]
    fn settings_deserializes_key_ordering_true() {
        let json = serde_json::json!({"keyOrdering": true});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.key_ordering);
    }

    #[test]
    fn settings_defaults_key_ordering_to_false_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(!settings.key_ordering);
    }

    // ---- Schemas deserialization ----

    #[test]
    fn settings_deserializes_schemas_from_json() {
        let json = serde_json::json!({"schemas": {"https://example.com/schema.json": "*.yaml"}});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(
            settings
                .schemas
                .get("https://example.com/schema.json")
                .map(String::as_str),
            Some("*.yaml")
        );
    }

    #[test]
    fn settings_defaults_to_empty_schemas_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.schemas.is_empty());
    }

    // ---- Schema associations wiring ----

    #[test]
    fn get_schema_associations_returns_empty_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_schema_associations().is_empty());
    }

    #[test]
    fn get_schema_associations_converts_settings_to_vec() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"schemas": {"https://example.com/schema.json": "*.yaml"}});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        let associations = backend.get_schema_associations();
        assert_eq!(associations.len(), 1);
        assert_eq!(associations[0].url, "https://example.com/schema.json");
        assert_eq!(associations[0].pattern, "*.yaml");
    }

    // ---- Kubernetes version setting ----

    #[test]
    fn default_kubernetes_version_is_master() {
        assert_eq!(DEFAULT_KUBERNETES_VERSION, "master");
    }

    #[test]
    fn settings_deserializes_kubernetes_version() {
        let json = serde_json::json!({"kubernetesVersion": "1.29.0"});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.kubernetes_version.as_deref(), Some("1.29.0"));
    }

    #[test]
    fn settings_defaults_kubernetes_version_to_none_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.kubernetes_version.is_none());
    }

    #[test]
    fn get_kubernetes_version_returns_default_when_not_configured() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert_eq!(backend.get_kubernetes_version(), DEFAULT_KUBERNETES_VERSION);
    }

    #[test]
    fn get_kubernetes_version_returns_configured_value() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"kubernetesVersion": "1.29.0"});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert_eq!(backend.get_kubernetes_version(), "1.29.0");
    }

    // ---- YAML version setting ----

    #[test]
    fn settings_deserializes_yaml_version() {
        let json = serde_json::json!({"yamlVersion": "1.1"});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.yaml_version.as_deref(), Some("1.1"));
    }

    #[test]
    fn settings_defaults_yaml_version_to_none_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.yaml_version.is_none());
    }

    #[test]
    fn get_yaml_version_returns_v1_2_when_nothing_configured() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_2);
    }

    #[test]
    fn get_yaml_version_returns_v1_1_when_setting_is_1_1() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"yamlVersion": "1.1"});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_1);
    }

    #[test]
    fn get_yaml_version_returns_v1_2_when_setting_is_1_2() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"yamlVersion": "1.2"});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_2);
    }

    #[test]
    fn get_yaml_version_returns_v1_1_from_modeline_overriding_v1_2_setting() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"yamlVersion": "1.2"});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        let text = "# yaml-language-server: $yamlVersion=1.1\nkey: value\n";
        assert_eq!(backend.get_yaml_version(text), YamlVersion::V1_1);
    }

    #[test]
    fn get_yaml_version_returns_v1_2_from_modeline_overriding_v1_1_setting() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"yamlVersion": "1.1"});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        let text = "# yaml-language-server: $yamlVersion=1.2\nkey: value\n";
        assert_eq!(backend.get_yaml_version(text), YamlVersion::V1_2);
    }

    #[test]
    fn get_yaml_version_returns_v1_2_default_when_setting_has_invalid_value() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"yamlVersion": "2.0"});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_2);
    }

    #[test]
    fn get_yaml_version_ignores_invalid_modeline_and_uses_setting() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"yamlVersion": "1.1"});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        let text = "# yaml-language-server: $yamlVersion=2.0\nkey: value\n";
        assert_eq!(backend.get_yaml_version(text), YamlVersion::V1_1);
    }

    // ---- Custom tags wiring ----

    #[test]
    fn get_custom_tags_returns_empty_vec_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_custom_tags().is_empty());
    }

    #[test]
    fn should_advertise_code_action_provider() {
        let caps = Backend::capabilities();

        assert!(
            caps.code_action_provider.is_some(),
            "capabilities should include code_action_provider"
        );
    }

    #[test]
    fn should_advertise_code_lens_provider() {
        let caps = Backend::capabilities();
        assert!(caps.code_lens_provider.is_some());
    }

    #[test]
    fn should_advertise_on_type_formatting_provider() {
        let caps = Backend::capabilities();
        assert!(caps.document_on_type_formatting_provider.is_some());
    }

    #[test]
    fn should_advertise_semantic_tokens_provider() {
        let caps = Backend::capabilities();
        assert!(
            caps.semantic_tokens_provider.is_some(),
            "capabilities should include semantic_tokens_provider"
        );
    }

    // ---- schemaStore setting ----

    #[test]
    fn settings_deserializes_schema_store_true() {
        let json = serde_json::json!({"schemaStore": true});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.schema_store, Some(true));
    }

    #[test]
    fn settings_deserializes_schema_store_false() {
        let json = serde_json::json!({"schemaStore": false});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.schema_store, Some(false));
    }

    #[test]
    fn settings_defaults_schema_store_to_none_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.schema_store, None);
    }

    #[test]
    fn get_schema_store_enabled_returns_true_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_schema_store_enabled());
    }

    #[test]
    fn get_schema_store_enabled_returns_false_when_disabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"schemaStore": false});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert!(!backend.get_schema_store_enabled());
    }

    #[test]
    fn get_schema_store_enabled_returns_true_when_explicitly_enabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({"schemaStore": true});
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert!(backend.get_schema_store_enabled());
    }

    // ---- Formatting settings ----

    #[test]
    fn settings_deserializes_format_print_width() {
        let json = serde_json::json!({ "formatPrintWidth": 120 });
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.format_print_width, Some(120));
    }

    #[test]
    fn settings_deserializes_format_single_quote() {
        let json = serde_json::json!({ "formatSingleQuote": true });
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.format_single_quote, Some(true));
    }

    #[test]
    fn settings_deserializes_format_preserve_quotes() {
        let json = serde_json::json!({ "formatPreserveQuotes": true });
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.format_preserve_quotes, Some(true));
    }

    #[test]
    fn settings_format_fields_default_to_none_when_absent() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.format_print_width, None);
        assert_eq!(settings.format_single_quote, None);
    }

    // ---- HTTP proxy setting ----

    #[test]
    fn settings_deserializes_http_proxy() {
        let json = serde_json::json!({ "httpProxy": "http://proxy.corp:8080" });
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(
            settings.http_proxy.as_deref(),
            Some("http://proxy.corp:8080")
        );
    }

    #[test]
    fn settings_defaults_http_proxy_to_none_when_absent() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.http_proxy.is_none());
    }

    #[test]
    fn get_http_proxy_returns_none_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_http_proxy().is_none());
    }

    #[test]
    fn get_http_proxy_returns_configured_value() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({ "httpProxy": "http://proxy.corp:8080" });
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert_eq!(
            backend.get_http_proxy().as_deref(),
            Some("http://proxy.corp:8080")
        );
    }

    // ---- Formatting capability ----

    #[test]
    fn should_advertise_document_formatting_provider() {
        let caps = Backend::capabilities();
        assert!(
            caps.document_formatting_provider.is_some(),
            "capabilities should include document_formatting_provider"
        );
    }

    #[test]
    fn should_advertise_document_range_formatting_provider() {
        let caps = Backend::capabilities();
        assert!(
            caps.document_range_formatting_provider.is_some(),
            "capabilities should include document_range_formatting_provider"
        );
    }

    // ---- Range formatting handler ----

    #[tokio::test]
    async fn range_formatting_returns_none_when_range_already_formatted() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let uri = Url::parse("file:///test.yaml").unwrap();
        if let Ok(mut store) = backend.document_store.lock() {
            store.open(uri.clone(), "key: value\nother: 1\n".to_string());
        }

        let params = DocumentRangeFormattingParams {
            text_document: TextDocumentIdentifier { uri },
            range: Range {
                start: Position {
                    line: 0,
                    character: 0,
                },
                end: Position {
                    line: 0,
                    character: 0,
                },
            },
            options: FormattingOptions {
                tab_size: 2,
                insert_spaces: true,
                ..FormattingOptions::default()
            },
            work_done_progress_params: WorkDoneProgressParams::default(),
        };

        let result = LanguageServer::range_formatting(backend, params)
            .await
            .unwrap();
        assert!(
            result.is_none(),
            "already-formatted range should return None"
        );
    }

    #[tokio::test]
    async fn range_formatting_returns_edit_scoped_to_requested_lines() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let uri = Url::parse("file:///test.yaml").unwrap();
        // Line 1 has a double-space after the colon; the formatter normalises
        // `b:  2` → `b: 2`, guaranteeing a change on exactly that line.
        let text = "a: 1\nb:  2\nc: 3\n";
        if let Ok(mut store) = backend.document_store.lock() {
            store.open(uri.clone(), text.to_string());
        }

        let params = DocumentRangeFormattingParams {
            text_document: TextDocumentIdentifier { uri },
            // Request only line 1 (b:  2).
            range: Range {
                start: Position {
                    line: 1,
                    character: 0,
                },
                end: Position {
                    line: 1,
                    character: 0,
                },
            },
            options: FormattingOptions {
                tab_size: 2,
                insert_spaces: true,
                ..FormattingOptions::default()
            },
            work_done_progress_params: WorkDoneProgressParams::default(),
        };

        let result = LanguageServer::range_formatting(backend, params)
            .await
            .unwrap();
        let edits = result.expect("formatter must produce an edit for `b:  2`");
        assert_eq!(edits.len(), 1);
        // The edit must cover line 1 only, not touch line 0 or line 2.
        assert_eq!(edits[0].range.start.line, 1, "edit start must be line 1");
        assert_eq!(
            edits[0].range.end.line, 2,
            "edit end must be line 2 (exclusive)"
        );
        assert!(
            edits[0].new_text.contains("b: 2"),
            "formatted line must normalise double-space: {:?}",
            edits[0].new_text
        );
    }

    // ---- Formatting handler: version-aware plumbing ----

    #[tokio::test]
    async fn formatting_handler_uses_v1_1_from_modeline() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let uri = Url::parse("file:///test.yaml").unwrap();
        // Double-space after colon forces a formatting change, ensuring the handler
        // runs and produces an edit. Modeline declares V1.1 so `"yes"` must stay quoted.
        let text = "# yaml-language-server: $yamlVersion=1.1\nvalue:  \"yes\"\n";
        if let Ok(mut store) = backend.document_store.lock() {
            store.open(uri.clone(), text.to_string());
        }

        let params = DocumentFormattingParams {
            text_document: TextDocumentIdentifier { uri },
            options: FormattingOptions {
                tab_size: 2,
                insert_spaces: true,
                ..FormattingOptions::default()
            },
            work_done_progress_params: WorkDoneProgressParams::default(),
        };

        let result = LanguageServer::formatting(backend, params).await.unwrap();
        let edits = result.expect("formatter must produce an edit for double-space input");
        let new_text = edits
            .iter()
            .map(|e| e.new_text.as_str())
            .collect::<String>();
        assert!(
            new_text.contains("\"yes\""),
            "V1.1 modeline: yes must stay quoted in formatted output: {new_text:?}"
        );
    }

    // ---- Color provider capability ----

    #[test]
    fn should_advertise_color_provider() {
        let caps = Backend::capabilities();
        assert!(
            caps.color_provider.is_some(),
            "capabilities should include color_provider"
        );
    }

    // ---- colorDecorators setting ----

    #[test]
    fn settings_deserializes_color_decorators_false() {
        let json = serde_json::json!({ "colorDecorators": false });
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.color_decorators, Some(false));
    }

    #[test]
    fn settings_defaults_color_decorators_to_none_when_absent() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.color_decorators.is_none());
    }

    #[test]
    fn get_color_decorators_enabled_returns_true_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_color_decorators_enabled());
    }

    #[test]
    fn get_color_decorators_enabled_returns_false_when_disabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({ "colorDecorators": false });
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        assert!(!backend.get_color_decorators_enabled());
    }

    // ---- document_color handler ----

    #[tokio::test]
    async fn document_color_returns_colors_for_yaml_with_hex_values() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let uri = Url::parse("file:///test.yaml").unwrap();
        if let Ok(mut store) = backend.document_store.lock() {
            store.open(uri.clone(), "color: '#ff0000'\n".to_string());
        }

        let params = DocumentColorParams {
            text_document: TextDocumentIdentifier { uri },
            work_done_progress_params: WorkDoneProgressParams::default(),
            partial_result_params: PartialResultParams::default(),
        };

        let colors = LanguageServer::document_color(backend, params)
            .await
            .unwrap();
        assert!(!colors.is_empty(), "should detect hex color in YAML value");
    }

    #[tokio::test]
    async fn document_color_returns_empty_when_color_decorators_disabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();

        let json = serde_json::json!({ "colorDecorators": false });
        let new_settings: Settings = serde_json::from_value(json).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }

        let uri = Url::parse("file:///test.yaml").unwrap();
        if let Ok(mut store) = backend.document_store.lock() {
            store.open(uri.clone(), "color: '#ff0000'\n".to_string());
        }

        let params = DocumentColorParams {
            text_document: TextDocumentIdentifier { uri },
            work_done_progress_params: WorkDoneProgressParams::default(),
            partial_result_params: PartialResultParams::default(),
        };

        let colors = LanguageServer::document_color(backend, params)
            .await
            .unwrap();
        assert!(
            colors.is_empty(),
            "should return empty when color decorators are disabled"
        );
    }

    // ---- validate setting ----

    #[test]
    fn settings_deserializes_validate_false() {
        let json = serde_json::json!({"validate": false});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.validate, Some(false));
    }

    #[test]
    fn settings_deserializes_validate_true() {
        let json = serde_json::json!({"validate": true});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.validate, Some(true));
    }

    #[test]
    fn settings_defaults_validate_to_none_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.validate.is_none());
    }

    #[test]
    fn get_validate_returns_true_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_validate());
    }

    #[test]
    fn get_validate_returns_false_when_disabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        let new_settings: Settings =
            serde_json::from_value(serde_json::json!({"validate": false})).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }
        assert!(!backend.get_validate());
    }

    #[test]
    fn get_validate_returns_true_when_explicitly_enabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        let new_settings: Settings =
            serde_json::from_value(serde_json::json!({"validate": true})).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }
        assert!(backend.get_validate());
    }

    // ---- hover setting ----

    #[test]
    fn settings_deserializes_hover_false() {
        let json = serde_json::json!({"hover": false});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.hover, Some(false));
    }

    #[test]
    fn settings_defaults_hover_to_none_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.hover.is_none());
    }

    #[test]
    fn get_hover_enabled_returns_true_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_hover_enabled());
    }

    #[test]
    fn get_hover_enabled_returns_false_when_disabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        let new_settings: Settings =
            serde_json::from_value(serde_json::json!({"hover": false})).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }
        assert!(!backend.get_hover_enabled());
    }

    // ---- completion setting ----

    #[test]
    fn settings_deserializes_completion_false() {
        let json = serde_json::json!({"completion": false});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.completion, Some(false));
    }

    #[test]
    fn settings_defaults_completion_to_none_when_missing() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.completion.is_none());
    }

    #[test]
    fn get_completion_enabled_returns_true_by_default() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert!(backend.get_completion_enabled());
    }

    #[test]
    fn get_completion_enabled_returns_false_when_disabled() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        let new_settings: Settings =
            serde_json::from_value(serde_json::json!({"completion": false})).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }
        assert!(!backend.get_completion_enabled());
    }

    // ---- maxItemsComputed setting ----

    #[test]
    fn settings_deserializes_max_items_computed() {
        let json = serde_json::json!({"maxItemsComputed": 100});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.max_items_computed, Some(100));
    }

    #[test]
    fn settings_defaults_max_items_computed_to_none_when_absent() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.max_items_computed.is_none());
    }

    #[test]
    fn settings_accepts_max_items_computed_zero() {
        let json = serde_json::json!({"maxItemsComputed": 0});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.max_items_computed, Some(0));
    }

    #[test]
    fn get_max_items_computed_returns_5000_when_setting_absent() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        assert_eq!(backend.get_max_items_computed(), 5000);
    }

    #[test]
    fn get_max_items_computed_returns_configured_value() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        let new_settings: Settings =
            serde_json::from_value(serde_json::json!({"maxItemsComputed": 100})).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }
        assert_eq!(backend.get_max_items_computed(), 100);
    }

    #[test]
    fn get_max_items_computed_returns_5000_when_field_is_none() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        let new_settings: Settings = serde_json::from_value(serde_json::json!({})).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }
        assert_eq!(backend.get_max_items_computed(), 5000);
    }

    #[test]
    fn get_max_items_computed_returns_zero_when_configured_to_zero() {
        let (service, _) = tower_lsp::LspService::new(Backend::new);
        let backend = service.inner();
        let new_settings: Settings =
            serde_json::from_value(serde_json::json!({"maxItemsComputed": 0})).unwrap();
        if let Ok(mut s) = backend.settings.lock() {
            *s = new_settings;
        }
        assert_eq!(backend.get_max_items_computed(), 0);
    }

    // ---- duplicateKeys setting ----

    #[test]
    fn settings_deserializes_duplicate_keys_off() {
        let json = serde_json::json!({"duplicateKeys": "off"});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.duplicate_keys.as_deref(), Some("off"));
    }

    #[test]
    fn settings_deserializes_duplicate_keys_warning() {
        let json = serde_json::json!({"duplicateKeys": "warning"});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.duplicate_keys.as_deref(), Some("warning"));
    }

    #[test]
    fn settings_deserializes_duplicate_keys_error() {
        let json = serde_json::json!({"duplicateKeys": "error"});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.duplicate_keys.as_deref(), Some("error"));
    }

    #[test]
    fn settings_defaults_duplicate_keys_to_none_when_absent() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.duplicate_keys.is_none());
    }

    // ---- formatRemoveDuplicateKeys setting ----

    // K1: formatRemoveDuplicateKeys: true deserializes correctly.
    #[test]
    fn settings_deserializes_format_remove_duplicate_keys_true() {
        let json = serde_json::json!({"formatRemoveDuplicateKeys": true});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.format_remove_duplicate_keys, Some(true));
    }

    // K2: formatRemoveDuplicateKeys: false deserializes correctly.
    #[test]
    fn settings_deserializes_format_remove_duplicate_keys_false() {
        let json = serde_json::json!({"formatRemoveDuplicateKeys": false});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert_eq!(settings.format_remove_duplicate_keys, Some(false));
    }

    // K3: field absent — defaults to None.
    #[test]
    fn settings_defaults_format_remove_duplicate_keys_to_none_when_absent() {
        let json = serde_json::json!({});
        let settings: Settings = serde_json::from_value(json).unwrap();
        assert!(settings.format_remove_duplicate_keys.is_none());
    }
}
