use insta::assert_json_snapshot;
use lsp_types::{CompletionResponse, CompletionTriggerKind, NotebookCellKind, Position, Range};
use ruff_db::system::SystemPath;
use ty_server::ClientOptions;

use crate::{TestServer, TestServerBuilder};

#[test]
fn publish_diagnostics_open() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("test.ipynb");
    builder.add_python_cell(
        r#"from typing import Literal

type Style = Literal["italic", "bold", "underline"]"#,
    );

    builder.add_python_cell(
        r#"def with_style(line: str, word, style: Style) -> str:
    if style == "italic":
        return line.replace(word, f"*{word}*")
    elif style == "bold":
        return line.replace(word, f"__{word}__")

    position = line.find(word)
    output = line + "\n"
    output += " " * position
    output += "-" * len(word)
"#,
    );

    builder.add_python_cell(
        r#"print(with_style("ty is a fast type checker for Python.", "fast", "underlined"))
"#,
    );

    builder.open(&mut server);

    let cell1_diagnostics =
        server.await_notification::<lsp_types::notification::PublishDiagnostics>();
    let cell2_diagnostics =
        server.await_notification::<lsp_types::notification::PublishDiagnostics>();
    let cell3_diagnostics =
        server.await_notification::<lsp_types::notification::PublishDiagnostics>();

    assert_json_snapshot!([cell1_diagnostics, cell2_diagnostics, cell3_diagnostics]);

    Ok(())
}

#[test]
fn diagnostic_end_of_file() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("test.ipynb");
    builder.add_python_cell(
        r#"from typing import Literal

type Style = Literal["italic", "bold", "underline"]"#,
    );

    builder.add_python_cell(
        r#"def with_style(line: str, word, style: Style) -> str:
    if style == "italic":
        return line.replace(word, f"*{word}*")
    elif style == "bold":
        return line.replace(word, f"__{word}__")

    position = line.find(word)
    output = line + "\n"
    output += " " * position
    output += "-" * len(word)
"#,
    );

    let cell_3 = builder.add_python_cell(
        r#"with_style("test", "word", "underline")

IOError"#,
    );

    let notebook_url = builder.open(&mut server);

    server.collect_publish_diagnostic_notifications(3);

    server.send_notification::<lsp_types::notification::DidChangeNotebookDocument>(
        lsp_types::DidChangeNotebookDocumentParams {
            notebook_document: lsp_types::VersionedNotebookDocumentIdentifier {
                version: 0,
                uri: notebook_url,
            },
            change: lsp_types::NotebookDocumentChangeEvent {
                metadata: None,
                cells: Some(lsp_types::NotebookDocumentCellChange {
                    structure: None,
                    data: None,
                    text_content: Some(vec![lsp_types::NotebookDocumentChangeTextContent {
                        document: lsp_types::VersionedTextDocumentIdentifier {
                            uri: cell_3,
                            version: 0,
                        },

                        changes: {
                            vec![lsp_types::TextDocumentContentChangeEvent {
                                range: Some(Range::new(Position::new(0, 16), Position::new(0, 17))),
                                range_length: Some(1),
                                text: String::new(),
                            }]
                        },
                    }]),
                }),
            },
        },
    );

    let diagnostics = server.collect_publish_diagnostic_notifications(3);
    assert_json_snapshot!(diagnostics);

    Ok(())
}

#[test]
fn semantic_tokens() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");
    let first_cell = builder.add_python_cell(
        r#"from typing import Literal

type Style = Literal["italic", "bold", "underline"]"#,
    );

    let second_cell = builder.add_python_cell(
        r#"def with_style(line: str, word, style: Style) -> str:
    if style == "italic":
        return line.replace(word, f"*{word}*")
    elif style == "bold":
        return line.replace(word, f"__{word}__")

    position = line.find(word)
    output = line + "\n"
    output += " " * position
    output += "-" * len(word)
"#,
    );

    let third_cell = builder.add_python_cell(
        r#"print(with_style("ty is a fast type checker for Python.", "fast", "underlined"))
"#,
    );

    builder.open(&mut server);

    let cell1_tokens = semantic_tokens_full_for_cell(&mut server, &first_cell);
    let cell2_tokens = semantic_tokens_full_for_cell(&mut server, &second_cell);
    let cell3_tokens = semantic_tokens_full_for_cell(&mut server, &third_cell);

    assert_json_snapshot!([cell1_tokens, cell2_tokens, cell3_tokens]);

    server.collect_publish_diagnostic_notifications(3);

    Ok(())
}

#[test]
fn swap_cells() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");

    let first_cell = builder.add_python_cell(
        r#"b = a
"#,
    );

    let second_cell = builder.add_python_cell(r#"a = 10"#);

    builder.add_python_cell(r#"c = b"#);

    let notebook = builder.open(&mut server);

    let diagnostics = server.collect_publish_diagnostic_notifications(3);
    assert_json_snapshot!(diagnostics, @r###"
    {
      "vscode-notebook-cell://src/test.ipynb#0": [
        {
          "range": {
            "start": {
              "line": 0,
              "character": 4
            },
            "end": {
              "line": 0,
              "character": 5
            }
          },
          "severity": 1,
          "code": "unresolved-reference",
          "codeDescription": {
            "href": "https://ty.dev/rules#unresolved-reference"
          },
          "source": "ty",
          "message": "Name `a` used when not defined",
          "relatedInformation": []
        }
      ],
      "vscode-notebook-cell://src/test.ipynb#1": [],
      "vscode-notebook-cell://src/test.ipynb#2": []
    }
    "###);

    // Re-order the cells from `b`, `a`, `c` to `a`, `b`, `c` (swapping cell 1 and 2)
    server.send_notification::<lsp_types::notification::DidChangeNotebookDocument>(
        lsp_types::DidChangeNotebookDocumentParams {
            notebook_document: lsp_types::VersionedNotebookDocumentIdentifier {
                version: 1,
                uri: notebook,
            },
            change: lsp_types::NotebookDocumentChangeEvent {
                metadata: None,
                cells: Some(lsp_types::NotebookDocumentCellChange {
                    structure: Some(lsp_types::NotebookDocumentCellChangeStructure {
                        array: lsp_types::NotebookCellArrayChange {
                            start: 0,
                            delete_count: 2,
                            cells: Some(vec![
                                lsp_types::NotebookCell {
                                    kind: NotebookCellKind::Code,
                                    document: second_cell,
                                    metadata: None,
                                    execution_summary: None,
                                },
                                lsp_types::NotebookCell {
                                    kind: NotebookCellKind::Code,
                                    document: first_cell,
                                    metadata: None,
                                    execution_summary: None,
                                },
                            ]),
                        },
                        did_open: None,
                        did_close: None,
                    }),
                    data: None,
                    text_content: None,
                }),
            },
        },
    );

    let diagnostics = server.collect_publish_diagnostic_notifications(3);

    assert_json_snapshot!(diagnostics, @r###"
    {
      "vscode-notebook-cell://src/test.ipynb#0": [],
      "vscode-notebook-cell://src/test.ipynb#1": [],
      "vscode-notebook-cell://src/test.ipynb#2": []
    }
    "###);

    Ok(())
}

#[test]
fn auto_import() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .with_workspace(
            SystemPath::new("src"),
            Some(ClientOptions::default().with_experimental_auto_import(true)),
        )?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");

    builder.add_python_cell(
        r#"from typing import TYPE_CHECKING
"#,
    );

    let second_cell = builder.add_python_cell(
        r#"# leading comment
b: Litera
"#,
    );

    builder.open(&mut server);

    server.collect_publish_diagnostic_notifications(2);

    let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));

    assert_json_snapshot!(completions);

    Ok(())
}

#[test]
fn auto_import_same_cell() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .with_workspace(
            SystemPath::new("src"),
            Some(ClientOptions::default().with_experimental_auto_import(true)),
        )?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");

    let first_cell = builder.add_python_cell(
        r#"from typing import TYPE_CHECKING
b: Litera
"#,
    );

    builder.open(&mut server);

    server.collect_publish_diagnostic_notifications(1);

    let completions = literal_completions(&mut server, &first_cell, Position::new(1, 9));

    assert_json_snapshot!(completions);

    Ok(())
}

#[test]
fn auto_import_from_future() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .with_workspace(
            SystemPath::new("src"),
            Some(ClientOptions::default().with_experimental_auto_import(true)),
        )?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");

    builder.add_python_cell(r#"from typing import TYPE_CHECKING"#);

    let second_cell = builder.add_python_cell(
        r#"from __future__ import annotations
b: Litera
"#,
    );

    builder.open(&mut server);

    server.collect_publish_diagnostic_notifications(2);

    let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));

    assert_json_snapshot!(completions);

    Ok(())
}

#[test]
fn auto_import_docstring() -> anyhow::Result<()> {
    let mut server = TestServerBuilder::new()?
        .with_workspace(
            SystemPath::new("src"),
            Some(ClientOptions::default().with_experimental_auto_import(true)),
        )?
        .build()
        .wait_until_workspaces_are_initialized();

    server.initialization_result().unwrap();

    let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");

    builder.add_python_cell(
        r#"from typing import TYPE_CHECKING
"#,
    );

    let second_cell = builder.add_python_cell(
        r#""""A cell level docstring"""
b: Litera
"#,
    );

    builder.open(&mut server);

    server.collect_publish_diagnostic_notifications(2);

    let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));

    assert_json_snapshot!(completions);

    Ok(())
}

fn semantic_tokens_full_for_cell(
    server: &mut TestServer,
    cell_uri: &lsp_types::Url,
) -> Option<lsp_types::SemanticTokensResult> {
    let cell1_tokens_req_id = server.send_request::<lsp_types::request::SemanticTokensFullRequest>(
        lsp_types::SemanticTokensParams {
            work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
            partial_result_params: lsp_types::PartialResultParams::default(),
            text_document: lsp_types::TextDocumentIdentifier {
                uri: cell_uri.clone(),
            },
        },
    );

    server.await_response::<lsp_types::request::SemanticTokensFullRequest>(&cell1_tokens_req_id)
}

#[derive(Debug)]
pub(crate) struct NotebookBuilder {
    notebook_url: lsp_types::Url,
    // The cells: (cell_metadata, content, language_id)
    cells: Vec<(lsp_types::NotebookCell, String, String)>,
}

impl NotebookBuilder {
    pub(crate) fn virtual_file(name: &str) -> Self {
        let url: lsp_types::Url = format!("vs-code:/{name}").parse().unwrap();
        Self {
            notebook_url: url,
            cells: Vec::new(),
        }
    }

    pub(crate) fn add_python_cell(&mut self, content: &str) -> lsp_types::Url {
        let index = self.cells.len();
        let id = format!(
            "vscode-notebook-cell:/{}#{}",
            self.notebook_url.path(),
            index
        );

        let url: lsp_types::Url = id.parse().unwrap();

        self.cells.push((
            lsp_types::NotebookCell {
                kind: NotebookCellKind::Code,
                document: url.clone(),
                metadata: None,
                execution_summary: None,
            },
            content.to_string(),
            "python".to_string(),
        ));

        url
    }

    pub(crate) fn open(self, server: &mut TestServer) -> lsp_types::Url {
        server.send_notification::<lsp_types::notification::DidOpenNotebookDocument>(
            lsp_types::DidOpenNotebookDocumentParams {
                notebook_document: lsp_types::NotebookDocument {
                    uri: self.notebook_url.clone(),
                    notebook_type: "jupyter-notebook".to_string(),
                    version: 0,
                    metadata: None,
                    cells: self.cells.iter().map(|(cell, _, _)| cell.clone()).collect(),
                },
                cell_text_documents: self
                    .cells
                    .iter()
                    .map(|(cell, content, language_id)| lsp_types::TextDocumentItem {
                        uri: cell.document.clone(),
                        language_id: language_id.clone(),
                        version: 0,
                        text: content.clone(),
                    })
                    .collect(),
            },
        );

        self.notebook_url
    }
}

fn literal_completions(
    server: &mut TestServer,
    cell: &lsp_types::Url,
    position: Position,
) -> Vec<lsp_types::CompletionItem> {
    let completions_id =
        server.send_request::<lsp_types::request::Completion>(lsp_types::CompletionParams {
            text_document_position: lsp_types::TextDocumentPositionParams {
                text_document: lsp_types::TextDocumentIdentifier { uri: cell.clone() },
                position,
            },
            work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
            partial_result_params: lsp_types::PartialResultParams::default(),
            context: Some(lsp_types::CompletionContext {
                trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
                trigger_character: None,
            }),
        });

    // There are a ton of imports we don't care about in here...
    // The import bit is that an edit is always restricted to the current cell. That means,
    // we can't add `Literal` to the `from typing import TYPE_CHECKING` import in cell 1
    let completions = server.await_response::<lsp_types::request::Completion>(&completions_id);
    let mut items = match completions {
        Some(CompletionResponse::Array(array)) => array,
        Some(CompletionResponse::List(lsp_types::CompletionList { items, .. })) => items,
        None => return vec![],
    };

    items.retain(|item| item.label.starts_with("Litera"));

    items
}
