use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    time::Duration,
};

use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
use lsp_types::{
    request::{Completion, GotoDefinition, HoverRequest, Request},
    CompletionItem, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams,
    GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind,
    OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, Url,
};
use miette::{IntoDiagnostic, Result};
use nu_cli::NuCompleter;
use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::{
    engine::{EngineState, Stack, StateWorkingSet},
    DeclId, Span, Value, VarId,
};
use reedline::Completer;
use ropey::Rope;

mod diagnostics;
mod notification;

#[derive(Debug)]
enum Id {
    Variable(VarId),
    Declaration(DeclId),
    Value(FlatShape),
}

pub struct LanguageServer {
    connection: Connection,
    io_threads: Option<IoThreads>,
    ropes: BTreeMap<PathBuf, Rope>,
}

impl LanguageServer {
    pub fn initialize_stdio_connection() -> Result<Self> {
        let (connection, io_threads) = Connection::stdio();
        Self::initialize_connection(connection, Some(io_threads))
    }

    fn initialize_connection(
        connection: Connection,
        io_threads: Option<IoThreads>,
    ) -> Result<Self> {
        Ok(Self {
            connection,
            io_threads,
            ropes: BTreeMap::new(),
        })
    }

    pub fn serve_requests(
        mut self,
        engine_state: EngineState,
        ctrlc: Arc<AtomicBool>,
    ) -> Result<()> {
        let server_capabilities = serde_json::to_value(ServerCapabilities {
            text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind(
                TextDocumentSyncKind::INCREMENTAL,
            )),
            definition_provider: Some(OneOf::Left(true)),
            hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
            completion_provider: Some(lsp_types::CompletionOptions::default()),
            ..Default::default()
        })
        .expect("Must be serializable");

        let _initialization_params = self
            .connection
            .initialize_while(server_capabilities, || !ctrlc.load(Ordering::SeqCst))
            .into_diagnostic()?;

        while !ctrlc.load(Ordering::SeqCst) {
            let msg = match self
                .connection
                .receiver
                .recv_timeout(Duration::from_secs(1))
            {
                Ok(msg) => msg,
                Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
                    continue;
                }
                Err(_) => break,
            };

            match msg {
                Message::Request(request) => {
                    if self
                        .connection
                        .handle_shutdown(&request)
                        .into_diagnostic()?
                    {
                        return Ok(());
                    }

                    let mut engine_state = engine_state.clone();
                    let resp = match request.method.as_str() {
                        GotoDefinition::METHOD => Self::handle_lsp_request(
                            &mut engine_state,
                            request,
                            |engine_state, params| self.goto_definition(engine_state, params),
                        ),
                        HoverRequest::METHOD => Self::handle_lsp_request(
                            &mut engine_state,
                            request,
                            |engine_state, params| self.hover(engine_state, params),
                        ),
                        Completion::METHOD => Self::handle_lsp_request(
                            &mut engine_state,
                            request,
                            |engine_state, params| self.complete(engine_state, params),
                        ),
                        _ => {
                            continue;
                        }
                    };

                    self.connection
                        .sender
                        .send(Message::Response(resp))
                        .into_diagnostic()?;
                }
                Message::Response(_) => {}
                Message::Notification(notification) => {
                    if let Some(updated_file) = self.handle_lsp_notification(notification) {
                        let mut engine_state = engine_state.clone();
                        self.publish_diagnostics_for_file(updated_file, &mut engine_state)?;
                    }
                }
            }
        }

        if let Some(io_threads) = self.io_threads {
            io_threads.join().into_diagnostic()?;
        }

        Ok(())
    }

    fn handle_lsp_request<P, H, R>(
        engine_state: &mut EngineState,
        req: lsp_server::Request,
        mut param_handler: H,
    ) -> Response
    where
        P: serde::de::DeserializeOwned,
        H: FnMut(&mut EngineState, &P) -> Option<R>,
        R: serde::ser::Serialize,
    {
        match serde_json::from_value::<P>(req.params) {
            Ok(params) => Response {
                id: req.id,
                result: Some(
                    param_handler(engine_state, &params)
                        .and_then(|response| serde_json::to_value(response).ok())
                        .unwrap_or(serde_json::Value::Null),
                ),
                error: None,
            },

            Err(err) => Response {
                id: req.id,
                result: None,
                error: Some(ResponseError {
                    code: 1,
                    message: err.to_string(),
                    data: None,
                }),
            },
        }
    }

    fn span_to_range(span: &Span, rope_of_file: &Rope, offset: usize) -> lsp_types::Range {
        let line = rope_of_file.byte_to_line(span.start - offset);
        let character = span.start - offset - rope_of_file.line_to_char(line);

        let start = lsp_types::Position {
            line: line as u32,
            character: character as u32,
        };

        let line = rope_of_file.byte_to_line(span.end - offset);
        let character = span.end - offset - rope_of_file.line_to_char(line);

        let end = lsp_types::Position {
            line: line as u32,
            character: character as u32,
        };

        lsp_types::Range { start, end }
    }

    pub fn lsp_position_to_location(position: &lsp_types::Position, rope_of_file: &Rope) -> usize {
        let line_idx = rope_of_file.line_to_char(position.line as usize);
        line_idx + position.character as usize
    }

    fn find_id(
        working_set: &mut StateWorkingSet,
        path: &Path,
        file: &Rope,
        location: usize,
    ) -> Option<(Id, usize, Span)> {
        let file_path = path.to_string_lossy();

        // TODO: think about passing down the rope into the working_set
        let contents = file.bytes().collect::<Vec<u8>>();
        let block = parse(working_set, Some(&file_path), &contents, false);
        let flattened = flatten_block(working_set, &block);

        let offset = working_set.get_span_for_filename(&file_path)?.start;
        let location = location + offset;

        for (span, shape) in flattened {
            if location >= span.start && location < span.end {
                match &shape {
                    FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => {
                        return Some((Id::Variable(*var_id), offset, span));
                    }
                    FlatShape::InternalCall(decl_id) => {
                        return Some((Id::Declaration(*decl_id), offset, span));
                    }
                    _ => return Some((Id::Value(shape), offset, span)),
                }
            }
        }
        None
    }

    fn rope<'a, 'b: 'a>(&'b self, file_url: &Url) -> Option<(&'a Rope, &'a PathBuf)> {
        let file_path = file_url.to_file_path().ok()?;

        self.ropes
            .get_key_value(&file_path)
            .map(|(path, rope)| (rope, path))
    }

    fn read_in_file<'a>(
        &mut self,
        engine_state: &'a mut EngineState,
        file_url: &Url,
    ) -> Option<(&Rope, &PathBuf, StateWorkingSet<'a>)> {
        let (file, path) = self.rope(file_url)?;

        // TODO: AsPath thingy
        engine_state.start_in_file(Some(&path.to_string_lossy()));

        let working_set = StateWorkingSet::new(engine_state);

        Some((file, path, working_set))
    }

    fn goto_definition(
        &mut self,
        engine_state: &mut EngineState,
        params: &GotoDefinitionParams,
    ) -> Option<GotoDefinitionResponse> {
        let cwd = std::env::current_dir().expect("Could not get current working directory.");
        engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));

        let (file, path, mut working_set) = self.read_in_file(
            engine_state,
            &params.text_document_position_params.text_document.uri,
        )?;

        let (id, _, _) = Self::find_id(
            &mut working_set,
            path,
            file,
            Self::lsp_position_to_location(&params.text_document_position_params.position, file),
        )?;

        match id {
            Id::Declaration(decl_id) => {
                if let Some(block_id) = working_set.get_decl(decl_id).get_block_id() {
                    let block = working_set.get_block(block_id);
                    if let Some(span) = &block.span {
                        for (file_path, file_start, file_end) in working_set.files() {
                            if span.start >= *file_start && span.start < *file_end {
                                return Some(GotoDefinitionResponse::Scalar(Location {
                                    uri: Url::from_file_path(file_path).ok()?,
                                    range: Self::span_to_range(span, file, *file_start),
                                }));
                            }
                        }
                    }
                }
            }
            Id::Variable(var_id) => {
                let var = working_set.get_variable(var_id);
                for (_, file_start, file_end) in working_set.files() {
                    if var.declaration_span.start >= *file_start
                        && var.declaration_span.start < *file_end
                    {
                        return Some(GotoDefinitionResponse::Scalar(Location {
                            uri: params
                                .text_document_position_params
                                .text_document
                                .uri
                                .clone(),
                            range: Self::span_to_range(&var.declaration_span, file, *file_start),
                        }));
                    }
                }
            }
            Id::Value(_) => {}
        }
        None
    }

    fn hover(&mut self, engine_state: &mut EngineState, params: &HoverParams) -> Option<Hover> {
        let cwd = std::env::current_dir().expect("Could not get current working directory.");
        engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));

        let (file, path, mut working_set) = self.read_in_file(
            engine_state,
            &params.text_document_position_params.text_document.uri,
        )?;

        let (id, _, _) = Self::find_id(
            &mut working_set,
            path,
            file,
            Self::lsp_position_to_location(&params.text_document_position_params.position, file),
        )?;

        match id {
            Id::Variable(var_id) => {
                let var = working_set.get_variable(var_id);
                let contents = format!("{}{}", if var.mutable { "mutable " } else { "" }, var.ty);

                Some(Hover {
                    contents: HoverContents::Scalar(lsp_types::MarkedString::String(contents)),
                    // TODO
                    range: None,
                })
            }
            Id::Declaration(decl_id) => {
                let decl = working_set.get_decl(decl_id);

                let mut description = String::new();

                // First description
                description.push_str(&format!("{}\n", decl.usage().replace('\r', "")));

                // Additional description
                if !decl.extra_usage().is_empty() {
                    description.push_str(&format!("\n{}\n", decl.extra_usage()));
                }

                // Usage
                description.push_str("### Usage \n```\n");
                let signature = decl.signature();
                description.push_str(&format!("  {}", signature.name));
                if !signature.named.is_empty() {
                    description.push_str(" {flags}");
                }
                for required_arg in &signature.required_positional {
                    description.push_str(&format!(" <{}>", required_arg.name));
                }
                for optional_arg in &signature.optional_positional {
                    description.push_str(&format!(" <{}?>", optional_arg.name));
                }
                if let Some(arg) = &signature.rest_positional {
                    description.push_str(&format!(" <...{}>", arg.name));
                }
                description.push_str("\n```\n");

                // Flags
                if !signature.named.is_empty() {
                    description.push_str("\n### Flags\n\n");
                    let mut first = true;
                    for named in &signature.named {
                        if first {
                            first = false;
                        } else {
                            description.push('\n');
                        }
                        description.push_str("  ");
                        if let Some(short_flag) = &named.short {
                            description.push_str(&format!("`-{short_flag}`"));
                        }
                        if !named.long.is_empty() {
                            if named.short.is_some() {
                                description.push_str(", ");
                            }
                            description.push_str(&format!("`--{}`", named.long));
                        }
                        if let Some(arg) = &named.arg {
                            description.push_str(&format!(" `<{}>`", arg.to_type()));
                        }
                        if !named.desc.is_empty() {
                            description.push_str(&format!(" - {}", named.desc));
                        }
                        description.push('\n');
                    }
                    description.push('\n');
                }

                // Parameters
                if !signature.required_positional.is_empty()
                    || !signature.optional_positional.is_empty()
                    || signature.rest_positional.is_some()
                {
                    description.push_str("\n### Parameters\n\n");
                    let mut first = true;
                    for required_arg in &signature.required_positional {
                        if first {
                            first = false;
                        } else {
                            description.push('\n');
                        }
                        description.push_str(&format!(
                            "  `{}: {}`",
                            required_arg.name,
                            required_arg.shape.to_type()
                        ));
                        if !required_arg.desc.is_empty() {
                            description.push_str(&format!(" - {}", required_arg.desc));
                        }
                        description.push('\n');
                    }
                    for optional_arg in &signature.optional_positional {
                        if first {
                            first = false;
                        } else {
                            description.push('\n');
                        }
                        description.push_str(&format!(
                            "  `{}: {}`",
                            optional_arg.name,
                            optional_arg.shape.to_type()
                        ));
                        if !optional_arg.desc.is_empty() {
                            description.push_str(&format!(" - {}", optional_arg.desc));
                        }
                        description.push('\n');
                    }
                    if let Some(arg) = &signature.rest_positional {
                        if !first {
                            description.push('\n');
                        }
                        description.push_str(&format!(
                            " `...{}: {}`",
                            arg.name,
                            arg.shape.to_type()
                        ));
                        if !arg.desc.is_empty() {
                            description.push_str(&format!(" - {}", arg.desc));
                        }
                        description.push('\n');
                    }
                    description.push('\n');
                }

                // Input/output types
                if !signature.input_output_types.is_empty() {
                    description.push_str("\n### Input/output types\n");
                    description.push_str("\n```\n");
                    for input_output in &signature.input_output_types {
                        description
                            .push_str(&format!(" {} | {}\n", input_output.0, input_output.1));
                    }
                    description.push_str("\n```\n");
                }

                // Examples
                if !decl.examples().is_empty() {
                    description.push_str("### Example(s)\n");
                    for example in decl.examples() {
                        description.push_str(&format!(
                            "  {}\n```\n  {}\n```\n",
                            example.description, example.example
                        ));
                    }
                }

                Some(Hover {
                    contents: HoverContents::Markup(MarkupContent {
                        kind: MarkupKind::Markdown,
                        value: description,
                    }),
                    // TODO
                    range: None,
                })
            }
            Id::Value(shape) => {
                let hover = String::from(match shape {
                    FlatShape::And => "and",
                    FlatShape::Binary => "binary",
                    FlatShape::Block => "block",
                    FlatShape::Bool => "bool",
                    FlatShape::Closure => "closure",
                    FlatShape::DateTime => "datetime",
                    FlatShape::Directory => "directory",
                    FlatShape::External => "external",
                    FlatShape::ExternalArg => "external arg",
                    FlatShape::Filepath => "file path",
                    FlatShape::Flag => "flag",
                    FlatShape::Float => "float",
                    FlatShape::GlobPattern => "glob pattern",
                    FlatShape::Int => "int",
                    FlatShape::Keyword => "keyword",
                    FlatShape::List => "list",
                    FlatShape::MatchPattern => "match-pattern",
                    FlatShape::Nothing => "nothing",
                    FlatShape::Range => "range",
                    FlatShape::Record => "record",
                    FlatShape::String => "string",
                    FlatShape::StringInterpolation => "string interpolation",
                    FlatShape::Table => "table",
                    _ => {
                        return None;
                    }
                });

                Some(Hover {
                    contents: HoverContents::Scalar(lsp_types::MarkedString::String(hover)),
                    // TODO
                    range: None,
                })
            }
        }
    }

    fn complete(
        &mut self,
        engine_state: &mut EngineState,
        params: &CompletionParams,
    ) -> Option<CompletionResponse> {
        let cwd = std::env::current_dir().expect("Could not get current working directory.");
        engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));

        let (rope_of_file, _, _) = self.read_in_file(
            engine_state,
            &params.text_document_position.text_document.uri,
        )?;

        let stack = Stack::new();
        let mut completer = NuCompleter::new(Arc::new(engine_state.clone()), stack);

        let location =
            Self::lsp_position_to_location(&params.text_document_position.position, rope_of_file);
        let results = completer.complete(&rope_of_file.to_string()[..location], location);
        if results.is_empty() {
            None
        } else {
            Some(CompletionResponse::Array(
                results
                    .into_iter()
                    .map(|r| {
                        let mut start = params.text_document_position.position;
                        start.character -= (r.span.end - r.span.start) as u32;

                        CompletionItem {
                            label: r.value.clone(),
                            detail: r.description,
                            text_edit: Some(CompletionTextEdit::Edit(TextEdit {
                                range: Range {
                                    start,
                                    end: params.text_document_position.position,
                                },
                                new_text: r.value,
                            })),
                            ..Default::default()
                        }
                    })
                    .collect(),
            ))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use assert_json_diff::assert_json_eq;
    use lsp_types::{
        notification::{
            DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification,
        },
        request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown},
        CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
        GotoDefinitionParams, InitializeParams, InitializedParams, PartialResultParams,
        TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem,
        TextDocumentPositionParams, Url, WorkDoneProgressParams,
    };
    use nu_test_support::fs::{fixtures, root};
    use std::sync::mpsc::Receiver;

    pub fn initialize_language_server() -> (Connection, Receiver<Result<()>>) {
        use std::sync::mpsc;
        let (client_connection, server_connection) = Connection::memory();
        let lsp_server = LanguageServer::initialize_connection(server_connection, None).unwrap();

        let (send, recv) = mpsc::channel();
        std::thread::spawn(move || {
            let engine_state = nu_cmd_lang::create_default_context();
            let engine_state = nu_command::add_shell_command_context(engine_state);
            send.send(lsp_server.serve_requests(engine_state, Arc::new(AtomicBool::new(false))))
        });

        client_connection
            .sender
            .send(Message::Request(lsp_server::Request {
                id: 1.into(),
                method: Initialize::METHOD.to_string(),
                params: serde_json::to_value(InitializeParams {
                    ..Default::default()
                })
                .unwrap(),
            }))
            .unwrap();
        client_connection
            .sender
            .send(Message::Notification(lsp_server::Notification {
                method: Initialized::METHOD.to_string(),
                params: serde_json::to_value(InitializedParams {}).unwrap(),
            }))
            .unwrap();

        let _initialize_response = client_connection
            .receiver
            .recv_timeout(std::time::Duration::from_secs(2))
            .unwrap();

        (client_connection, recv)
    }

    #[test]
    fn shutdown_on_request() {
        let (client_connection, recv) = initialize_language_server();

        client_connection
            .sender
            .send(Message::Request(lsp_server::Request {
                id: 2.into(),
                method: Shutdown::METHOD.to_string(),
                params: serde_json::Value::Null,
            }))
            .unwrap();
        client_connection
            .sender
            .send(Message::Notification(lsp_server::Notification {
                method: Exit::METHOD.to_string(),
                params: serde_json::Value::Null,
            }))
            .unwrap();

        assert!(recv
            .recv_timeout(std::time::Duration::from_secs(2))
            .unwrap()
            .is_ok());
    }

    #[test]
    fn goto_definition_for_none_existing_file() {
        let (client_connection, _recv) = initialize_language_server();

        let mut none_existent_path = root();
        none_existent_path.push("none-existent.nu");

        client_connection
            .sender
            .send(Message::Request(lsp_server::Request {
                id: 2.into(),
                method: GotoDefinition::METHOD.to_string(),
                params: serde_json::to_value(GotoDefinitionParams {
                    text_document_position_params: TextDocumentPositionParams {
                        text_document: TextDocumentIdentifier {
                            uri: Url::from_file_path(none_existent_path).unwrap(),
                        },
                        position: lsp_types::Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    work_done_progress_params: WorkDoneProgressParams::default(),
                    partial_result_params: PartialResultParams::default(),
                })
                .unwrap(),
            }))
            .unwrap();

        let resp = client_connection
            .receiver
            .recv_timeout(std::time::Duration::from_secs(2))
            .unwrap();
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(result, serde_json::json!(null));
    }

    pub fn open_unchecked(client_connection: &Connection, uri: Url) -> lsp_server::Notification {
        open(client_connection, uri).unwrap()
    }

    pub fn open(
        client_connection: &Connection,
        uri: Url,
    ) -> Result<lsp_server::Notification, String> {
        let text =
            std::fs::read_to_string(uri.to_file_path().unwrap()).map_err(|e| e.to_string())?;

        client_connection
            .sender
            .send(Message::Notification(lsp_server::Notification {
                method: DidOpenTextDocument::METHOD.to_string(),
                params: serde_json::to_value(DidOpenTextDocumentParams {
                    text_document: TextDocumentItem {
                        uri,
                        language_id: String::from("nu"),
                        version: 1,
                        text,
                    },
                })
                .unwrap(),
            }))
            .map_err(|e| e.to_string())?;

        let notification = client_connection
            .receiver
            .recv_timeout(Duration::from_secs(2))
            .map_err(|e| e.to_string())?;

        if let Message::Notification(n) = notification {
            Ok(n)
        } else {
            Err(String::from("Did not receive a notification from server"))
        }
    }

    pub fn update(
        client_connection: &Connection,
        uri: Url,
        text: String,
        range: Option<Range>,
    ) -> lsp_server::Notification {
        client_connection
            .sender
            .send(lsp_server::Message::Notification(
                lsp_server::Notification {
                    method: DidChangeTextDocument::METHOD.to_string(),
                    params: serde_json::to_value(DidChangeTextDocumentParams {
                        text_document: lsp_types::VersionedTextDocumentIdentifier {
                            uri,
                            version: 2,
                        },
                        content_changes: vec![TextDocumentContentChangeEvent {
                            range,
                            range_length: None,
                            text,
                        }],
                    })
                    .unwrap(),
                },
            ))
            .unwrap();

        let notification = client_connection
            .receiver
            .recv_timeout(Duration::from_secs(2))
            .unwrap();

        if let Message::Notification(n) = notification {
            n
        } else {
            panic!();
        }
    }

    fn goto_definition(
        client_connection: &Connection,
        uri: Url,
        line: u32,
        character: u32,
    ) -> Message {
        client_connection
            .sender
            .send(Message::Request(lsp_server::Request {
                id: 2.into(),
                method: GotoDefinition::METHOD.to_string(),
                params: serde_json::to_value(GotoDefinitionParams {
                    text_document_position_params: TextDocumentPositionParams {
                        text_document: TextDocumentIdentifier { uri },
                        position: lsp_types::Position { line, character },
                    },
                    work_done_progress_params: WorkDoneProgressParams::default(),
                    partial_result_params: PartialResultParams::default(),
                })
                .unwrap(),
            }))
            .unwrap();

        client_connection
            .receiver
            .recv_timeout(std::time::Duration::from_secs(2))
            .unwrap()
    }

    #[test]
    fn goto_definition_of_variable() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("goto");
        script.push("var.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = goto_definition(&client_connection, script.clone(), 2, 12);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!({
               "uri": script,
               "range": {
                  "start": { "line": 0, "character": 4 },
                  "end": { "line": 0, "character": 12 }
               }
            })
        );
    }

    #[test]
    fn goto_definition_of_command() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("goto");
        script.push("command.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = goto_definition(&client_connection, script.clone(), 4, 1);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!({
               "uri": script,
               "range": {
                  "start": { "line": 0, "character": 17 },
                  "end": { "line": 2, "character": 1 }
               }
            })
        );
    }

    #[test]
    fn goto_definition_of_command_parameter() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("goto");
        script.push("command.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = goto_definition(&client_connection, script.clone(), 1, 14);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!({
               "uri": script,
               "range": {
                  "start": { "line": 0, "character": 11 },
                  "end": { "line": 0, "character": 15 }
               }
            })
        );
    }

    pub fn hover(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message {
        client_connection
            .sender
            .send(Message::Request(lsp_server::Request {
                id: 2.into(),
                method: HoverRequest::METHOD.to_string(),
                params: serde_json::to_value(HoverParams {
                    text_document_position_params: TextDocumentPositionParams {
                        text_document: TextDocumentIdentifier { uri },
                        position: lsp_types::Position { line, character },
                    },
                    work_done_progress_params: WorkDoneProgressParams::default(),
                })
                .unwrap(),
            }))
            .unwrap();

        client_connection
            .receiver
            .recv_timeout(std::time::Duration::from_secs(2))
            .unwrap()
    }

    #[test]
    fn hover_on_variable() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("hover");
        script.push("var.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = hover(&client_connection, script.clone(), 2, 0);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!({
                "contents": "table"
            })
        );
    }

    #[test]
    fn hover_on_command() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("hover");
        script.push("command.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = hover(&client_connection, script.clone(), 3, 0);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!({
                "contents": {
                    "kind": "markdown",
                    "value": "Renders some greeting message\n### Usage \n```\n  hello {flags}\n```\n\n### Flags\n\n  `-h`, `--help` - Display the help message for this command\n\n"
                }
            })
        );
    }

    fn complete(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message {
        client_connection
            .sender
            .send(Message::Request(lsp_server::Request {
                id: 2.into(),
                method: Completion::METHOD.to_string(),
                params: serde_json::to_value(CompletionParams {
                    text_document_position: TextDocumentPositionParams {
                        text_document: TextDocumentIdentifier { uri },
                        position: lsp_types::Position { line, character },
                    },
                    work_done_progress_params: WorkDoneProgressParams::default(),
                    partial_result_params: PartialResultParams::default(),
                    context: None,
                })
                .unwrap(),
            }))
            .unwrap();

        client_connection
            .receiver
            .recv_timeout(std::time::Duration::from_secs(2))
            .unwrap()
    }

    #[test]
    fn complete_on_variable() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("completion");
        script.push("var.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = complete(&client_connection, script, 2, 9);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!([
               {
                  "label": "$greeting",
                  "textEdit": {
                     "newText": "$greeting",
                     "range": {
                        "start": { "character": 5, "line": 2 },
                        "end": { "character": 9, "line": 2 }
                     }
                  }
               }
            ])
        );
    }

    #[test]
    fn complete_command_with_space() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("completion");
        script.push("command.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = complete(&client_connection, script, 0, 8);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!([
               {
                  "label": "config nu",
                  "detail": "Edit nu configurations.",
                  "textEdit": {
                     "range": {
                        "start": { "line": 0, "character": 0 },
                        "end": { "line": 0, "character": 8 },
                     },
                     "newText": "config nu"
                  }
               }
            ])
        );
    }

    #[test]
    fn complete_command_with_utf_line() {
        let (client_connection, _recv) = initialize_language_server();

        let mut script = fixtures();
        script.push("lsp");
        script.push("completion");
        script.push("utf_pipeline.nu");
        let script = Url::from_file_path(script).unwrap();

        open_unchecked(&client_connection, script.clone());

        let resp = complete(&client_connection, script, 0, 14);
        let result = if let Message::Response(response) = resp {
            response.result
        } else {
            panic!()
        };

        assert_json_eq!(
            result,
            serde_json::json!([
               {
                  "label": "str trim",
                  "detail": "Trim whitespace or specific character.",
                  "textEdit": {
                     "range": {
                        "start": { "line": 0, "character": 9 },
                        "end": { "line": 0, "character": 14 },
                     },
                     "newText": "str trim"
                  }
               }
            ])
        );
    }
}
