use std::path::Path;
use std::process::ExitCode;

use fallow_config::OutputFormat;
use fallow_core::git_env::clear_ambient_git_env;
use fallow_core::results::AnalysisResults;

use super::counts::{CheckCounts, DupesCounts, REGRESSION_SCHEMA_VERSION, RegressionBaseline};
use super::outcome::RegressionOutcome;
use super::tolerance::Tolerance;

use crate::error::emit_error;

/// Number of seconds in one day.
const SECS_PER_DAY: u64 = 86_400;

// ── Public API ──────────────────────────────────────────────────

/// Where to save the regression baseline.
#[derive(Clone, Copy)]
pub enum SaveRegressionTarget<'a> {
    /// Don't save.
    None,
    /// Save into the config file (.fallowrc.json / .fallowrc.jsonc / fallow.toml / .fallow.toml).
    Config,
    /// Save to an explicit file path.
    File(&'a Path),
}

/// Options for regression detection.
#[derive(Clone, Copy)]
pub struct RegressionOpts<'a> {
    pub fail_on_regression: bool,
    pub tolerance: Tolerance,
    /// Explicit regression baseline file path (overrides config).
    pub regression_baseline_file: Option<&'a Path>,
    /// Where to save the regression baseline.
    pub save_target: SaveRegressionTarget<'a>,
    /// Whether --changed-since or --workspace is active (makes counts incomparable).
    pub scoped: bool,
    pub quiet: bool,
}

/// Check whether a path is likely gitignored by running `git check-ignore`.
/// Returns `false` if git is unavailable or the check fails (conservative).
fn is_likely_gitignored(path: &Path, root: &Path) -> bool {
    let mut command = std::process::Command::new("git");
    command
        .args(["check-ignore", "-q"])
        .arg(path)
        .current_dir(root);
    clear_ambient_git_env(&mut command);
    command.output().ok().is_some_and(|o| o.status.success())
}

/// Get the current git SHA, if available.
fn current_git_sha(root: &Path) -> Option<String> {
    let mut command = std::process::Command::new("git");
    command.args(["rev-parse", "HEAD"]).current_dir(root);
    clear_ambient_git_env(&mut command);
    command
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
}

/// Save the current analysis results as a regression baseline.
///
/// # Errors
///
/// Returns an error if the baseline cannot be serialized or written to disk.
pub fn save_regression_baseline(
    path: &Path,
    root: &Path,
    check_counts: Option<&CheckCounts>,
    dupes_counts: Option<&DupesCounts>,
    output: OutputFormat,
) -> Result<(), ExitCode> {
    let baseline = RegressionBaseline {
        schema_version: REGRESSION_SCHEMA_VERSION,
        fallow_version: env!("CARGO_PKG_VERSION").to_string(),
        timestamp: chrono_now(),
        git_sha: current_git_sha(root),
        check: check_counts.cloned(),
        dupes: dupes_counts.cloned(),
    };
    let json = serde_json::to_string_pretty(&baseline).map_err(|e| {
        emit_error(
            &format!("failed to serialize regression baseline: {e}"),
            2,
            output,
        )
    })?;
    // Ensure parent directory exists
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    std::fs::write(path, json).map_err(|e| {
        emit_error(
            &format!("failed to save regression baseline: {e}"),
            2,
            output,
        )
    })?;
    // Always print save confirmation — this is a side effect the user must verify,
    // not progress noise that --quiet should suppress.
    eprintln!("Regression baseline saved to {}", path.display());
    // Warn if the saved path appears to be gitignored
    if is_likely_gitignored(path, root) {
        eprintln!(
            "Warning: '{}' may be gitignored. Commit this file so CI can compare against it.",
            path.display()
        );
    }
    Ok(())
}

/// Save regression baseline counts into the project's config file.
///
/// Reads the existing config, adds/updates the `regression.baseline` section,
/// and writes it back. For JSONC files, comments are preserved using a targeted
/// insertion/replacement strategy.
///
/// # Errors
///
/// Returns an error if the config file cannot be read, updated, or written back.
pub fn save_baseline_to_config(
    config_path: &Path,
    counts: &CheckCounts,
    output: OutputFormat,
) -> Result<(), ExitCode> {
    // If the config file doesn't exist yet, create a minimal one
    let content = match std::fs::read_to_string(config_path) {
        Ok(c) => c,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            let is_toml = config_path.extension().is_some_and(|ext| ext == "toml");
            if is_toml {
                String::new()
            } else {
                "{}".to_string()
            }
        }
        Err(e) => {
            return Err(emit_error(
                &format!(
                    "failed to read config file '{}': {e}",
                    config_path.display()
                ),
                2,
                output,
            ));
        }
    };

    let baseline = counts.to_config_baseline();
    let is_toml = config_path.extension().is_some_and(|ext| ext == "toml");

    let updated = if is_toml {
        Ok(update_toml_regression(&content, &baseline))
    } else {
        update_json_regression(&content, &baseline)
    }
    .map_err(|e| {
        emit_error(
            &format!(
                "failed to update config file '{}': {e}",
                config_path.display()
            ),
            2,
            output,
        )
    })?;

    std::fs::write(config_path, updated).map_err(|e| {
        emit_error(
            &format!(
                "failed to write config file '{}': {e}",
                config_path.display()
            ),
            2,
            output,
        )
    })?;

    eprintln!(
        "Regression baseline saved to {} (regression.baseline section)",
        config_path.display()
    );
    Ok(())
}

/// Update a JSONC config file with regression baseline, preserving comments.
/// Find a JSON key in content, skipping `//` line comments and `/* */` block comments.
/// Returns the byte offset of the opening `"` of the key.
fn find_json_key(content: &str, key: &str) -> Option<usize> {
    let needle = format!("\"{key}\"");
    let mut search_from = 0;
    while let Some(pos) = content[search_from..].find(&needle) {
        let abs_pos = search_from + pos;
        // Check if this match is inside a // comment line
        let line_start = content[..abs_pos].rfind('\n').map_or(0, |i| i + 1);
        let line_prefix = content[line_start..abs_pos].trim_start();
        if line_prefix.starts_with("//") {
            search_from = abs_pos + needle.len();
            continue;
        }
        // Check if inside a /* */ block comment
        let before = &content[..abs_pos];
        let last_open = before.rfind("/*");
        let last_close = before.rfind("*/");
        if let Some(open_pos) = last_open
            && last_close.is_none_or(|close_pos| close_pos < open_pos)
        {
            search_from = abs_pos + needle.len();
            continue;
        }
        return Some(abs_pos);
    }
    None
}

fn update_json_regression(
    content: &str,
    baseline: &fallow_config::RegressionBaseline,
) -> Result<String, String> {
    let baseline_json =
        serde_json::to_string_pretty(baseline).map_err(|e| format!("serialization error: {e}"))?;

    // Indent the baseline JSON by 4 spaces (nested inside "regression": { "baseline": ... })
    let indented: String = baseline_json
        .lines()
        .enumerate()
        .map(|(i, line)| {
            if i == 0 {
                format!("    {line}")
            } else {
                format!("\n    {line}")
            }
        })
        .collect();

    let regression_block = format!("  \"regression\": {{\n    \"baseline\": {indented}\n  }}");

    // Check if "regression" key already exists — replace it.
    // Only match "regression" that appears as a JSON key (preceded by whitespace or line start),
    // not inside comments or string values.
    if let Some(start) = find_json_key(content, "regression") {
        let after_key = &content[start..];
        if let Some(brace_start) = after_key.find('{') {
            let abs_brace = start + brace_start;
            let mut depth = 0;
            let mut end = abs_brace;
            let mut found_close = false;
            for (i, ch) in content[abs_brace..].char_indices() {
                match ch {
                    '{' => depth += 1,
                    '}' => {
                        depth -= 1;
                        if depth == 0 {
                            end = abs_brace + i + 1;
                            found_close = true;
                            break;
                        }
                    }
                    _ => {}
                }
            }
            if !found_close {
                return Err("malformed JSON: unmatched brace in regression object".to_string());
            }
            let mut result = String::new();
            result.push_str(&content[..start]);
            result.push_str(&regression_block[2..]); // skip leading "  " — reuse original indent
            result.push_str(&content[end..]);
            return Ok(result);
        }
    }

    // No existing regression key — insert before the last `}`
    if let Some(last_brace) = content.rfind('}') {
        // Find the last non-whitespace character before the closing brace
        let before_brace = content[..last_brace].trim_end();
        let needs_comma = !before_brace.ends_with('{') && !before_brace.ends_with(',');

        let mut result = String::new();
        result.push_str(before_brace);
        if needs_comma {
            result.push(',');
        }
        result.push('\n');
        result.push_str(&regression_block);
        result.push('\n');
        result.push_str(&content[last_brace..]);
        Ok(result)
    } else {
        Err("config file has no closing brace".to_string())
    }
}

/// Update a TOML config file with regression baseline.
fn update_toml_regression(content: &str, baseline: &fallow_config::RegressionBaseline) -> String {
    use std::fmt::Write;
    // Build the TOML section
    let mut section = String::from("[regression.baseline]\n");
    let _ = writeln!(section, "totalIssues = {}", baseline.total_issues);
    let _ = writeln!(section, "unusedFiles = {}", baseline.unused_files);
    let _ = writeln!(section, "unusedExports = {}", baseline.unused_exports);
    let _ = writeln!(section, "unusedTypes = {}", baseline.unused_types);
    let _ = writeln!(
        section,
        "unusedDependencies = {}",
        baseline.unused_dependencies
    );
    let _ = writeln!(
        section,
        "unusedDevDependencies = {}",
        baseline.unused_dev_dependencies
    );
    let _ = writeln!(
        section,
        "unusedOptionalDependencies = {}",
        baseline.unused_optional_dependencies
    );
    let _ = writeln!(
        section,
        "unusedEnumMembers = {}",
        baseline.unused_enum_members
    );
    let _ = writeln!(
        section,
        "unusedClassMembers = {}",
        baseline.unused_class_members
    );
    let _ = writeln!(
        section,
        "unresolvedImports = {}",
        baseline.unresolved_imports
    );
    let _ = writeln!(
        section,
        "unlistedDependencies = {}",
        baseline.unlisted_dependencies
    );
    let _ = writeln!(section, "duplicateExports = {}", baseline.duplicate_exports);
    let _ = writeln!(
        section,
        "circularDependencies = {}",
        baseline.circular_dependencies
    );
    let _ = writeln!(
        section,
        "typeOnlyDependencies = {}",
        baseline.type_only_dependencies
    );
    let _ = writeln!(
        section,
        "testOnlyDependencies = {}",
        baseline.test_only_dependencies
    );

    // Check if [regression.baseline] already exists — replace it
    if let Some(start) = content.find("[regression.baseline]") {
        // Find the next section header or end of file
        let after = &content[start + "[regression.baseline]".len()..];
        let end_offset = after.find("\n[").map_or(content.len(), |i| {
            start + "[regression.baseline]".len() + i + 1
        });

        let mut result = String::new();
        result.push_str(&content[..start]);
        result.push_str(&section);
        if end_offset < content.len() {
            result.push_str(&content[end_offset..]);
        }
        result
    } else {
        // Append the section
        let mut result = content.to_string();
        if !result.ends_with('\n') {
            result.push('\n');
        }
        result.push('\n');
        result.push_str(&section);
        result
    }
}

/// Load a regression baseline from disk.
///
/// # Errors
///
/// Returns an error if the file does not exist, cannot be read, or contains invalid JSON.
pub fn load_regression_baseline(path: &Path) -> Result<RegressionBaseline, ExitCode> {
    let content = std::fs::read_to_string(path).map_err(|e| {
        if e.kind() == std::io::ErrorKind::NotFound {
            eprintln!(
                "Error: no regression baseline found at '{}'.\n\
                 Run with --save-regression-baseline on your main branch to create one.",
                path.display()
            );
        } else {
            eprintln!(
                "Error: failed to read regression baseline '{}': {e}",
                path.display()
            );
        }
        ExitCode::from(2)
    })?;
    serde_json::from_str(&content).map_err(|e| {
        eprintln!(
            "Error: failed to parse regression baseline '{}': {e}",
            path.display()
        );
        ExitCode::from(2)
    })
}

/// Compare current check results against a regression baseline.
///
/// Resolution order for the baseline:
/// 1. Explicit file via `--regression-baseline <PATH>`
/// 2. Config-embedded `regression.baseline` section
/// 3. Error with actionable message
///
/// # Errors
///
/// Returns an error if the baseline file cannot be loaded, is missing check data,
/// or no baseline source is available.
pub fn compare_check_regression(
    results: &AnalysisResults,
    opts: &RegressionOpts<'_>,
    config_baseline: Option<&fallow_config::RegressionBaseline>,
) -> Result<Option<RegressionOutcome>, ExitCode> {
    if !opts.fail_on_regression {
        return Ok(None);
    }

    // Skip if results are scoped (counts not comparable to full-project baseline)
    if opts.scoped {
        let reason = "--changed-since or --workspace is active; regression check skipped \
                      (counts not comparable to full-project baseline)";
        if !opts.quiet {
            eprintln!("Warning: {reason}");
        }
        return Ok(Some(RegressionOutcome::Skipped { reason }));
    }

    // Resolution order: explicit file > config section > error
    let baseline_counts: CheckCounts = if let Some(baseline_path) = opts.regression_baseline_file {
        // Explicit --regression-baseline <PATH>: load from file
        let baseline = load_regression_baseline(baseline_path)?;
        let Some(counts) = baseline.check else {
            eprintln!(
                "Error: regression baseline '{}' has no check data",
                baseline_path.display()
            );
            return Err(ExitCode::from(2));
        };
        counts
    } else if let Some(config_baseline) = config_baseline {
        // Config-embedded baseline: read from .fallowrc.json / .fallowrc.jsonc / fallow.toml / .fallow.toml
        CheckCounts::from_config_baseline(config_baseline)
    } else {
        eprintln!(
            "Error: no regression baseline found.\n\
             Either add a `regression.baseline` section to your config file\n\
             (run with --save-regression-baseline to generate it),\n\
             or provide an explicit file via --regression-baseline <PATH>."
        );
        return Err(ExitCode::from(2));
    };

    let current_total = results.total_issues();
    let baseline_total = baseline_counts.total_issues;

    if opts.tolerance.exceeded(baseline_total, current_total) {
        let current_counts = CheckCounts::from_results(results);
        let type_deltas = baseline_counts.deltas(&current_counts);
        Ok(Some(RegressionOutcome::Exceeded {
            baseline_total,
            current_total,
            tolerance: opts.tolerance,
            type_deltas,
        }))
    } else {
        Ok(Some(RegressionOutcome::Pass {
            baseline_total,
            current_total,
        }))
    }
}

/// ISO 8601 UTC timestamp without external dependencies.
fn chrono_now() -> String {
    let duration = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    let secs = duration.as_secs();
    // Manual UTC decomposition — avoids chrono dependency
    let days = secs / SECS_PER_DAY;
    let time_secs = secs % SECS_PER_DAY;
    let hours = time_secs / 3600;
    let minutes = (time_secs % 3600) / 60;
    let seconds = time_secs % 60;
    // Days since epoch to Y-M-D (civil date algorithm)
    let z = days + 719_468;
    let era = z / 146_097;
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    format!("{y:04}-{m:02}-{d:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
}

#[cfg(test)]
mod tests {
    use super::*;
    use fallow_core::results::*;
    use std::path::PathBuf;

    // ── update_json_regression ──────────────────────────────────────

    fn sample_baseline() -> fallow_config::RegressionBaseline {
        fallow_config::RegressionBaseline {
            total_issues: 5,
            unused_files: 2,
            ..Default::default()
        }
    }

    #[test]
    fn json_insert_into_empty_object() {
        let result = update_json_regression("{}", &sample_baseline()).unwrap();
        assert!(result.contains("\"regression\""));
        assert!(result.contains("\"totalIssues\": 5"));
        // Should be valid JSON
        serde_json::from_str::<serde_json::Value>(&result).unwrap();
    }

    #[test]
    fn json_insert_into_existing_config() {
        let config = r#"{
  "entry": ["src/main.ts"],
  "production": true
}"#;
        let result = update_json_regression(config, &sample_baseline()).unwrap();
        assert!(result.contains("\"regression\""));
        assert!(result.contains("\"entry\""));
        serde_json::from_str::<serde_json::Value>(&result).unwrap();
    }

    #[test]
    fn json_replace_existing_regression() {
        let config = r#"{
  "entry": ["src/main.ts"],
  "regression": {
    "baseline": {
      "totalIssues": 99
    }
  }
}"#;
        let result = update_json_regression(config, &sample_baseline()).unwrap();
        // Old value replaced
        assert!(!result.contains("99"));
        assert!(result.contains("\"totalIssues\": 5"));
        serde_json::from_str::<serde_json::Value>(&result).unwrap();
    }

    #[test]
    fn json_skips_regression_in_comment() {
        let config = "{\n  // See \"regression\" docs\n  \"entry\": []\n}";
        let result = update_json_regression(config, &sample_baseline()).unwrap();
        // Should insert new regression, not try to replace the comment
        assert!(result.contains("\"regression\":"));
        assert!(result.contains("\"entry\""));
    }

    #[test]
    fn json_malformed_brace_returns_error() {
        // regression key exists but no matching closing brace
        let config = r#"{ "regression": { "baseline": { "totalIssues": 1 }"#;
        let result = update_json_regression(config, &sample_baseline());
        assert!(result.is_err());
    }

    // ── update_toml_regression ──────────────────────────────────────

    #[test]
    fn toml_insert_into_empty() {
        let result = update_toml_regression("", &sample_baseline());
        assert!(result.contains("[regression.baseline]"));
        assert!(result.contains("totalIssues = 5"));
    }

    #[test]
    fn toml_insert_after_existing_content() {
        let config = "[rules]\nunused-files = \"warn\"\n";
        let result = update_toml_regression(config, &sample_baseline());
        assert!(result.contains("[rules]"));
        assert!(result.contains("[regression.baseline]"));
        assert!(result.contains("totalIssues = 5"));
    }

    #[test]
    fn toml_replace_existing_section() {
        let config =
            "[regression.baseline]\ntotalIssues = 99\n\n[rules]\nunused-files = \"warn\"\n";
        let result = update_toml_regression(config, &sample_baseline());
        assert!(!result.contains("99"));
        assert!(result.contains("totalIssues = 5"));
        assert!(result.contains("[rules]"));
    }

    // ── find_json_key ───────────────────────────────────────────────

    #[test]
    fn find_json_key_basic() {
        assert_eq!(find_json_key(r#"{"foo": 1}"#, "foo"), Some(1));
    }

    #[test]
    fn find_json_key_skips_comment() {
        let content = "{\n  // \"foo\" is important\n  \"bar\": 1\n}";
        assert_eq!(find_json_key(content, "foo"), None);
        assert!(find_json_key(content, "bar").is_some());
    }

    #[test]
    fn find_json_key_not_found() {
        assert_eq!(find_json_key("{}", "missing"), None);
    }

    #[test]
    fn find_json_key_skips_block_comment() {
        let content = "{\n  /* \"foo\": old value */\n  \"foo\": 1\n}";
        // Should find the real key, not the one inside /* */
        let pos = find_json_key(content, "foo").unwrap();
        assert!(content[pos..].starts_with("\"foo\": 1"));
    }

    // ── chrono_now ─────────────────────────────────────────────────

    #[test]
    fn chrono_now_format() {
        let ts = chrono_now();
        // Should be ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
        assert_eq!(ts.len(), 20);
        assert!(ts.ends_with('Z'));
        assert_eq!(&ts[4..5], "-");
        assert_eq!(&ts[7..8], "-");
        assert_eq!(&ts[10..11], "T");
        assert_eq!(&ts[13..14], ":");
        assert_eq!(&ts[16..17], ":");
    }

    // ── save/load roundtrip ────────────────────────────────────────

    #[test]
    fn save_load_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("regression-baseline.json");
        let counts = CheckCounts {
            total_issues: 15,
            unused_files: 3,
            unused_exports: 5,
            unused_types: 2,
            unused_dependencies: 1,
            unused_dev_dependencies: 1,
            unused_optional_dependencies: 0,
            unused_enum_members: 1,
            unused_class_members: 0,
            unresolved_imports: 1,
            unlisted_dependencies: 0,
            duplicate_exports: 1,
            circular_dependencies: 0,
            type_only_dependencies: 0,
            test_only_dependencies: 0,
            boundary_violations: 0,
        };
        let dupes = DupesCounts {
            clone_groups: 4,
            duplication_percentage: 2.5,
        };

        save_regression_baseline(
            &path,
            dir.path(),
            Some(&counts),
            Some(&dupes),
            OutputFormat::Human,
        )
        .unwrap();
        let loaded = load_regression_baseline(&path).unwrap();

        assert_eq!(loaded.schema_version, REGRESSION_SCHEMA_VERSION);
        let check = loaded.check.unwrap();
        assert_eq!(check.total_issues, 15);
        assert_eq!(check.unused_files, 3);
        assert_eq!(check.unused_exports, 5);
        assert_eq!(check.unused_types, 2);
        assert_eq!(check.unused_dependencies, 1);
        assert_eq!(check.unresolved_imports, 1);
        assert_eq!(check.duplicate_exports, 1);
        let dupes = loaded.dupes.unwrap();
        assert_eq!(dupes.clone_groups, 4);
        assert!((dupes.duplication_percentage - 2.5).abs() < f64::EPSILON);
    }

    #[test]
    fn save_load_roundtrip_check_only() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("regression-baseline.json");
        let counts = CheckCounts {
            total_issues: 5,
            unused_files: 5,
            ..CheckCounts::from_config_baseline(&fallow_config::RegressionBaseline::default())
        };

        save_regression_baseline(&path, dir.path(), Some(&counts), None, OutputFormat::Human)
            .unwrap();
        let loaded = load_regression_baseline(&path).unwrap();

        assert!(loaded.check.is_some());
        assert!(loaded.dupes.is_none());
        assert_eq!(loaded.check.unwrap().unused_files, 5);
    }

    #[test]
    fn save_creates_parent_directories() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("nested").join("dir").join("baseline.json");
        let counts = CheckCounts {
            total_issues: 1,
            unused_files: 1,
            ..CheckCounts::from_config_baseline(&fallow_config::RegressionBaseline::default())
        };

        save_regression_baseline(&path, dir.path(), Some(&counts), None, OutputFormat::Human)
            .unwrap();
        assert!(path.exists());
    }

    #[test]
    fn load_nonexistent_file_returns_error() {
        let result = load_regression_baseline(Path::new("/tmp/nonexistent-baseline-12345.json"));
        assert!(result.is_err());
    }

    #[test]
    fn load_invalid_json_returns_error() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("bad.json");
        std::fs::write(&path, "not valid json {{{").unwrap();
        let result = load_regression_baseline(&path);
        assert!(result.is_err());
    }

    // ── save_baseline_to_config ────────────────────────────────────

    #[test]
    fn save_baseline_to_json_config() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join(".fallowrc.json");
        std::fs::write(&config_path, r#"{"entry": ["src/main.ts"]}"#).unwrap();

        let counts = CheckCounts {
            total_issues: 7,
            unused_files: 3,
            unused_exports: 4,
            ..CheckCounts::from_config_baseline(&fallow_config::RegressionBaseline::default())
        };
        save_baseline_to_config(&config_path, &counts, OutputFormat::Human).unwrap();

        let content = std::fs::read_to_string(&config_path).unwrap();
        assert!(content.contains("\"regression\""));
        assert!(content.contains("\"totalIssues\": 7"));
        // Should still be valid JSON
        serde_json::from_str::<serde_json::Value>(&content).unwrap();
    }

    #[test]
    fn save_baseline_to_toml_config() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join("fallow.toml");
        std::fs::write(&config_path, "[rules]\nunused-files = \"warn\"\n").unwrap();

        let counts = CheckCounts {
            total_issues: 7,
            unused_files: 3,
            unused_exports: 4,
            ..CheckCounts::from_config_baseline(&fallow_config::RegressionBaseline::default())
        };
        save_baseline_to_config(&config_path, &counts, OutputFormat::Human).unwrap();

        let content = std::fs::read_to_string(&config_path).unwrap();
        assert!(content.contains("[regression.baseline]"));
        assert!(content.contains("totalIssues = 7"));
        assert!(content.contains("[rules]"));
    }

    #[test]
    fn save_baseline_to_nonexistent_json_config() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join(".fallowrc.json");
        // File doesn't exist — should create it from scratch

        let counts = CheckCounts {
            total_issues: 1,
            unused_files: 1,
            ..CheckCounts::from_config_baseline(&fallow_config::RegressionBaseline::default())
        };
        save_baseline_to_config(&config_path, &counts, OutputFormat::Human).unwrap();

        let content = std::fs::read_to_string(&config_path).unwrap();
        assert!(content.contains("\"regression\""));
        serde_json::from_str::<serde_json::Value>(&content).unwrap();
    }

    #[test]
    fn save_baseline_to_nonexistent_toml_config() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join("fallow.toml");

        let counts = CheckCounts {
            total_issues: 0,
            ..CheckCounts::from_config_baseline(&fallow_config::RegressionBaseline::default())
        };
        save_baseline_to_config(&config_path, &counts, OutputFormat::Human).unwrap();

        let content = std::fs::read_to_string(&config_path).unwrap();
        assert!(content.contains("[regression.baseline]"));
        assert!(content.contains("totalIssues = 0"));
    }

    // ── update_json_regression edge cases ──────────────────────────

    #[test]
    fn json_insert_with_trailing_comma() {
        let config = r#"{
  "entry": ["src/main.ts"],
}"#;
        // Trailing comma — our insertion should still produce reasonable output
        let result = update_json_regression(config, &sample_baseline()).unwrap();
        assert!(result.contains("\"regression\""));
    }

    #[test]
    fn json_no_closing_brace_returns_error() {
        let result = update_json_regression("", &sample_baseline());
        assert!(result.is_err());
    }

    #[test]
    fn json_nested_regression_object_replaced_correctly() {
        let config = r#"{
  "regression": {
    "baseline": {
      "totalIssues": 99,
      "unusedFiles": 10
    },
    "tolerance": "5%"
  },
  "entry": ["src/main.ts"]
}"#;
        let result = update_json_regression(config, &sample_baseline()).unwrap();
        assert!(!result.contains("99"));
        assert!(result.contains("\"totalIssues\": 5"));
        assert!(result.contains("\"entry\""));
    }

    // ── update_toml_regression edge cases ──────────────────────────

    #[test]
    fn toml_content_without_trailing_newline() {
        let config = "[rules]\nunused-files = \"warn\"";
        let result = update_toml_regression(config, &sample_baseline());
        assert!(result.contains("[regression.baseline]"));
        assert!(result.contains("[rules]"));
    }

    #[test]
    fn toml_replace_section_not_at_end() {
        let config = "[regression.baseline]\ntotalIssues = 99\nunusedFiles = 10\n\n[rules]\nunused-files = \"warn\"\n";
        let result = update_toml_regression(config, &sample_baseline());
        assert!(!result.contains("99"));
        assert!(result.contains("totalIssues = 5"));
        assert!(result.contains("[rules]"));
        assert!(result.contains("unused-files = \"warn\""));
    }

    #[test]
    fn toml_replace_section_at_end() {
        let config =
            "[rules]\nunused-files = \"warn\"\n\n[regression.baseline]\ntotalIssues = 99\n";
        let result = update_toml_regression(config, &sample_baseline());
        assert!(!result.contains("99"));
        assert!(result.contains("totalIssues = 5"));
        assert!(result.contains("[rules]"));
    }

    // ── find_json_key edge cases ────────────────────────────────────

    #[test]
    fn find_json_key_multiple_same_keys() {
        // Returns the first occurrence
        let content = r#"{"foo": 1, "bar": {"foo": 2}}"#;
        let pos = find_json_key(content, "foo").unwrap();
        assert_eq!(pos, 1);
    }

    #[test]
    fn find_json_key_in_nested_comment_then_real() {
        let content = "{\n  // \"entry\": old\n  /* \"entry\": also old */\n  \"entry\": []\n}";
        let pos = find_json_key(content, "entry").unwrap();
        assert!(content[pos..].starts_with("\"entry\": []"));
    }

    // ── compare_check_regression ────────────────────────────────────

    fn make_opts(
        fail: bool,
        tolerance: Tolerance,
        scoped: bool,
        baseline_file: Option<&Path>,
    ) -> RegressionOpts<'_> {
        RegressionOpts {
            fail_on_regression: fail,
            tolerance,
            regression_baseline_file: baseline_file,
            save_target: SaveRegressionTarget::None,
            scoped,
            quiet: true,
        }
    }

    #[test]
    fn compare_returns_none_when_disabled() {
        let results = AnalysisResults::default();
        let opts = make_opts(false, Tolerance::Absolute(0), false, None);
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 5,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();
        assert!(outcome.is_none());
    }

    #[test]
    fn compare_returns_skipped_when_scoped() {
        let results = AnalysisResults::default();
        let opts = make_opts(true, Tolerance::Absolute(0), true, None);
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 5,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();
        assert!(matches!(outcome, Some(RegressionOutcome::Skipped { .. })));
    }

    #[test]
    fn compare_pass_with_config_baseline() {
        let results = AnalysisResults::default(); // 0 issues
        let opts = make_opts(true, Tolerance::Absolute(0), false, None);
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 0,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();
        match outcome {
            Some(RegressionOutcome::Pass {
                baseline_total,
                current_total,
            }) => {
                assert_eq!(baseline_total, 0);
                assert_eq!(current_total, 0);
            }
            other => panic!("expected Pass, got {other:?}"),
        }
    }

    #[test]
    fn compare_exceeded_with_config_baseline() {
        let mut results = AnalysisResults::default();
        results.unused_files.push(UnusedFile {
            path: PathBuf::from("a.ts"),
        });
        results.unused_files.push(UnusedFile {
            path: PathBuf::from("b.ts"),
        });
        let opts = make_opts(true, Tolerance::Absolute(0), false, None);
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 0,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();
        match outcome {
            Some(RegressionOutcome::Exceeded {
                baseline_total,
                current_total,
                ..
            }) => {
                assert_eq!(baseline_total, 0);
                assert_eq!(current_total, 2);
            }
            other => panic!("expected Exceeded, got {other:?}"),
        }
    }

    #[test]
    fn compare_pass_within_tolerance() {
        let mut results = AnalysisResults::default();
        results.unused_files.push(UnusedFile {
            path: PathBuf::from("a.ts"),
        });
        let opts = make_opts(true, Tolerance::Absolute(5), false, None);
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 0,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();
        assert!(matches!(outcome, Some(RegressionOutcome::Pass { .. })));
    }

    #[test]
    fn compare_improvement_is_pass() {
        // Current has fewer issues than baseline
        let results = AnalysisResults::default(); // 0 issues
        let opts = make_opts(true, Tolerance::Absolute(0), false, None);
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 10,
            unused_files: 5,
            unused_exports: 5,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();
        match outcome {
            Some(RegressionOutcome::Pass {
                baseline_total,
                current_total,
            }) => {
                assert_eq!(baseline_total, 10);
                assert_eq!(current_total, 0);
            }
            other => panic!("expected Pass, got {other:?}"),
        }
    }

    #[test]
    fn compare_with_file_baseline() {
        let dir = tempfile::tempdir().unwrap();
        let baseline_path = dir.path().join("baseline.json");

        // Save a baseline to file
        let counts = CheckCounts {
            total_issues: 5,
            unused_files: 5,
            ..CheckCounts::from_config_baseline(&fallow_config::RegressionBaseline::default())
        };
        save_regression_baseline(
            &baseline_path,
            dir.path(),
            Some(&counts),
            None,
            OutputFormat::Human,
        )
        .unwrap();

        // Compare with empty results -> pass (improvement)
        let results = AnalysisResults::default();
        let opts = make_opts(true, Tolerance::Absolute(0), false, Some(&baseline_path));
        let outcome = compare_check_regression(&results, &opts, None).unwrap();
        assert!(matches!(outcome, Some(RegressionOutcome::Pass { .. })));
    }

    #[test]
    fn compare_file_baseline_missing_check_data_returns_error() {
        let dir = tempfile::tempdir().unwrap();
        let baseline_path = dir.path().join("baseline.json");

        // Save a baseline with no check data (dupes only)
        save_regression_baseline(
            &baseline_path,
            dir.path(),
            None,
            Some(&DupesCounts {
                clone_groups: 1,
                duplication_percentage: 1.0,
            }),
            OutputFormat::Human,
        )
        .unwrap();

        let results = AnalysisResults::default();
        let opts = make_opts(true, Tolerance::Absolute(0), false, Some(&baseline_path));
        let outcome = compare_check_regression(&results, &opts, None);
        assert!(outcome.is_err());
    }

    #[test]
    fn compare_no_baseline_source_returns_error() {
        let results = AnalysisResults::default();
        let opts = make_opts(true, Tolerance::Absolute(0), false, None);
        let outcome = compare_check_regression(&results, &opts, None);
        assert!(outcome.is_err());
    }

    #[test]
    fn compare_exceeded_includes_type_deltas() {
        let mut results = AnalysisResults::default();
        results.unused_files.push(UnusedFile {
            path: PathBuf::from("a.ts"),
        });
        results.unused_files.push(UnusedFile {
            path: PathBuf::from("b.ts"),
        });
        results.unused_exports.push(UnusedExport {
            path: PathBuf::from("c.ts"),
            export_name: "foo".into(),
            is_type_only: false,
            line: 1,
            col: 0,
            span_start: 0,
            is_re_export: false,
        });

        let opts = make_opts(true, Tolerance::Absolute(0), false, None);
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 0,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();

        match outcome {
            Some(RegressionOutcome::Exceeded { type_deltas, .. }) => {
                assert!(type_deltas.contains(&("unused_files", 2)));
                assert!(type_deltas.contains(&("unused_exports", 1)));
            }
            other => panic!("expected Exceeded, got {other:?}"),
        }
    }

    #[test]
    fn compare_with_percentage_tolerance() {
        let mut results = AnalysisResults::default();
        // Add 1 issue
        results.unused_files.push(UnusedFile {
            path: PathBuf::from("a.ts"),
        });

        let opts = make_opts(true, Tolerance::Percentage(50.0), false, None);
        // baseline=10, 50% of 10 = 5, delta=1-10=-9 (improvement, should pass)
        // Wait, total_issues in config is the baseline for comparison.
        // results has 1 issue, baseline has 10 -> improvement -> pass
        let config_baseline = fallow_config::RegressionBaseline {
            total_issues: 10,
            unused_files: 10,
            ..Default::default()
        };
        let outcome = compare_check_regression(&results, &opts, Some(&config_baseline)).unwrap();
        assert!(matches!(outcome, Some(RegressionOutcome::Pass { .. })));
    }
}
