use std::borrow::Cow;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;

use ls_types::{Diagnostic, DiagnosticSeverity, Position, Range, Uri, WorkspaceFolder};
use notify_debouncer_full::{DebounceEventResult, new_debouncer};
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tower_lsp_server::Client;

use std::collections::HashSet;

use crate::{
    BackendRuntime, BaconLs, Correction, DiagKey, DiagnosticData, LOCATIONS_FILE, PKG_NAME, State, diag_key,
    path_to_file_uri,
};

#[derive(Debug, Deserialize, Serialize)]
struct BaconConfig {
    jobs: Jobs,
    exports: Exports,
}

#[derive(Debug, Deserialize, Serialize)]
struct Jobs {
    #[serde(rename = "bacon-ls")]
    bacon_ls: BaconLsJob,
}

#[derive(Debug, Deserialize, Serialize)]
struct BaconLsJob {
    #[serde(skip_deserializing)]
    command: Vec<String>,
    analyzer: String,
    need_stdout: bool,
}

#[derive(Debug, Deserialize, Serialize)]
struct Exports {
    #[serde(rename = "cargo-json-spans")]
    cargo_json_spans: CargoJsonSpans,
}

#[derive(Debug, Deserialize, Serialize)]
struct CargoJsonSpans {
    auto: bool,
    exporter: String,
    line_format: String,
    path: String,
}

const ERROR_MESSAGE: &str = "bacon configuration is not compatible with bacon-ls: please take a look to https://github.com/crisidev/bacon-ls?tab=readme-ov-file#configuration and adapt your bacon configuration";
const BACON_ANALYZER: &str = "cargo_json";
const BACON_EXPORTER: &str = "analyzer";
const BACON_COMMAND: [&str; 7] = [
    "cargo",
    "clippy",
    "--tests",
    "--all-targets",
    "--all-features",
    "--message-format",
    "json-diagnostic-rendered-ansi",
];
const LINE_FORMAT: &str = "{diagnostic.level}|:|{span.file_name}|:|{span.line_start}|:|{span.line_end}|:|{span.column_start}|:|{span.column_end}|:|{diagnostic.message}|:|{diagnostic.rendered}|:|{span.suggested_replacement}";

pub(crate) struct Bacon;

impl Bacon {
    async fn validate_preferences_file(path: &Path) -> Result<(), String> {
        let toml_content = tokio::fs::read_to_string(path)
            .await
            .map_err(|e| format!("{ERROR_MESSAGE}: {e}"))?;
        let config: BaconConfig = toml::from_str(&toml_content).map_err(|e| format!("{ERROR_MESSAGE}: {e}"))?;
        tracing::debug!("bacon config is {config:#?}");
        if config.jobs.bacon_ls.analyzer == BACON_ANALYZER
            && config.jobs.bacon_ls.need_stdout
            && config.exports.cargo_json_spans.auto
            && config.exports.cargo_json_spans.exporter == BACON_EXPORTER
            && config.exports.cargo_json_spans.line_format == LINE_FORMAT
            && config.exports.cargo_json_spans.path == LOCATIONS_FILE
        {
            tracing::info!("bacon configuration {} is valid", path.display());
            Ok(())
        } else {
            Err(ERROR_MESSAGE.to_string())
        }
    }

    async fn create_preferences_file(filename: &str) -> Result<(), String> {
        let bacon_config = BaconConfig {
            jobs: Jobs {
                bacon_ls: BaconLsJob {
                    command: BACON_COMMAND.map(|c| c.to_string()).into_iter().collect(),
                    analyzer: BACON_ANALYZER.to_string(),
                    need_stdout: true,
                },
            },
            exports: Exports {
                cargo_json_spans: CargoJsonSpans {
                    auto: true,
                    exporter: BACON_EXPORTER.to_string(),
                    line_format: LINE_FORMAT.to_string(),
                    path: LOCATIONS_FILE.to_string(),
                },
            },
        };
        tracing::info!("creating new bacon preference file {filename}",);
        let toml_string = toml::to_string_pretty(&bacon_config)
            .map_err(|e| format!("error serializing bacon preferences {filename} content: {e}"))?;
        let mut file = File::create(filename)
            .await
            .map_err(|e| format!("error creating bacon preferences {filename}: {e}"))?;
        file.write_all(toml_string.as_bytes())
            .await
            .map_err(|e| format!("error writing bacon preferences {filename}: {e}"))?;
        Ok(())
    }

    async fn validate_preferences_impl(bacon_prefs: &[u8], create_prefs_file: bool) -> Result<(), String> {
        let bacon_prefs_files = String::from_utf8_lossy(bacon_prefs);
        let bacon_prefs_files_split: Vec<&str> = bacon_prefs_files.split("\n").collect();
        let mut preference_file_exists = false;
        for prefs_file in bacon_prefs_files_split.iter() {
            let prefs_file_path = Path::new(prefs_file);
            if prefs_file_path.exists() {
                preference_file_exists = true;
                Self::validate_preferences_file(prefs_file_path).await?;
            } else {
                tracing::debug!("skipping non existing bacon preference file {prefs_file}");
            }
        }

        if !preference_file_exists && create_prefs_file {
            Self::create_preferences_file(bacon_prefs_files_split[0]).await?;
        }

        Ok(())
    }

    /// Walks `root` recursively for files named `locations_file_name`. Iterative
    /// (stack-based) so it doesn't grow the async stack on deep trees, and uses
    /// `tokio::fs` so the directory walk yields to the runtime instead of
    /// blocking the executor on large workspaces.
    pub(crate) async fn find_bacon_locations(root: &Path, locations_file_name: &str) -> std::io::Result<Vec<PathBuf>> {
        let mut results = Vec::new();
        let mut stack: Vec<PathBuf> = vec![root.to_path_buf()];
        while let Some(dir) = stack.pop() {
            let mut entries = tokio::fs::read_dir(&dir).await?;
            while let Some(entry) = entries.next_entry().await? {
                let path = entry.path();
                let file_type = entry.file_type().await?;
                if file_type.is_dir() {
                    stack.push(path);
                } else if path.file_name().is_some_and(|name| name == locations_file_name) {
                    results.push(path);
                }
            }
        }
        Ok(results)
    }

    fn parse_severity(severity_str: &str) -> DiagnosticSeverity {
        match severity_str {
            "error" => DiagnosticSeverity::ERROR,
            "warning" => DiagnosticSeverity::WARNING,
            "info" | "information" | "note" | "failure-note" => DiagnosticSeverity::INFORMATION,
            "hint" | "help" => DiagnosticSeverity::HINT,
            other => {
                tracing::warn!("unknown bacon severity level {other:?}, defaulting to INFORMATION");
                DiagnosticSeverity::INFORMATION
            }
        }
    }

    fn parse_positions(fields: &[&str]) -> Option<(u32, u32, u32, u32)> {
        let line_start = fields.first()?.parse().ok()?;
        let line_end = fields.get(1)?.parse().ok()?;
        let column_start = fields.get(2)?.parse().ok()?;
        let column_end = fields.get(3)?.parse().ok()?;
        Some((line_start, line_end, column_start, column_end))
    }

    fn parse_bacon_diagnostic_line(line: &str, folder_path: &Path) -> Option<(Uri, Diagnostic)> {
        // Split line into parts; expect exactly 7 parts in the format specified.
        let line_split: Vec<_> = line.splitn(9, "|:|").collect();

        if line_split.len() != 9 {
            tracing::error!(
                "malformed line: expected 9 parts in the format of `severity|:|path|:|line_start|:|line_end|:|column_start|:|column_end|:|message|:|rendered_message|:|replacement` but found {}: {}",
                line_split.len(),
                line
            );
            return None;
        }

        // Parse elements from the split line
        let severity = Self::parse_severity(line_split[0]);
        let file_path = folder_path.join(line_split[1]);

        // Handle potential parse errors
        let (line_start, line_end, column_start, column_end) = match Self::parse_positions(&line_split[2..6]) {
            Some(values) => values,
            None => {
                tracing::error!("error parsing diagnostic position {:?}", &line_split[2..6]);
                return None;
            }
        };

        let Some(file_path_str) = file_path.to_str() else {
            tracing::error!("file path is not valid UTF-8: {}", file_path.display());
            return None;
        };
        let path = match str::parse::<Uri>(&path_to_file_uri(file_path_str)) {
            Ok(url) => url,
            Err(e) => {
                tracing::error!("error parsing file path {}: {}", file_path.display(), e);
                return None;
            }
        };

        let mut message = line_split[6].replace("\\n", "\n").trim_end_matches('\n').to_string();
        let range = Range::new(
            Position::new(line_start.saturating_sub(1), column_start.saturating_sub(1)),
            Position::new(line_end.saturating_sub(1), column_end.saturating_sub(1)),
        );
        let replacement = line_split[8];
        let data = if replacement != "none" {
            tracing::debug!("storing potential quick fix code action to replace word with {replacement}");
            Some(serde_json::json!(DiagnosticData {
                corrections: vec![Correction::from_single(range, replacement)]
            }))
        } else {
            None
        };

        tracing::debug!(
            "new diagnostic: severity: {severity:?}, path: {path:?}, line_start: {line_start}, line_end: {line_end}, column_start: {column_start}, column_end: {column_end}, message: {message}",
        );

        // Create the Diagnostic object
        let rendered_message = line_split[7];
        if rendered_message != "none" {
            message = ansi_regex::ansi_regex()
                .replace_all(rendered_message, "")
                .trim_end_matches('\n')
                .to_string()
        }
        let diagnostic = Diagnostic {
            range,
            severity: Some(severity),
            source: Some(PKG_NAME.to_string()),
            message,
            data,
            ..Diagnostic::default()
        };

        Some((path, diagnostic))
    }

    fn deduplicate_diagnostics(
        path: Uri,
        uri: &Uri,
        diagnostic: Diagnostic,
        diagnostics: &mut Vec<(Uri, Diagnostic)>,
        seen: &mut HashSet<DiagKey>,
    ) {
        if &path != uri {
            return;
        }
        if seen.insert(diag_key(&diagnostic)) {
            diagnostics.push((path, diagnostic));
        }
    }

    pub(crate) async fn validate_preferences(bacon_command: &str, create_prefs_file: bool) -> Result<(), String> {
        let bacon_prefs = Command::new(bacon_command)
            .arg("--prefs")
            .output()
            .await
            .map_err(|e| e.to_string())?;
        Self::validate_preferences_impl(&bacon_prefs.stdout, create_prefs_file).await
    }

    pub(crate) async fn run_in_background(
        bacon_command: &str,
        bacon_command_args: &str,
        current_dir: Option<&PathBuf>,
        cancel_token: CancellationToken,
    ) -> Result<JoinHandle<()>, String> {
        tracing::info!("starting bacon in background with arguments `{bacon_command_args}`");
        let log_bacon = env::var("BACON_LS_LOG_BACON").unwrap_or("on".to_string());
        let mut command = Command::new(bacon_command);
        command
            .args(bacon_command_args.split_whitespace().collect::<Vec<&str>>())
            .stdin(Stdio::null())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .kill_on_drop(true);
        if let Some(current_dir) = current_dir {
            command.current_dir(current_dir);
        }

        match command.spawn() {
            Ok(mut child) => {
                // Handle stdout
                if log_bacon != "off"
                    && let Some(stdout) = child.stdout.take()
                {
                    let reader = BufReader::new(stdout).lines();
                    tokio::spawn(async move {
                        let mut reader = reader;
                        while let Ok(Some(line)) = reader.next_line().await {
                            tracing::info!("[bacon stdout]: {}", line);
                        }
                    });
                }

                // Handle stderr
                if log_bacon != "off"
                    && let Some(stderr) = child.stderr.take()
                {
                    let reader = BufReader::new(stderr).lines();
                    tokio::spawn(async move {
                        let mut reader = reader;
                        while let Ok(Some(line)) = reader.next_line().await {
                            tracing::error!("[bacon stderr]: {}", line);
                        }
                    });
                }

                // Wait for the child process to finish
                Ok(tokio::spawn(async move {
                    tracing::debug!("waiting for bacon to terminate");
                    tokio::select! {
                        _ = child.wait() => {},
                        _ = cancel_token.cancelled() => {},
                    };
                }))
            }
            Err(e) => Err(format!("failed to start bacon: {e}")),
        }
    }

    async fn diagnostics(
        uri: &Uri,
        locations_file_name: &str,
        workspace_folders: Option<&[WorkspaceFolder]>,
    ) -> Vec<(Uri, Diagnostic)> {
        let mut diagnostics: Vec<(Uri, Diagnostic)> = vec![];
        let mut seen: HashSet<DiagKey> = HashSet::new();

        if let Some(workspace_folders) = workspace_folders {
            for folder in workspace_folders.iter() {
                let Some(mut folder_path) = folder.uri.to_file_path() else {
                    tracing::warn!("skipping workspace folder with non-file URI: {}", folder.uri.as_str());
                    continue;
                };
                if let Some(git_root) = BaconLs::find_git_root_directory(&folder_path).await
                    && git_root.join("Cargo.toml").exists()
                {
                    tracing::debug!(
                        "found git root directory {}, using it for files base path",
                        git_root.display()
                    );
                    folder_path = Cow::Owned(git_root);
                }
                let bacon_locations = match Bacon::find_bacon_locations(&folder_path, locations_file_name).await {
                    Ok(v) => v,
                    Err(e) => {
                        tracing::warn!("unable to find valid bacon location files: {e}");
                        Vec::new()
                    }
                };
                for bacon_location in bacon_locations.iter() {
                    tracing::info!("found bacon locations file to parse {}", bacon_location.display());
                    match File::open(&bacon_location).await {
                        Ok(fd) => {
                            let reader = BufReader::new(fd);
                            let mut lines = reader.lines();
                            let mut buffer = String::new();

                            while let Some(line) = lines.next_line().await.unwrap_or_else(|e| {
                                tracing::error!("error reading line from file {}: {e}", bacon_location.display());
                                None
                            }) {
                                let trimmed = line.trim_end();

                                // Use the first word to determine the start of a new diagnostic
                                let is_new_diagnostic = trimmed.starts_with("warning")
                                    || trimmed.starts_with("error")
                                    || trimmed.starts_with("info")
                                    || trimmed.starts_with("note")
                                    || trimmed.starts_with("failure-note")
                                    || trimmed.starts_with("help");

                                if is_new_diagnostic {
                                    // Process the collected buffer before starting a new entry
                                    if !buffer.is_empty()
                                        && let Some((path, diagnostic)) =
                                            Self::parse_bacon_diagnostic_line(&buffer, &folder_path)
                                    {
                                        tracing::debug!("found diagnostic for {}", path.as_str());
                                        Self::deduplicate_diagnostics(
                                            path.clone(),
                                            uri,
                                            diagnostic,
                                            &mut diagnostics,
                                            &mut seen,
                                        );
                                    }
                                    // Reset buffer for new diagnostic entry
                                    buffer.clear();
                                }

                                // Append current line to buffer
                                if !buffer.is_empty() {
                                    buffer.push('\n'); // Preserve multiline structure
                                }
                                buffer.push_str(trimmed);
                            }

                            // Flush the remaining buffer after loop ends
                            if !buffer.is_empty()
                                && let Some((path, diagnostic)) =
                                    Self::parse_bacon_diagnostic_line(&buffer, &folder_path)
                            {
                                Self::deduplicate_diagnostics(
                                    path.clone(),
                                    uri,
                                    diagnostic,
                                    &mut diagnostics,
                                    &mut seen,
                                );
                            }
                        }
                        Err(e) => {
                            tracing::error!("unable to read file {}: {e}", bacon_location.display())
                        }
                    }
                }
            }
        }
        diagnostics
    }

    async fn diagnostics_vec(
        uri: &Uri,
        locations_file_name: &str,
        workspace_folders: Option<&[WorkspaceFolder]>,
    ) -> Vec<Diagnostic> {
        Self::diagnostics(uri, locations_file_name, workspace_folders)
            .await
            .into_iter()
            .map(|(_, y)| y)
            .collect::<Vec<Diagnostic>>()
    }

    pub(crate) async fn synchronize_diagnostics(state: Arc<RwLock<State>>, client: Arc<Client>) {
        tracing::info!("starting background task in charge of syncronizing diagnostics for all open files");
        let (tx, rx) = flume::unbounded::<DebounceEventResult>();

        let (locations_file, proj_root, wait_time, shutdown_token) = {
            let state = state.read().await;
            let Some(BackendRuntime::Bacon { config, runtime }) = &state.backend else {
                tracing::error!("synchronize_diagnostics called without bacon backend");
                return;
            };
            (
                config.locations_file.clone(),
                state.project_root.clone(),
                config.synchronize_all_open_files_wait,
                runtime.shutdown_token.clone(),
            )
        };

        let mut watcher = match new_debouncer(wait_time, None, move |ev: DebounceEventResult| {
            // Returns an error if all senders are dropped.
            let _res = tx.send(ev);
        }) {
            Ok(watcher) => watcher,
            Err(e) => {
                let msg = format!(
                    "bacon-ls could not create a file watcher: {e}. \
                     Diagnostics will still update on save but open-file \
                     synchronization is disabled."
                );
                tracing::error!("{msg}");
                client.show_message(ls_types::MessageType::WARNING, msg).await;
                return;
            }
        };

        let locations_file_path =
            proj_root.map_or_else(|| PathBuf::from(&locations_file), |root| root.join(&locations_file));
        loop {
            match watcher.watch(PathBuf::from(&locations_file_path), notify::RecursiveMode::Recursive) {
                Ok(_) => {
                    tracing::info!("watching '{}' for changes...", locations_file_path.display());
                    break;
                }
                Err(e) => {
                    tracing::warn!(
                        "unable to watch '{}', retrying in 1 second",
                        locations_file_path.display()
                    );
                    tracing::error!(".bacon_locations watcher error: {e}");
                    tokio::time::sleep(Duration::from_secs(1)).await;
                }
            }
        }

        while let Some(Ok(res)) = tokio::select! {
            ev = rx.recv_async() => {
                Some(ev)
            }
            _ = shutdown_token.cancelled() => {
                None
            }
        } {
            let events = match res {
                Ok(events) => events,
                Err(err) => {
                    tracing::error!(?err, "watch error");
                    continue;
                }
            };
            // Only publish if the file was modified.
            if !events.iter().any(|ev| ev.kind.is_modify()) {
                continue;
            }

            let mut loop_state = state.write().await;
            let Some(BackendRuntime::Bacon { runtime, .. }) = &mut loop_state.backend else {
                tracing::error!("backend changed during sync loop");
                return;
            };
            runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);
            let version = runtime.diagnostics_version;
            let open_files = runtime.open_files.clone();
            let workspace_folders = loop_state.workspace_folders.clone();
            drop(loop_state);
            tracing::debug!(
                "running periodic diagnostic publish for open files `{}`",
                open_files.iter().map(|f| f.to_string()).collect::<Vec<_>>().join(",")
            );
            for uri in open_files.iter() {
                Self::publish_diagnostics(&client, uri, &locations_file, workspace_folders.as_deref(), version).await;
            }
        }
    }

    pub(crate) async fn publish_diagnostics(
        client: &Arc<Client>,
        uri: &Uri,
        locations_file_name: &str,
        workspace_folders: Option<&[WorkspaceFolder]>,
        version: i32,
    ) {
        let diagnostics_vec = Self::diagnostics_vec(uri, locations_file_name, workspace_folders).await;
        tracing::info!("sent {} bacon diagnostics for {uri:?}", diagnostics_vec.len());
        client
            .publish_diagnostics(uri.clone(), diagnostics_vec, Some(version))
            .await;
    }
}

#[cfg(test)]
mod tests {
    use std::io::Write;

    use super::*;
    use pretty_assertions::assert_eq;
    use tempfile::TempDir;

    #[tokio::test]
    async fn test_valid_bacon_preferences() {
        let valid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", valid_toml).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_ok());
    }

    #[tokio::test]
    async fn test_invalid_analyzer() {
        let invalid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "incorrect_analyzer"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );

        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", invalid_toml).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_invalid_line_format() {
        let invalid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "invalid_line_format"
            path = "{LOCATIONS_FILE}"
        "#
        );

        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", invalid_toml).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_validate_preferences() {
        let valid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );
        assert!(
            Bacon::validate_preferences_impl(valid_toml.as_bytes(), false)
                .await
                .is_ok()
        );
    }

    #[tokio::test]
    async fn test_file_creation_failure() {
        let invalid_path = "/invalid/path/to/file.toml";
        let result = Bacon::create_preferences_file(invalid_path).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("error creating bacon preferences"));
    }

    #[tokio::test]
    async fn test_file_write_failure() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        // Simulate write failure by closing the file prematurely
        let file = File::create(&file_path).await.unwrap();
        drop(file); // Close the file to simulate failure
        let result = Bacon::create_preferences_file(file_path.to_str().unwrap()).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_empty_bacon_preferences_file() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("empty_prefs.toml");
        std::fs::File::create(&file_path).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_run_in_background() {
        let cancel_token = CancellationToken::new();
        let handle = Bacon::run_in_background("cargo", "--version", None, cancel_token.clone()).await;
        assert!(handle.is_ok());
        cancel_token.cancel();
        handle.unwrap().await.unwrap();
    }

    const ERROR_LINE: &str = "error|:|/app/github/bacon-ls/src/lib.rs|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope\n    |\n352 |         one\n    |         ^^^ help: a unit variant with a similar name exists: `None`\n    |\n   ::: /Users/matteobigoi/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:576:5\n    |\n576 |     None,\n    |     ---- similarly named unit variant `None` defined here\n\nFor more information about this error, try `rustc --explain E0425`.\nerror: could not compile `bacon-ls` (lib) due to 1 previous error|:|none|:|none";

    #[test]
    fn test_parse_bacon_diagnostic_line_with_spans_ok() {
        let result = Bacon::parse_bacon_diagnostic_line(ERROR_LINE, Path::new("/app/github/bacon-ls"));
        let (url, diagnostic) = result.unwrap();
        assert_eq!(url.to_string(), "file:///app/github/bacon-ls/src/lib.rs");
        assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR));
        assert_eq!(diagnostic.source, Some(PKG_NAME.to_string()));
        assert_eq!(
            diagnostic.message,
            r#"cannot find value `one` in this scope
    |
352 |         one
    |         ^^^ help: a unit variant with a similar name exists: `None`
    |
   ::: /Users/matteobigoi/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:576:5
    |
576 |     None,
    |     ---- similarly named unit variant `None` defined here

For more information about this error, try `rustc --explain E0425`.
error: could not compile `bacon-ls` (lib) due to 1 previous error"#
        );
        let result = Bacon::parse_bacon_diagnostic_line(ERROR_LINE, Path::new("/app/github/bacon-ls"));
        let (url, diagnostic) = result.unwrap();
        assert_eq!(url.to_string(), "file:///app/github/bacon-ls/src/lib.rs");
        assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR));
        assert_eq!(diagnostic.source, Some(PKG_NAME.to_string()));
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_with_spans_ko() {
        // Unparsable line
        let result = Bacon::parse_bacon_diagnostic_line("warning:/file:1:1", Path::new("/app/github/bacon-ls"));
        assert_eq!(result, None);

        // Empty line
        let result = Bacon::parse_bacon_diagnostic_line("", Path::new("/app/github/bacon-ls"));
        assert_eq!(result, None);
    }

    #[tokio::test]
    #[cfg(not(target_os = "windows"))]
    async fn test_bacon_multiline_diagnostics_production() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join(".bacon-locations");
        let mut tmp_file = std::fs::File::create(file_path).unwrap();
        let error_path = format!("{}/src/lib.rs", tmp_dir.path().display());
        let error_path_url = str::parse::<Uri>(&format!("file://{error_path}")).unwrap();
        writeln!(
            tmp_file,
            "warning|:|src/lib.rs|:|130|:|142|:|33|:|34|:|this if statement can be collapsed|:|none|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            r#"help|:|{error_path}|:|130|:|142|:|33|:|34|:|collapse nested if block|:|none|:|if Some(&the_path) == uri && !diagnostics.iter().any(
                                        |(existing_path, existing_diagnostic)| {{
                                            existing_path.path() == the_path.path()
                                                && diagnostic.range == existing_diagnostic.range
                                                && diagnostic.severity
                                                    == existing_diagnostic.severity
                                                && diagnostic.message == existing_diagnostic.message
                                        }},
                                    ) {{
                                    diagnostics.push((path, diagnostic));
                                }}"#
        ).unwrap();
        writeln!(
            tmp_file,
            "warning|:|{error_path}|:|150|:|162|:|33|:|34|:|this if statement can be collapsed again|:|none|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            r#"warning|:|{error_path}|:|150|:|162|:|33|:|34|:|collapse nested if block|:|if Some(&other_path) == uri && !diagnostics.iter().any(
                                        |(existing_path, existing_diagnostic)| {{
                                            existing_path.path() == other_path.path()
                                                && diagnostic.range == existing_diagnostic.range
                                                && diagnostic.severity
                                                    == existing_diagnostic.severity
                                                && diagnostic.message == existing_diagnostic.message
                                        }},
                                    ) {{
                                    diagnostics.push((path, diagnostic));
                                }}|:|none"#
        ).unwrap();

        let workspace_folders = Some(vec![WorkspaceFolder {
            name: tmp_dir.path().display().to_string(),
            uri: str::parse::<Uri>(&format!("file://{}", tmp_dir.path().display())).unwrap(),
        }]);
        let diagnostics = Bacon::diagnostics(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;
        assert_eq!(diagnostics.len(), 4);
        assert!(diagnostics[0].1.data.is_none());
        assert_eq!(diagnostics[0].1.message.len(), 34);
        assert!(diagnostics[1].1.data.is_some());
        assert_eq!(diagnostics[1].1.message.len(), 24);
        assert!(diagnostics[2].1.data.is_none());
        assert_eq!(diagnostics[2].1.message.len(), 40);
        assert!(diagnostics[3].1.data.is_none());
        assert_eq!(diagnostics[3].1.message.len(), 766);
    }

    #[tokio::test]
    #[cfg(not(target_os = "windows"))]
    async fn test_bacon_diagnostics_production_and_deduplication() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join(".bacon-locations");
        let mut tmp_file = std::fs::File::create(file_path).unwrap();
        let error_path = format!("{}/src/lib.rs", tmp_dir.path().display());
        let error_path_url = str::parse::<Uri>(&format!("file://{error_path}")).unwrap();
        writeln!(
            tmp_file,
            "error|:|{error_path}|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope|:|none|:|none"
        )
        .unwrap();
        // duplicate the line
        writeln!(
            tmp_file,
            "error|:|{error_path}|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope|:|none|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            "warning|:|{error_path}|:|354|:|354|:|9|:|20|:|cannot find value `two` in this scope|:|some|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            "help|:|{error_path}|:|356|:|356|:|9|:|20|:|cannot find value `three` in this scope|:|none|:|some other"
        )
        .unwrap();

        let workspace_folders = Some(vec![WorkspaceFolder {
            name: tmp_dir.path().display().to_string(),
            uri: str::parse::<Uri>(&format!("file://{}", tmp_dir.path().display())).unwrap(),
        }]);
        let diagnostics = Bacon::diagnostics(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;
        assert_eq!(diagnostics.len(), 3);
        let diagnostics_vec =
            Bacon::diagnostics_vec(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;
        assert_eq!(diagnostics_vec.len(), 3);
    }

    #[test]
    fn test_parse_severity_known_levels() {
        assert_eq!(Bacon::parse_severity("error"), DiagnosticSeverity::ERROR);
        assert_eq!(Bacon::parse_severity("warning"), DiagnosticSeverity::WARNING);
        assert_eq!(Bacon::parse_severity("note"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("info"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("information"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("failure-note"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("help"), DiagnosticSeverity::HINT);
        assert_eq!(Bacon::parse_severity("hint"), DiagnosticSeverity::HINT);
    }

    #[test]
    fn test_parse_severity_unknown_level_defaults_to_information() {
        assert_eq!(
            Bacon::parse_severity("future-rustc-level"),
            DiagnosticSeverity::INFORMATION
        );
    }

    #[test]
    fn test_parse_positions_valid() {
        let parts = ["1", "2", "3", "4"];
        assert_eq!(Bacon::parse_positions(&parts), Some((1, 2, 3, 4)));
    }

    #[test]
    fn test_parse_positions_non_numeric_returns_none() {
        let parts = ["1", "x", "3", "4"];
        assert_eq!(Bacon::parse_positions(&parts), None);
    }

    #[test]
    fn test_parse_positions_too_few_fields_returns_none() {
        let parts = ["1", "2", "3"];
        assert_eq!(Bacon::parse_positions(&parts), None);
    }

    #[test]
    #[cfg(not(target_os = "windows"))]
    fn test_parse_bacon_diagnostic_line_with_replacement_attaches_correction() {
        // Skipped on Windows: this test asserts the produced URI as a unix-style
        // string. On Windows `Path::new("/proj").join("src/lib.rs")` produces
        // backslashes which percent-encode to `%5C` in the URI.
        let line = "warning|:|src/lib.rs|:|10|:|10|:|5|:|8|:|unused import|:|none|:|use foo::bar;";
        let (uri, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new("/proj")).expect("must parse");
        assert_eq!(uri.to_string(), "file:///proj/src/lib.rs");
        assert!(
            diag.data.is_some(),
            "non-`none` replacement should attach correction data"
        );
        // Position is converted from 1-based to 0-based.
        assert_eq!(diag.range.start.line, 9);
        assert_eq!(diag.range.start.character, 4);
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_zero_position_saturates() {
        // Defensive: 0 in any position field should saturate to 0 rather than
        // panic on `0 - 1`.
        let line = "error|:|src/lib.rs|:|0|:|0|:|0|:|0|:|boom|:|none|:|none";
        let (_, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new("/p")).unwrap();
        assert_eq!(diag.range.start.line, 0);
        assert_eq!(diag.range.start.character, 0);
        assert_eq!(diag.range.end.line, 0);
        assert_eq!(diag.range.end.character, 0);
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_strips_ansi_from_rendered() {
        let line = "error|:|src/lib.rs|:|1|:|1|:|1|:|1|:|raw|:|\u{1b}[31mbright red\u{1b}[0m|:|none";
        let (_, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new("/p")).unwrap();
        // When the rendered slot is non-`none`, it replaces the message and
        // ANSI codes are stripped.
        assert_eq!(diag.message, "bright red");
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_too_few_fields_returns_none() {
        let line = "error|:|/file|:|1|:|2|:|3|:|4|:|message";
        assert_eq!(Bacon::parse_bacon_diagnostic_line(line, Path::new("/p")), None);
    }

    #[test]
    fn test_deduplicate_diagnostics_skips_when_path_does_not_match() {
        let other_uri = str::parse::<Uri>("file:///other.rs").unwrap();
        let target_uri = str::parse::<Uri>("file:///target.rs").unwrap();
        let mut diagnostics = Vec::new();
        let mut seen = HashSet::new();
        let diag = Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "msg".into(),
            ..Diagnostic::default()
        };
        Bacon::deduplicate_diagnostics(other_uri, &target_uri, diag, &mut diagnostics, &mut seen);
        assert!(diagnostics.is_empty(), "diagnostic for a different URI must be dropped");
        assert!(seen.is_empty(), "seen set must not record skipped entries");
    }

    #[test]
    fn test_deduplicate_diagnostics_drops_exact_duplicate() {
        let uri = str::parse::<Uri>("file:///x.rs").unwrap();
        let mut diagnostics = Vec::new();
        let mut seen = HashSet::new();
        let make = || Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "same".into(),
            ..Diagnostic::default()
        };
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, make(), &mut diagnostics, &mut seen);
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, make(), &mut diagnostics, &mut seen);
        assert_eq!(diagnostics.len(), 1, "duplicate should not be re-added");
    }

    #[test]
    fn test_deduplicate_diagnostics_keeps_distinct_severity() {
        let uri = str::parse::<Uri>("file:///x.rs").unwrap();
        let mut diagnostics = Vec::new();
        let mut seen = HashSet::new();
        let mut diag = Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "m".into(),
            ..Diagnostic::default()
        };
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, diag.clone(), &mut diagnostics, &mut seen);
        diag.severity = Some(DiagnosticSeverity::WARNING);
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, diag, &mut diagnostics, &mut seen);
        assert_eq!(diagnostics.len(), 2, "different severity ⇒ different diagnostic");
    }

    #[tokio::test]
    async fn test_find_bacon_locations_finds_nested_files() {
        let tmp = TempDir::new().unwrap();
        // Create root/.bacon-locations and root/sub/sub2/.bacon-locations,
        // plus an unrelated file that must not match.
        let root = tmp.path();
        std::fs::write(root.join(".bacon-locations"), "").unwrap();
        let nested = root.join("sub").join("sub2");
        std::fs::create_dir_all(&nested).unwrap();
        std::fs::write(nested.join(".bacon-locations"), "").unwrap();
        std::fs::write(root.join("sub").join("other.txt"), "").unwrap();

        let mut found = Bacon::find_bacon_locations(root, ".bacon-locations").await.unwrap();
        found.sort();
        assert_eq!(found.len(), 2, "should find both .bacon-locations files");
        assert!(found.iter().all(|p| p.file_name().unwrap() == ".bacon-locations"));
    }

    #[tokio::test]
    async fn test_find_bacon_locations_empty_dir_returns_empty() {
        let tmp = TempDir::new().unwrap();
        let found = Bacon::find_bacon_locations(tmp.path(), ".bacon-locations")
            .await
            .unwrap();
        assert!(found.is_empty());
    }

    #[tokio::test]
    async fn test_find_bacon_locations_missing_root_errors() {
        let tmp = TempDir::new().unwrap();
        let missing = tmp.path().join("does-not-exist");
        let result = Bacon::find_bacon_locations(&missing, ".bacon-locations").await;
        assert!(result.is_err(), "missing root must surface an io error");
    }

    #[tokio::test]
    async fn test_validate_preferences_impl_creates_when_missing_and_requested() {
        let tmp = TempDir::new().unwrap();
        let target = tmp.path().join("nested-prefs.toml");
        // bacon prefs lookup returns one path, file does not exist, create_prefs_file=true.
        let prefs_list = target.to_str().unwrap();
        Bacon::validate_preferences_impl(prefs_list.as_bytes(), true)
            .await
            .expect("should create prefs file when missing");
        assert!(target.exists(), "prefs file should be created");
        // And the freshly-created file must validate cleanly.
        Bacon::validate_preferences_file(&target)
            .await
            .expect("freshly created prefs file should validate");
    }

    #[tokio::test]
    async fn test_validate_preferences_impl_no_create_when_disabled() {
        let tmp = TempDir::new().unwrap();
        let target = tmp.path().join("absent-prefs.toml");
        let prefs_list = target.to_str().unwrap();
        Bacon::validate_preferences_impl(prefs_list.as_bytes(), false)
            .await
            .expect("missing prefs with create=false is not an error");
        assert!(!target.exists());
    }
}
