#![expect(clippy::result_large_err)]
use dap::DapRegistry;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Fs as _, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{
    DebugRequest, DebugScenario, LaunchRequest, SharedTaskContext, TaskContext, VariableName,
    ZedDebugConfig,
};
use text::Point;
use util::path;

use crate::NewProcessMode;
use crate::new_process_modal::NewProcessModal;
use crate::tests::{init_test, init_test_workspace};

#[gpui::test]
async fn test_debug_session_substitutes_variables_and_relativizes_paths(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(executor.clone());
    fs.insert_tree(
        path!("/project"),
        json!({
            "main.rs": "fn main() {}"
        }),
    )
    .await;

    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
    let workspace = init_test_workspace(&project, cx).await;
    let cx = &mut VisualTestContext::from_window(*workspace, cx);

    let test_variables = vec![(
        VariableName::WorktreeRoot,
        path!("/test/worktree/path").to_string(),
    )]
    .into_iter()
    .collect();

    let task_context: SharedTaskContext = TaskContext {
        cwd: None,
        task_variables: test_variables,
        project_env: Default::default(),
    }
    .into();

    let home_dir = paths::home_dir();

    let test_cases: Vec<(&'static str, &'static str)> = vec![
        // Absolute path - should not be relativized
        (
            path!("/absolute/path/to/program"),
            path!("/absolute/path/to/program"),
        ),
        // Relative path - should be prefixed with worktree root
        (
            format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
            path!("/test/worktree/path/src/program"),
        ),
        // Home directory path - should be expanded to full home directory path
        (
            format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
            home_dir
                .join("src")
                .join("program")
                .to_string_lossy()
                .to_string()
                .leak(),
        ),
        // Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
        (
            format!(
                "$ZED_WORKTREE_ROOT{0}src{0}program",
                std::path::MAIN_SEPARATOR
            )
            .leak(),
            path!("/test/worktree/path/src/program"),
        ),
    ];

    let called_launch = Arc::new(AtomicBool::new(false));

    for (input_path, expected_path) in test_cases {
        let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
            let called_launch = called_launch.clone();
            move |client| {
                client.on_request::<dap::requests::Launch, _>({
                    let called_launch = called_launch.clone();

                    move |_, args| {
                        let config = args.raw.as_object().unwrap();

                        assert_eq!(
                            config["program"].as_str().unwrap(),
                            expected_path,
                            "Program path was not correctly substituted for input: {}",
                            input_path
                        );

                        assert_eq!(
                            config["cwd"].as_str().unwrap(),
                            expected_path,
                            "CWD path was not correctly substituted for input: {}",
                            input_path
                        );

                        let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
                            input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
                        } else {
                            input_path.to_string()
                        };

                        assert_eq!(
                            config["otherField"].as_str().unwrap(),
                            &expected_other_field,
                            "Other field was incorrectly modified for input: {}",
                            input_path
                        );

                        called_launch.store(true, Ordering::SeqCst);

                        Ok(())
                    }
                });
            }
        });

        let scenario = DebugScenario {
            adapter: "fake-adapter".into(),
            label: "test-debug-session".into(),
            build: None,
            config: json!({
                "request": "launch",
                "program": input_path,
                "cwd": input_path,
                "otherField": input_path
            }),
            tcp_connection: None,
        };

        workspace
            .update(cx, |multi, window, cx| {
                multi.workspace().update(cx, |workspace, cx| {
                    workspace.start_debug_session(
                        scenario,
                        task_context.clone(),
                        None,
                        None,
                        window,
                        cx,
                    );
                })
            })
            .unwrap();

        cx.run_until_parked();

        assert!(called_launch.load(Ordering::SeqCst));
        called_launch.store(false, Ordering::SeqCst);
    }
}

#[gpui::test]
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
    init_test(cx);

    let fs = FakeFs::new(executor.clone());
    fs.insert_tree(
        path!("/project"),
        json!({
            "main.rs": "fn main() {}"
        }),
    )
    .await;

    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
    let workspace = init_test_workspace(&project, cx).await;
    let cx = &mut VisualTestContext::from_window(*workspace, cx);

    workspace
        .update(cx, |multi, window, cx| {
            multi.workspace().update(cx, |workspace, cx| {
                NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
            });
        })
        .unwrap();

    cx.run_until_parked();

    let modal = workspace
        .update(cx, |workspace, _, cx| {
            workspace.active_modal::<NewProcessModal>(cx)
        })
        .unwrap()
        .expect("Modal should be active");

    modal.update_in(cx, |modal, window, cx| {
        modal.set_configure("/project/main", "/project", false, window, cx);
        modal.save_debug_scenario(window, cx);
    });

    cx.executor().run_until_parked();

    let editor = workspace
        .update(cx, |workspace, _window, cx| {
            workspace.active_item_as::<Editor>(cx).unwrap()
        })
        .unwrap();

    let debug_json_content = fs
        .load(path!("/project/.zed/debug.json").as_ref())
        .await
        .expect("debug.json should exist")
        .lines()
        .filter(|line| !line.starts_with("//"))
        .collect::<Vec<_>>()
        .join("\n");

    let expected_content = indoc::indoc! {r#"
        [
          {
            "adapter": "fake-adapter",
            "label": "main (fake-adapter)",
            "request": "launch",
            "program": "/project/main",
            "cwd": "/project",
            "args": [],
            "env": {}
          }
        ]"#};

    pretty_assertions::assert_eq!(expected_content, debug_json_content);

    editor.update(cx, |editor, cx| {
        assert_eq!(
            editor
                .selections
                .newest::<Point>(&editor.display_snapshot(cx))
                .head(),
            Point::new(5, 2)
        )
    });

    modal.update_in(cx, |modal, window, cx| {
        modal.set_configure("/project/other", "/project", true, window, cx);
        modal.save_debug_scenario(window, cx);
    });

    cx.executor().run_until_parked();

    let expected_content = indoc::indoc! {r#"
        [
          {
            "adapter": "fake-adapter",
            "label": "main (fake-adapter)",
            "request": "launch",
            "program": "/project/main",
            "cwd": "/project",
            "args": [],
            "env": {}
          },
          {
            "adapter": "fake-adapter",
            "label": "other (fake-adapter)",
            "request": "launch",
            "program": "/project/other",
            "cwd": "/project",
            "args": [],
            "env": {}
          }
        ]"#};

    let debug_json_content = fs
        .load(path!("/project/.zed/debug.json").as_ref())
        .await
        .expect("debug.json should exist")
        .lines()
        .filter(|line| !line.starts_with("//"))
        .collect::<Vec<_>>()
        .join("\n");
    pretty_assertions::assert_eq!(expected_content, debug_json_content);
}

#[gpui::test]
async fn test_debug_modal_subtitles_with_multiple_worktrees(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(executor.clone());

    fs.insert_tree(
        path!("/workspace1"),
        json!({
            ".zed": {
                "debug.json": r#"[
                    {
                        "adapter": "fake-adapter",
                        "label": "Debug App 1",
                        "request": "launch",
                        "program": "./app1",
                        "cwd": "."
                    },
                    {
                        "adapter": "fake-adapter",
                        "label": "Debug Tests 1",
                        "request": "launch",
                        "program": "./test1",
                        "cwd": "."
                    }
                ]"#
            },
            "main.rs": "fn main() {}"
        }),
    )
    .await;

    let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;

    let workspace = init_test_workspace(&project, cx).await;
    let cx = &mut VisualTestContext::from_window(*workspace, cx);

    workspace
        .update(cx, |multi, window, cx| {
            multi.workspace().update(cx, |workspace, cx| {
                NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
            });
        })
        .unwrap();

    cx.run_until_parked();

    let modal = workspace
        .update(cx, |workspace, _, cx| {
            workspace.active_modal::<NewProcessModal>(cx)
        })
        .unwrap()
        .expect("Modal should be active");

    cx.executor().run_until_parked();

    let subtitles = modal.update_in(cx, |modal, _, cx| {
        modal.debug_picker_candidate_subtitles(cx)
    });

    assert_eq!(
        subtitles.as_slice(),
        [path!(".zed/debug.json"), path!(".zed/debug.json")]
    );
}

#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
    init_test(cx);

    let mut expected_adapters = vec![
        "CodeLLDB",
        "Debugpy",
        "JavaScript",
        "Delve",
        "GDB",
        "fake-adapter",
    ];

    let adapter_names = cx.update(|cx| {
        let registry = DapRegistry::global(cx);
        registry.enumerate_adapters::<Vec<_>>()
    });

    let zed_config = ZedDebugConfig {
        label: "test_debug_session".into(),
        adapter: "test_adapter".into(),
        request: DebugRequest::Launch(LaunchRequest {
            program: "test_program".into(),
            cwd: None,
            args: vec![],
            env: Default::default(),
        }),
        stop_on_entry: Some(true),
    };

    for adapter_name in adapter_names {
        let adapter_str = adapter_name.to_string();
        if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
            expected_adapters.remove(pos);
        }

        let adapter = cx
            .update(|cx| {
                let registry = DapRegistry::global(cx);
                registry.adapter(adapter_name.as_ref())
            })
            .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));

        let mut adapter_specific_config = zed_config.clone();
        adapter_specific_config.adapter = adapter_name.to_string().into();

        let debug_scenario = adapter
            .config_from_zed_format(adapter_specific_config)
            .await
            .unwrap_or_else(|_| {
                panic!(
                    "Adapter {} should successfully convert from Zed format",
                    adapter_name
                )
            });

        assert!(
            debug_scenario.config.is_object(),
            "Adapter {} should produce a JSON object for config",
            adapter_name
        );

        let request_type = adapter
            .request_kind(&debug_scenario.config)
            .await
            .unwrap_or_else(|_| {
                panic!(
                    "Adapter {} should validate the config successfully",
                    adapter_name
                )
            });

        match request_type {
            dap::StartDebuggingRequestArgumentsRequest::Launch => {}
            dap::StartDebuggingRequestArgumentsRequest::Attach => {
                panic!(
                    "Expected Launch request but got Attach for adapter {}",
                    adapter_name
                );
            }
        }
    }

    assert!(
        expected_adapters.is_empty(),
        "The following expected adapters were not found in the registry: {:?}",
        expected_adapters
    );
}
