use jackin::config::ConfigEditor;
use jackin::paths::JackinPaths;
use jackin::workspace::{self, WorkspaceConfig, WorkspaceEdit, parse_mount_spec_resolved};
use std::path::Path;
use std::sync::{Mutex, OnceLock};

struct RestoreCwd(std::path::PathBuf);

impl Drop for RestoreCwd {
    fn drop(&mut self) {
        let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
        let target = if self.0.exists() { &self.0 } else { &fallback };
        let _ = std::env::set_current_dir(target);
    }
}

fn with_cwd<T>(dir: &Path, f: impl FnOnce() -> T) -> T {
    static CWD_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    let _guard = CWD_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
    let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
    let original = std::env::current_dir().unwrap_or_else(|_| fallback.clone());
    std::env::set_current_dir(dir).unwrap();
    let _restore = RestoreCwd(original);
    f()
}

/// Bootstrap a fresh, empty config file for a ConfigEditor-based test.
///
/// Returns a temp dir (kept alive for the test's duration) and the
/// corresponding `JackinPaths`.
fn bootstrap_paths() -> (tempfile::TempDir, JackinPaths) {
    let temp = tempfile::tempdir().unwrap();
    let paths = JackinPaths::for_tests(temp.path());
    paths.ensure_base_dirs().unwrap();
    // Write a minimal valid config so ConfigEditor::open succeeds.
    std::fs::write(&paths.config_file, "").unwrap();
    (temp, paths)
}

/// Simulates `jackin workspace create jackin --workdir jackin --mount sibling-project`
/// from a parent directory. Both relative workdir and mount must be resolved to
/// absolute paths so that `create_workspace` validation passes.
#[test]
fn workspace_create_resolves_relative_workdir_and_mounts() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let workdir_dir = temp.path().join("jackin");
    let mount_dir = temp.path().join("sibling-project");
    std::fs::create_dir_all(&workdir_dir).unwrap();
    std::fs::create_dir_all(&mount_dir).unwrap();

    let mut editor = ConfigEditor::open(&paths).unwrap();
    let result = with_cwd(temp.path(), || {
        let expanded_workdir = workspace::resolve_path("jackin");
        let mount = parse_mount_spec_resolved("sibling-project").unwrap();
        editor.create_workspace(
            "jackin",
            WorkspaceConfig {
                workdir: expanded_workdir.clone(),
                mounts: vec![
                    workspace::MountConfig {
                        src: expanded_workdir.clone(),
                        dst: expanded_workdir,
                        readonly: false,
                        isolation: jackin::isolation::MountIsolation::Shared,
                    },
                    mount,
                ],
                ..Default::default()
            },
        )
    });

    result.unwrap();
    let config = editor.save().unwrap();
    let ws = config.workspaces.get("jackin").unwrap();
    assert!(ws.workdir.starts_with('/'));
    assert!(!ws.workdir.contains(".."));
    assert!(ws.mounts.iter().all(|m| m.src.starts_with('/')));
}

/// Simulates `jackin workspace create jackin --workdir . --mount ../jackin-agent-smith`
/// from inside the project directory. Dot-workdir and parent-relative mount must both
/// resolve to clean absolute paths.
#[test]
fn workspace_create_resolves_dot_workdir_and_dotdot_mount() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let workdir_dir = temp.path().join("jackin");
    let sibling_dir = temp.path().join("jackin-agent-smith");
    std::fs::create_dir_all(&workdir_dir).unwrap();
    std::fs::create_dir_all(&sibling_dir).unwrap();

    let mut editor = ConfigEditor::open(&paths).unwrap();
    let mut mount_for_assert = None;
    let result = with_cwd(&workdir_dir, || {
        let expanded_workdir = workspace::resolve_path(".");
        let mount = parse_mount_spec_resolved("../jackin-agent-smith").unwrap();
        mount_for_assert = Some(mount.clone());
        editor.create_workspace(
            "jackin",
            WorkspaceConfig {
                workdir: expanded_workdir.clone(),
                mounts: vec![
                    workspace::MountConfig {
                        src: expanded_workdir.clone(),
                        dst: expanded_workdir,
                        readonly: false,
                        isolation: jackin::isolation::MountIsolation::Shared,
                    },
                    mount,
                ],
                ..Default::default()
            },
        )
    });

    result.unwrap();
    let config = editor.save().unwrap();
    let ws = config.workspaces.get("jackin").unwrap();
    assert!(ws.workdir.starts_with('/'));
    assert!(!ws.workdir.contains(".."));
    let mount = mount_for_assert.unwrap();
    assert!(!mount.src.contains(".."), "mount src must not contain '..'");
    assert!(mount.src.ends_with("/jackin-agent-smith"));
}

/// Simulates `jackin workspace create my-app --workdir /tmp/app` without
/// `--mount`. Workdir is only the start path now, so at least one explicit
/// mount is required.
#[test]
fn workspace_create_requires_explicit_mount() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let workdir_dir = temp.path().join("my-app");
    std::fs::create_dir_all(&workdir_dir).unwrap();

    let expanded_workdir = workdir_dir.display().to_string();

    let mut editor = ConfigEditor::open(&paths).unwrap();
    let err = editor
        .create_workspace(
            "my-app",
            WorkspaceConfig {
                workdir: expanded_workdir,
                mounts: vec![],
                ..Default::default()
            },
        )
        .unwrap_err();

    assert!(
        err.to_string().contains("must define at least one mount"),
        "expected missing mount validation, got: {err}"
    );
}

/// Simulates `jackin workspace create monorepo --workdir /workspace
/// --mount /tmp/src:/workspace`. The workdir must not add any implicit mount.
#[test]
fn workspace_create_uses_only_explicit_mounts() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let src_dir = temp.path().join("src");
    std::fs::create_dir_all(&src_dir).unwrap();

    let src_path = src_dir.display().to_string();

    let all_mounts = vec![workspace::MountConfig {
        src: src_path.clone(),
        dst: "/workspace".to_string(),
        readonly: false,
        isolation: jackin::isolation::MountIsolation::Shared,
    }];

    let mut editor = ConfigEditor::open(&paths).unwrap();
    editor
        .create_workspace(
            "monorepo",
            WorkspaceConfig {
                workdir: "/workspace".to_string(),
                mounts: all_mounts,
                ..Default::default()
            },
        )
        .unwrap();

    let config = editor.save().unwrap();
    let ws = config.workspaces.get("monorepo").unwrap();
    assert_eq!(ws.mounts.len(), 1, "should only have the explicit mount");
    assert_eq!(ws.mounts[0].src, src_path);
    assert_eq!(ws.mounts[0].dst, "/workspace");
}

/// An explicit workdir mount should be preserved exactly as supplied.
#[test]
fn workspace_create_preserves_explicit_workdir_mount() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let workdir_dir = temp.path().join("project");
    std::fs::create_dir_all(&workdir_dir).unwrap();

    let expanded_workdir = workdir_dir.display().to_string();

    let all_mounts = vec![workspace::MountConfig {
        src: expanded_workdir.clone(),
        dst: expanded_workdir.clone(),
        readonly: true, // user chose read-only
        isolation: jackin::isolation::MountIsolation::Shared,
    }];

    let mut editor = ConfigEditor::open(&paths).unwrap();
    editor
        .create_workspace(
            "project",
            WorkspaceConfig {
                workdir: expanded_workdir,
                mounts: all_mounts,
                ..Default::default()
            },
        )
        .unwrap();

    let config = editor.save().unwrap();
    let ws = config.workspaces.get("project").unwrap();
    assert_eq!(ws.mounts.len(), 1, "no duplicate mount should be added");
    assert!(ws.mounts[0].readonly, "original mount properties preserved");
}

/// Simulates `jackin workspace edit jackin --mount sibling-dev` where the mount
/// is a relative directory name. The resolved mount must pass validation.
#[test]
fn workspace_edit_resolves_relative_mount() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let workdir_dir = temp.path().join("jackin");
    let dev_dir = temp.path().join("jackin-dev");
    std::fs::create_dir_all(&workdir_dir).unwrap();
    std::fs::create_dir_all(&dev_dir).unwrap();

    let workdir_abs = workdir_dir.display().to_string();

    // Create workspace first
    let mut editor = ConfigEditor::open(&paths).unwrap();
    editor
        .create_workspace(
            "jackin",
            WorkspaceConfig {
                workdir: workdir_abs.clone(),
                mounts: vec![workspace::MountConfig {
                    src: workdir_abs.clone(),
                    dst: workdir_abs,
                    readonly: false,
                    isolation: jackin::isolation::MountIsolation::Shared,
                }],
                ..Default::default()
            },
        )
        .unwrap();
    editor.save().unwrap();

    // Now edit it
    let mut editor2 = ConfigEditor::open(&paths).unwrap();
    let result = with_cwd(temp.path(), || {
        let mount = parse_mount_spec_resolved("jackin-dev").unwrap();
        editor2.edit_workspace(
            "jackin",
            WorkspaceEdit {
                upsert_mounts: vec![mount],
                ..WorkspaceEdit::default()
            },
        )
    });

    result.unwrap();
    let config = editor2.save().unwrap();
    let ws = config.workspaces.get("jackin").unwrap();
    assert_eq!(ws.mounts.len(), 2);
    assert!(ws.mounts[1].src.starts_with('/'));
    assert!(ws.mounts[1].src.ends_with("/jackin-dev"));
}

/// Simulates `jackin workspace edit my-app --no-workdir-mount` to remove the
/// auto-mounted workdir after workspace creation.
#[test]
fn workspace_edit_no_workdir_mount_removes_auto_mount() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let workdir_dir = temp.path().join("my-app");
    let extra_dir = temp.path().join("extra");
    std::fs::create_dir_all(&workdir_dir).unwrap();
    std::fs::create_dir_all(&extra_dir).unwrap();

    let workdir_abs = workdir_dir.display().to_string();
    let extra_abs = extra_dir.display().to_string();

    // Create workspace with auto-mounted workdir + an extra mount
    let mut editor = ConfigEditor::open(&paths).unwrap();
    editor
        .create_workspace(
            "my-app",
            WorkspaceConfig {
                workdir: workdir_abs.clone(),
                mounts: vec![
                    workspace::MountConfig {
                        src: workdir_abs.clone(),
                        dst: workdir_abs.clone(),
                        readonly: false,
                        isolation: jackin::isolation::MountIsolation::Shared,
                    },
                    workspace::MountConfig {
                        src: extra_abs,
                        dst: workdir_abs.clone() + "/extra",
                        readonly: false,
                        isolation: jackin::isolation::MountIsolation::Shared,
                    },
                ],
                ..Default::default()
            },
        )
        .unwrap();
    let config_before = editor.save().unwrap();
    assert_eq!(
        config_before.workspaces.get("my-app").unwrap().mounts.len(),
        2
    );

    // Now remove the workdir auto-mount
    let mut editor2 = ConfigEditor::open(&paths).unwrap();
    editor2
        .edit_workspace(
            "my-app",
            WorkspaceEdit {
                no_workdir_mount: true,
                ..WorkspaceEdit::default()
            },
        )
        .unwrap();

    let config = editor2.save().unwrap();
    let ws = config.workspaces.get("my-app").unwrap();
    assert_eq!(ws.mounts.len(), 1, "auto-mount should be removed");
    assert_eq!(ws.mounts[0].dst, workdir_abs + "/extra");
}

/// `--no-workdir-mount` on edit should fail if there is no auto-mounted workdir.
#[test]
fn workspace_edit_no_workdir_mount_fails_when_no_auto_mount() {
    let (_temp_home, paths) = bootstrap_paths();

    let temp = tempfile::tempdir().unwrap();
    let src_dir = temp.path().join("src");
    std::fs::create_dir_all(&src_dir).unwrap();

    let src_abs = src_dir.display().to_string();

    // Create workspace that was originally made with --no-workdir-mount
    let mut editor = ConfigEditor::open(&paths).unwrap();
    editor
        .create_workspace(
            "monorepo",
            WorkspaceConfig {
                workdir: "/workspace".to_string(),
                mounts: vec![workspace::MountConfig {
                    src: src_abs,
                    dst: "/workspace".to_string(),
                    readonly: false,
                    isolation: jackin::isolation::MountIsolation::Shared,
                }],
                ..Default::default()
            },
        )
        .unwrap();
    editor.save().unwrap();

    let mut editor2 = ConfigEditor::open(&paths).unwrap();
    let err = editor2
        .edit_workspace(
            "monorepo",
            WorkspaceEdit {
                no_workdir_mount: true,
                ..WorkspaceEdit::default()
            },
        )
        .unwrap_err();

    assert!(
        err.to_string().contains("no auto-mounted workdir found"),
        "expected clear error, got: {err}"
    );
}
