use super::*;
use crate::context::{ServerContext, ServerContextSnapshot};
use emmylua_code_analysis::{Emmyrc, file_path_to_uri};
use lsp_server::{Connection, Message};
use lsp_types::{
    ClientCapabilities, DidChangeWatchedFilesClientCapabilities, PublishDiagnosticsParams,
    WorkspaceClientCapabilities,
};
use std::{
    fs,
    path::{Path, PathBuf},
    sync::Arc,
    sync::atomic::{AtomicU64, Ordering},
    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};

static TEST_WORKSPACE_COUNTER: AtomicU64 = AtomicU64::new(0);

struct TestWorkspace {
    root: PathBuf,
}

impl TestWorkspace {
    fn new() -> Self {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let counter = TEST_WORKSPACE_COUNTER.fetch_add(1, Ordering::Relaxed);
        let root = std::env::temp_dir().join(format!(
            "emmylua-workspace-manager-{}-{}-{}",
            std::process::id(),
            unique,
            counter,
        ));
        fs::create_dir_all(&root).unwrap();
        Self { root }
    }

    fn write_file(&self, relative_path: &str) -> PathBuf {
        self.write_file_with_contents(relative_path, "return true\n")
    }

    fn write_file_with_contents(&self, relative_path: &str, contents: &str) -> PathBuf {
        let path = self.root.join(relative_path);
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(&path, contents).unwrap();
        path
    }

    fn path(&self, relative_path: &str) -> PathBuf {
        self.root.join(relative_path)
    }
}

impl Drop for TestWorkspace {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

fn to_string(path: &Path) -> String {
    path.to_string_lossy().to_string()
}

fn json_string(value: &str) -> String {
    serde_json::to_string(value).unwrap()
}

fn emmyrc_from_json(json: &str) -> Emmyrc {
    serde_json::from_str(json).unwrap()
}

fn dynamic_watch_capabilities() -> ClientCapabilities {
    ClientCapabilities {
        workspace: Some(WorkspaceClientCapabilities {
            did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
                dynamic_registration: Some(true),
                ..Default::default()
            }),
            ..Default::default()
        }),
        ..Default::default()
    }
}

fn collect_watch_registration_methods(client: &Connection, expected: usize) -> Vec<String> {
    let mut methods = Vec::new();
    let deadline = Instant::now() + Duration::from_secs(5);

    while methods.len() < expected {
        let now = Instant::now();
        if now >= deadline {
            break;
        }

        let remaining = deadline.saturating_duration_since(now);
        match client.receiver.recv_timeout(remaining) {
            Ok(Message::Request(request))
                if request.method == "client/registerCapability"
                    || request.method == "client/unregisterCapability" =>
            {
                methods.push(request.method);
            }
            Ok(_) => {}
            Err(_) => break,
        }
    }

    methods
}

fn recv_publish_diagnostics_for_uri(
    client: &Connection,
    uri: &Uri,
    timeout: Duration,
) -> Option<PublishDiagnosticsParams> {
    let deadline = Instant::now() + timeout;

    while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {
        match client.receiver.recv_timeout(remaining) {
            Ok(Message::Notification(notification))
                if notification.method == "textDocument/publishDiagnostics" =>
            {
                let params: PublishDiagnosticsParams =
                    serde_json::from_value(notification.params).ok()?;
                if &params.uri == uri {
                    return Some(params);
                }
            }
            Ok(_) => {}
            Err(_) => break,
        }
    }

    None
}

async fn file_text(snapshot: &ServerContextSnapshot, uri: &Uri) -> Option<String> {
    let analysis = snapshot.analysis().read().await;
    let file_id = analysis.get_file_id(uri)?;
    analysis
        .compilation
        .get_db()
        .get_vfs()
        .get_file_content(&file_id)
        .map(|text| text.to_string())
}

// Run the same initialization path that startup uses for a single workspace root.
#[allow(dead_code)]
async fn run_init_analysis(snapshot: &ServerContextSnapshot, workspace_root: PathBuf) {
    init_analysis(
        snapshot.analysis(),
        snapshot.status_bar(),
        snapshot.file_diagnostic(),
        snapshot.lsp_features(),
        vec![WorkspaceFolder::new(workspace_root, false)],
        Arc::new(Emmyrc::default()),
        Vec::new(),
    )
    .await;
}

#[tokio::test(flavor = "multi_thread")]
async fn apply_workspace_reload_rebuilds_external_root_watchers() {
    let workspace = TestWorkspace::new();
    let external_library = TestWorkspace::new();
    let library_root = external_library.path("runtime/lua/vim");
    fs::create_dir_all(&library_root).unwrap();
    let (server, client) = Connection::memory();
    let context = ServerContext::new(server, dynamic_watch_capabilities());
    let snapshot = context.snapshot();
    let workspace_folders = vec![WorkspaceFolder::new(workspace.root.clone(), false)];

    {
        let mut workspace_manager = snapshot.workspace_manager().write().await;
        workspace_manager.workspace_folders = workspace_folders.clone();
    }

    apply_workspace_reload(
        snapshot.clone(),
        workspace_folders,
        Arc::new(emmyrc_from_json(&format!(
            r#"{{
                "workspace": {{
                    "library": [{}]
                }}
            }}"#,
            json_string(&to_string(&library_root)),
        ))),
    )
    .await;

    assert_eq!(
        collect_watch_registration_methods(&client, 2),
        vec![
            "client/unregisterCapability".to_string(),
            "client/registerCapability".to_string(),
        ]
    );
    assert!(snapshot.workspace_manager().read().await.watcher.is_some());

    snapshot
        .file_diagnostic()
        .cancel_workspace_diagnostic()
        .await;
    context.close().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn config_reload_is_not_cancelled_by_reindex() {
    let workspace = TestWorkspace::new();
    let library_root = workspace.path(".test-deps/runtime/lua/vim");
    let library_file = workspace.write_file(".test-deps/runtime/lua/vim/shared.lua");
    let library_uri = file_path_to_uri(&library_file).unwrap();
    let config_path = workspace.path(".emmyrc.json");
    fs::write(
        &config_path,
        format!(
            r#"{{
                "workspace": {{
                    "library": [{}]
                }}
            }}"#,
            json_string(&to_string(&library_root)),
        ),
    )
    .unwrap();
    let (server, _client) = Connection::memory();
    let context = ServerContext::new(server, dynamic_watch_capabilities());
    let snapshot = context.snapshot();

    {
        let mut workspace_manager = snapshot.workspace_manager().write().await;
        workspace_manager.workspace_folders =
            vec![WorkspaceFolder::new(workspace.root.clone(), false)];
    }

    snapshot
        .workspace_manager()
        .read()
        .await
        .add_update_emmyrc_task(snapshot.clone(), config_path);
    snapshot
        .workspace_manager()
        .read()
        .await
        .reindex_workspace(Duration::from_millis(50));

    tokio::time::sleep(CONFIG_RELOAD_DELAY + Duration::from_millis(250)).await;

    assert!(
        snapshot
            .analysis()
            .read()
            .await
            .get_file_id(&library_uri)
            .is_some()
    );

    snapshot
        .file_diagnostic()
        .cancel_workspace_diagnostic()
        .await;
    context.close().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn apply_workspace_reload_loads_configured_workspace_roots() {
    let workspace = TestWorkspace::new();
    let extra_workspace = TestWorkspace::new();
    let extra_file = extra_workspace.write_file("lua/shared.lua");
    let extra_uri = file_path_to_uri(&extra_file).unwrap();
    let (server, _client) = Connection::memory();
    let context = ServerContext::new(server, ClientCapabilities::default());
    let snapshot = context.snapshot();
    let workspace_folders = vec![WorkspaceFolder::new(workspace.root.clone(), false)];

    {
        let mut workspace_manager = snapshot.workspace_manager().write().await;
        workspace_manager.workspace_folders = workspace_folders.clone();
    }

    apply_workspace_reload(
        snapshot.clone(),
        workspace_folders,
        Arc::new(emmyrc_from_json(&format!(
            r#"{{
                "workspace": {{
                    "workspaceRoots": [{}]
                }}
            }}"#,
            json_string(&to_string(&extra_workspace.root)),
        ))),
    )
    .await;

    assert!(
        snapshot
            .analysis()
            .read()
            .await
            .get_file_id(&extra_uri)
            .is_some()
    );

    snapshot
        .file_diagnostic()
        .cancel_workspace_diagnostic()
        .await;
    context.close().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn apply_workspace_reload_removes_library_files_and_clears_diagnostics() {
    let workspace = TestWorkspace::new();
    let external_library = TestWorkspace::new();
    let library_root = external_library.path("runtime/lua/vim");
    let library_file = external_library.write_file("runtime/lua/vim/shared.lua");
    let library_uri = file_path_to_uri(&library_file).unwrap();
    let (server, client) = Connection::memory();
    let context = ServerContext::new(server, ClientCapabilities::default());
    let snapshot = context.snapshot();
    let workspace_folders = vec![WorkspaceFolder::new(workspace.root.clone(), false)];

    {
        let mut workspace_manager = snapshot.workspace_manager().write().await;
        workspace_manager.workspace_folders = workspace_folders.clone();
    }

    let library_emmyrc = Arc::new(emmyrc_from_json(&format!(
        r#"{{
            "workspace": {{
                "library": [{}]
            }}
        }}"#,
        json_string(&to_string(&library_root)),
    )));

    apply_workspace_reload(snapshot.clone(), workspace_folders.clone(), library_emmyrc).await;
    assert!(
        snapshot
            .analysis()
            .read()
            .await
            .get_file_id(&library_uri)
            .is_some()
    );

    let _ = recv_publish_diagnostics_for_uri(&client, &library_uri, Duration::from_millis(100));

    apply_workspace_reload(
        snapshot.clone(),
        workspace_folders,
        Arc::new(Emmyrc::default()),
    )
    .await;

    assert!(
        snapshot
            .analysis()
            .read()
            .await
            .get_file_id(&library_uri)
            .is_none()
    );
    let cleared = recv_publish_diagnostics_for_uri(&client, &library_uri, Duration::from_secs(5))
        .expect("expected cleared diagnostics for removed library file");
    assert!(cleared.diagnostics.is_empty());

    snapshot
        .file_diagnostic()
        .cancel_workspace_diagnostic()
        .await;
    context.close().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn apply_workspace_reload_preserves_unsaved_open_text_across_membership_changes() {
    let workspace = TestWorkspace::new();
    let external_library = TestWorkspace::new();
    let library_root = external_library.path("runtime/lua/vim");
    let library_file = external_library.write_file("runtime/lua/vim/shared.lua");
    let library_uri = file_path_to_uri(&library_file).unwrap();
    let unsaved_text = "return 'open-buffer'\n";
    let (server, _client) = Connection::memory();
    let context = ServerContext::new(server, ClientCapabilities::default());
    let snapshot = context.snapshot();
    let workspace_folders = vec![WorkspaceFolder::new(workspace.root.clone(), false)];

    {
        let mut workspace_manager = snapshot.workspace_manager().write().await;
        workspace_manager.workspace_folders = workspace_folders.clone();
        workspace_manager.sync_open_file(library_uri.clone(), unsaved_text.to_string());
    }

    let library_emmyrc = Arc::new(emmyrc_from_json(&format!(
        r#"{{
            "workspace": {{
                "library": [{}]
            }}
        }}"#,
        json_string(&to_string(&library_root)),
    )));

    apply_workspace_reload(
        snapshot.clone(),
        workspace_folders.clone(),
        library_emmyrc.clone(),
    )
    .await;
    assert_eq!(
        file_text(&snapshot, &library_uri).await.as_deref(),
        Some(unsaved_text)
    );

    apply_workspace_reload(
        snapshot.clone(),
        workspace_folders.clone(),
        Arc::new(Emmyrc::default()),
    )
    .await;
    assert!(
        snapshot
            .analysis()
            .read()
            .await
            .get_file_id(&library_uri)
            .is_none()
    );

    apply_workspace_reload(snapshot.clone(), workspace_folders, library_emmyrc).await;
    assert_eq!(
        file_text(&snapshot, &library_uri).await.as_deref(),
        Some(unsaved_text)
    );

    snapshot
        .file_diagnostic()
        .cancel_workspace_diagnostic()
        .await;
    context.close().await;
}
