// crates/atomcode-tuix/src/platform.rs
//
// Small cross-platform helpers. Every `$HOME`, `/tmp`, and shell-command
// decision in tuix routes through this module so Windows doesn't have
// to special-case each caller. Keeps `event_loop` and friends free of
// `#[cfg(unix)]` clutter.

use std::path::PathBuf;

/// User home directory, or `None` if it can't be determined.
///
/// - macOS / Linux: `$HOME`, falling back to `getpwuid_r`
/// - Windows: `%USERPROFILE%` (via `dirs`)
///
/// This function accounts for sudo scenarios where $HOME might be /root
/// but we want the actual user's home directory.
///
/// Prefer this over `std::env::var("HOME")` — the latter returns `None`
/// on stock Windows and sends us down a fallback path that then hits
/// `/tmp` (also nonexistent on Windows).
pub fn home_dir() -> Option<PathBuf> {
    atomcode_core::tool::real_home_dir()
}

/// Replace a leading `$HOME` in `path` with `~`. Returns `path`
/// unchanged if it doesn't start under home, or if home isn't known.
///
/// On Windows, `std::fs::canonicalize` returns paths with the verbatim
/// (`\\?\`) or UNC-verbatim (`\\?\UNC\`) prefix; both are stripped here
/// before any other processing so the status row never shows the raw
/// extended-length form (e.g. `\\?\D:\wwwroot\xingyu-api`).
///
/// On Windows the comparison is case-insensitive because
/// `canonicalize` may normalise the path to a different casing than
/// `dirs::home_dir()` returns (e.g. `C:\Users\…` vs `c:\users\…`).
///
/// Used by the status row + welcome page to keep long paths readable.
pub fn collapse_home(path: &str) -> String {
    collapse_home_with(path, home_dir().as_deref())
}

/// Implementation of [`collapse_home`] that accepts an explicit home
/// directory. This lets unit tests verify Windows-specific logic on any
/// platform without needing `cfg!(windows)`.
fn collapse_home_with(path: &str, home: Option<&std::path::Path>) -> String {
    let path: std::borrow::Cow<'_, str> = if let Some(rest) = path.strip_prefix(r"\\?\UNC\") {
        std::borrow::Cow::Owned(format!(r"\\{}", rest))
    } else if let Some(rest) = path.strip_prefix(r"\\?\") {
        std::borrow::Cow::Borrowed(rest)
    } else {
        std::borrow::Cow::Borrowed(path)
    };
    if let Some(home) = home {
        let home_str = home.to_string_lossy();
        if !home_str.is_empty() {
            // On Windows the filesystem is case-insensitive.  `canonicalize`
            // may return a path whose drive-letter / user-directory casing
            // differs from `dirs::home_dir()` (e.g. `C:\USERS\alice\…` vs
            // `C:\users\alice`).  Compare case-insensitively on Windows so
            // the prefix is reliably stripped and the status row shows
            // `~/atomcode` instead of the raw `C:\USERS\alice\atomcode`.
            let rest = if cfg!(windows) {
                // Case-insensitive prefix match on Windows: compare the
                // lowercased forms, but slice the *original* path at
                // `home_str.len()` — that offset is where the remainder
                // starts regardless of casing differences.  Using the
                // lowercase remainder's length (old code: `s.len()`) was
                // wrong because it equals `path.len() - home_str.len()`,
                // so `path[s.len()..]` took the *last* `home_str.len()`
                // characters instead of everything after the home prefix.
                if path.to_lowercase().starts_with(&home_str.to_lowercase()) {
                    Some(path[home_str.len()..].to_string())
                } else {
                    None
                }
            } else {
                path.strip_prefix(&*home_str).map(|s| s.to_string())
            };
            if let Some(rest) = rest {
                if rest.is_empty() {
                    return "~".to_string();
                }
                // Always emit forward slashes after `~` — the `~`
                // shortcut is a Unix shell convention and `~\foo`
                // (the Windows-native form) matches no actual shell:
                // PowerShell / cmd don't expand `~`, Git Bash / WSL
                // use `~/`. Mixed `~\…` reads as a typo. Normalising
                // here keeps every status-row path consistent with
                // the rest of the TUI (skill paths, command help,
                // docs) which all reference `~/.atomcode/...`.
                return format!("~{}", rest.replace('\\', "/"));
            }
        }
    }
    path.into_owned()
}

/// Path for the per-user input history file.
/// Uses ATOMCODE_HOME if set, otherwise falls back to home directory.
pub fn history_path() -> PathBuf {
    atomcode_core::config::Config::config_dir().join("history")
}

/// Path for the per-user image attachment cache. Sibling to `history_path()`.
/// Used by the History image-attachment feature to persist pasted bytes so
/// up-arrow recall can rehydrate them on a future submit.
pub fn image_cache_dir() -> PathBuf {
    atomcode_core::config::Config::config_dir().join("image-cache")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn collapse_home_rewrites_prefix() {
        if let Some(home) = home_dir() {
            let home_str = home.to_string_lossy().to_string();
            let nested = format!("{}/project/foo", home_str);
            let got = collapse_home(&nested);
            assert_eq!(got, "~/project/foo");
        }
    }

    /// Windows `home_str` uses `\`, and the input path strip_prefix
    /// leaves a backslash-prefixed remainder. The collapse output must
    /// still normalise to `~/foo`, not the hybrid `~\foo` form.
    #[test]
    fn collapse_home_emits_forward_slash_on_windows_separators() {
        if let Some(home) = home_dir() {
            let home_str = home.to_string_lossy().to_string();
            // Build the path with the same separator home_str uses, so
            // strip_prefix succeeds on both Unix and Windows test runs.
            let sep = if home_str.contains('\\') { '\\' } else { '/' };
            let nested = format!("{home_str}{sep}atomcode{sep}src");
            let got = collapse_home(&nested);
            assert_eq!(got, "~/atomcode/src");
            assert!(!got.contains('\\'), "must not retain backslashes: {got}");
        }
    }

    #[test]
    fn collapse_home_returns_unchanged_for_unrelated_path() {
        assert_eq!(collapse_home("/opt/tool/bar"), "/opt/tool/bar");
    }

    #[test]
    fn collapse_home_strips_windows_verbatim_prefix() {
        // The Windows extended-length / verbatim prefix is never
        // user-facing; strip it before display.
        assert_eq!(
            collapse_home(r"\\?\D:\wwwroot\xingyu-api"),
            r"D:\wwwroot\xingyu-api"
        );
    }

    #[test]
    fn collapse_home_strips_windows_verbatim_unc_prefix() {
        assert_eq!(
            collapse_home(r"\\?\UNC\server\share\proj"),
            r"\\server\share\proj"
        );
    }

    /// Simulates the exact scenario reported in issue #356: on Windows 10
    /// the user's home directory is `C:\Users\username` (as returned by
    /// `dirs::home_dir()`) but `canonicalize` produces
    /// `\\?\C:\Users\username\atomcode` — note the `\\?\` prefix that
    /// `collapse_home` already strips, **and** a potential case mismatch
    /// between the two paths.  On Windows the filesystem is
    /// case-insensitive so `C:\Users` and `c:\users` refer to the same
    /// directory, but `str::strip_prefix` is case-sensitive.  Without
    /// case-insensitive comparison the prefix is not matched and the raw
    /// path is shown instead of `~/atomcode`.
    #[test]
    fn collapse_home_windows_case_insensitive_match() {
        // Directly call `collapse_home_with` with synthetic paths so the
        // test works on every platform (macOS, Linux, CI).  We simulate
        // the Windows scenario: home_dir returns `C:\Users\username` but
        // canonicalize gives us a different casing.
        let home = std::path::Path::new(r"C:\Users\username");

        // Exact case — should always work
        assert_eq!(
            collapse_home_with(r"C:\Users\username\atomcode", Some(home)),
            "~/atomcode"
        );

        // Home dir uses `C:\Users\username`, canonicalize returns
        // `C:\USERS\username` — must still collapse to `~/atomcode`.
        // On non-Windows cfg!(windows) is false so this test verifies
        // the case-sensitive path; the Windows-specific test below
        // covers the case-insensitive branch.
        if cfg!(windows) {
            assert_eq!(
                collapse_home_with(r"C:\USERS\username\atomcode", Some(home)),
                "~/atomcode"
            );
        }
    }

    /// On Windows, a verbatim-prefixed path whose casing differs from
    /// `dirs::home_dir()` must still be collapsed.  This test only
    /// asserts the verbatim-stripping + case-insensitive match when
    /// actually running on Windows; on other platforms it verifies
    /// that the verbatim prefix is stripped correctly (which is
    /// platform-agnostic).
    #[test]
    fn collapse_home_windows_verbatim_with_different_case() {
        let home = std::path::Path::new(r"C:\Users\username");

        // Verbatim prefix stripped, exact-case home matched
        assert_eq!(
            collapse_home_with(r"\\?\C:\Users\username\atomcode", Some(home)),
            "~/atomcode"
        );

        // On Windows the case-insensitive branch must also handle
        // verbatim paths with different casing.
        if cfg!(windows) {
            assert_eq!(
                collapse_home_with(r"\\?\C:\USERS\username\atomcode", Some(home)),
                "~/atomcode"
            );
        }
    }

    #[test]
    fn collapse_home_windows_exact_home() {
        let home = std::path::Path::new(r"C:\Users\username");
        assert_eq!(
            collapse_home_with(r"C:\Users\username", Some(home)),
            "~"
        );
    }

    /// Regression test for the slice-offset bug in the Windows
    /// case-insensitive branch.  The old code did:
    ///
    ///   path.to_lowercase()
    ///       .strip_prefix(&home_str.to_lowercase())
    ///       .map(|s| path[s.len()..].to_string())
    ///
    /// where `s` was the *lowercase remainder* after stripping the
    /// home prefix.  `s.len()` equals `path.len() - home_str.len()`,
    /// so `path[s.len()..]` took the **last** `home_str.len()` chars
    /// of the original path instead of everything *after* the home
    /// prefix.  For `C:\Users\hao\Documents\WPSDrive\NotLoginPage`
    /// with home `C:\Users\hao`, the result was `~NotLoginPage`
    /// instead of `~/Documents/WPSDrive/NotLoginPage`.
    #[test]
    fn collapse_home_windows_deeply_nested_path() {
        let home = std::path::Path::new(r"C:\Users\hao");
        assert_eq!(
            collapse_home_with(
                r"C:\Users\hao\Documents\WPSDrive\NotLoginPage",
                Some(home),
            ),
            "~/Documents/WPSDrive/NotLoginPage"
        );
    }

    /// Same bug, but exercising the verbatim-prefix path that
    /// `std::fs::canonicalize` returns on Windows.  After the `\\?\`
    /// prefix is stripped the remaining path must still be sliced at
    /// `home_str.len()`, not at the lowercase-remainder length.
    #[test]
    fn collapse_home_windows_verbatim_deeply_nested_path() {
        let home = std::path::Path::new(r"C:\Users\hao");
        assert_eq!(
            collapse_home_with(
                r"\\?\C:\Users\hao\Documents\WPSDrive\NotLoginPage",
                Some(home),
            ),
            "~/Documents/WPSDrive/NotLoginPage"
        );
    }

    #[test]
    fn history_path_never_panics() {
        let _ = history_path();
    }

    #[test]
    fn image_cache_dir_lives_under_config_dir() {
        let p = image_cache_dir();
        let cfg = atomcode_core::config::Config::config_dir();
        assert!(p.starts_with(&cfg), "{:?} should be under {:?}", p, cfg);
        assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("image-cache"));
    }
}