use super::helpers::*;
use std::fs;
use tower_lsp_server::ls_types::Uri;

#[tokio::test]
async fn test_rename_citation_updates_bib_and_dependents() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    fs::write(root.join("_quarto.yml"), "project: default\n").unwrap();

    let bib_path = root.join("refs.bib");
    fs::write(&bib_path, "@article{oldkey,\n  title = {Old}\n}\n").unwrap();

    let doc1_path = root.join("doc1.qmd");
    let doc2_path = root.join("doc2.qmd");
    fs::write(
        &doc1_path,
        "---\nbibliography: refs.bib\n---\nSee [@oldkey].\n",
    )
    .unwrap();
    fs::write(
        &doc2_path,
        "---\nbibliography: refs.bib\n---\nAlso [@oldkey].\n",
    )
    .unwrap();

    let doc1_uri = Uri::from_file_path(&doc1_path).unwrap();
    let doc2_uri = Uri::from_file_path(&doc2_path).unwrap();
    let bib_uri = Uri::from_file_path(&bib_path).unwrap();
    let root_uri = Uri::from_file_path(root).unwrap();

    let server = TestLspServer::new();
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(
            doc1_uri.as_str(),
            &fs::read_to_string(&doc1_path).unwrap(),
            "quarto",
        )
        .await;
    server
        .open_document(
            doc2_uri.as_str(),
            &fs::read_to_string(&doc2_path).unwrap(),
            "quarto",
        )
        .await;

    let edit = server
        .rename(doc1_uri.as_str(), 3, 7, "newkey")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");

    assert!(changes.contains_key(&doc1_uri));
    assert!(changes.contains_key(&doc2_uri));
    assert!(changes.contains_key(&bib_uri));
}

#[tokio::test]
async fn test_rename_citation_updates_inline_references() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    std::fs::write(root.join("_quarto.yml"), "project: default\n").unwrap();

    let doc_path = root.join("doc.qmd");
    std::fs::write(
        &doc_path,
        "---\nreferences:\n  - id: oldkey\n    title: Inline\n---\n\nSee [@oldkey].\n",
    )
    .unwrap();

    let doc_uri = Uri::from_file_path(&doc_path).unwrap();
    let root_uri = Uri::from_file_path(root).unwrap();

    let server = TestLspServer::new();
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(
            doc_uri.as_str(),
            &std::fs::read_to_string(&doc_path).unwrap(),
            "quarto",
        )
        .await;

    let edit = server
        .rename(doc_uri.as_str(), 6, 7, "newkey")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");

    assert!(changes.contains_key(&doc_uri));
}

#[tokio::test]
async fn test_rename_citation_updates_csl_yaml() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    std::fs::write(root.join("_quarto.yml"), "project: default\n").unwrap();

    let bib_path = root.join("refs.yaml");
    std::fs::write(&bib_path, "- id: oldkey\n  title: Sample\n").unwrap();

    let doc_path = root.join("doc.qmd");
    std::fs::write(
        &doc_path,
        "---\nbibliography: refs.yaml\n---\n\nSee [@oldkey].\n",
    )
    .unwrap();

    let doc_uri = Uri::from_file_path(&doc_path).unwrap();
    let bib_uri = Uri::from_file_path(&bib_path).unwrap();
    let root_uri = Uri::from_file_path(root).unwrap();

    let server = TestLspServer::new();
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(
            doc_uri.as_str(),
            &std::fs::read_to_string(&doc_path).unwrap(),
            "quarto",
        )
        .await;

    let edit = server
        .rename(doc_uri.as_str(), 4, 7, "newkey")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");

    assert!(changes.contains_key(&doc_uri));
    assert!(changes.contains_key(&bib_uri));
}

#[tokio::test]
async fn test_rename_citation_updates_csl_json() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    std::fs::write(root.join("_quarto.yml"), "project: default\n").unwrap();

    let bib_path = root.join("refs.json");
    std::fs::write(&bib_path, "[{\"id\":\"oldkey\",\"title\":\"Sample\"}]").unwrap();

    let doc_path = root.join("doc.qmd");
    std::fs::write(
        &doc_path,
        "---\nbibliography: refs.json\n---\n\nSee [@oldkey].\n",
    )
    .unwrap();

    let doc_uri = Uri::from_file_path(&doc_path).unwrap();
    let bib_uri = Uri::from_file_path(&bib_path).unwrap();
    let root_uri = Uri::from_file_path(root).unwrap();

    let server = TestLspServer::new();
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(
            doc_uri.as_str(),
            &std::fs::read_to_string(&doc_path).unwrap(),
            "quarto",
        )
        .await;

    let edit = server
        .rename(doc_uri.as_str(), 4, 7, "newkey")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");

    assert!(changes.contains_key(&doc_uri));
    assert!(changes.contains_key(&bib_uri));
}

#[tokio::test]
async fn test_rename_citation_updates_ris() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    std::fs::write(root.join("_quarto.yml"), "project: default\n").unwrap();

    let bib_path = root.join("refs.ris");
    std::fs::write(&bib_path, "TY  - JOUR\nID  - oldkey\nER  - \n").unwrap();

    let doc_path = root.join("doc.qmd");
    std::fs::write(
        &doc_path,
        "---\nbibliography: refs.ris\n---\n\nSee [@oldkey].\n",
    )
    .unwrap();

    let doc_uri = Uri::from_file_path(&doc_path).unwrap();
    let bib_uri = Uri::from_file_path(&bib_path).unwrap();
    let root_uri = Uri::from_file_path(root).unwrap();

    let server = TestLspServer::new();
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(
            doc_uri.as_str(),
            &std::fs::read_to_string(&doc_path).unwrap(),
            "quarto",
        )
        .await;

    let edit = server
        .rename(doc_uri.as_str(), 4, 7, "newkey")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");

    assert!(changes.contains_key(&doc_uri));
    assert!(changes.contains_key(&bib_uri));
}

#[tokio::test]
async fn test_rename_chunk_label_updates_crossref_and_definition() {
    let server = TestLspServer::new();
    let content = r#"See @fig-plot.

```{r}
#| label: fig-plot
plot(1:10)
```
"#;
    server
        .open_document("file:///test.qmd", content, "quarto")
        .await;

    let edit = server
        .rename("file:///test.qmd", 0, 7, "fig-renamed")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.qmd".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.new_text == "fig-renamed"),
        "expected rename edits to use new key"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected crossref reference edit"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 3),
        "expected chunk label definition edit"
    );
}

#[tokio::test]
async fn test_rename_numbered_example_label_updates_definition_and_reference() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    let doc_path = root.join("doc.qmd");
    std::fs::write(root.join("panache.toml"), "flavor = \"pandoc\"\n").unwrap();
    let server = TestLspServer::new();
    let content = "(@good) First example.\n\nAs (@good) illustrates, details follow.\n";
    std::fs::write(&doc_path, content).unwrap();
    let root_uri = Uri::from_file_path(root).expect("root uri");
    let doc_uri = Uri::from_file_path(&doc_path).expect("doc uri");
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(doc_uri.as_str(), content, "quarto")
        .await;

    let edit = server
        .rename(doc_uri.as_str(), 2, 7, "better")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.new_text == "better"),
        "expected rename edits to use new example label"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected example definition edit"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 2),
        "expected example reference edit"
    );
}

#[tokio::test]
async fn test_rename_numbered_example_label_extension_off_returns_none() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    let doc_path = root.join("doc.qmd");
    std::fs::write(
        root.join("panache.toml"),
        "flavor = \"pandoc\"\n[extensions]\nexample-lists = false\n",
    )
    .unwrap();
    let server = TestLspServer::new();
    let content = "(@good) First example.\n\nAs (@good) illustrates.\n";
    std::fs::write(&doc_path, content).unwrap();
    let root_uri = Uri::from_file_path(root).expect("root uri");
    let doc_uri = Uri::from_file_path(&doc_path).expect("doc uri");
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(doc_uri.as_str(), content, "quarto")
        .await;

    let edit = server.rename(doc_uri.as_str(), 2, 7, "better").await;
    assert!(
        edit.is_none(),
        "expected no rename edit when example_lists is disabled"
    );
}

#[tokio::test]
async fn test_rename_bookdown_chunk_label_updates_crossrefs_and_definition() {
    let server = TestLspServer::new();
    let content = r#"Figure \@ref(fig:a-label).

```{r}
#| label: a-label
#| fig-cap: "A caption."
plot(1, 1)
```
"#;
    server
        .open_document("file:///test.Rmd", content, "rmarkdown")
        .await;

    let edit = server
        .rename("file:///test.Rmd", 0, 16, "renamed-label")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.Rmd".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.new_text == "renamed-label"),
        "expected rename edits to use new key"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected bookdown crossref reference edit"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 3),
        "expected chunk label definition edit"
    );
}

#[tokio::test]
async fn test_rename_bookdown_theorem_crossref_updates_div_id() {
    let server = TestLspServer::new();
    let content = r#"Exercise \@ref(exr:mu).

::: {#mu .exercise}
foobar
:::
"#;
    server
        .open_document("file:///test.Rmd", content, "rmarkdown")
        .await;

    let edit = server
        .rename("file:///test.Rmd", 0, 18, "renamed-label")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.Rmd".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.new_text == "renamed-label"),
        "expected rename edits to use new key"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected theorem crossref reference edit"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 2),
        "expected fenced div id edit"
    );
}

#[tokio::test]
async fn test_rename_heading_id_is_case_sensitive() {
    let server = TestLspServer::new();
    let content = r#"# Heading {#em}

A reference to [Heading](#em).

# Heading {#EM}

A reference to [Heading](#EM).
"#;
    server
        .open_document("file:///test.md", content, "markdown")
        .await;

    let edit = server
        .rename("file:///test.md", 6, 27, "renamed")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.md".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert_eq!(edits.len(), 2);
    assert!(edits.iter().all(|e| e.new_text == "renamed"));
    assert!(edits.iter().any(|e| e.range.start.line == 4));
    assert!(edits.iter().any(|e| e.range.start.line == 6));
}

#[tokio::test]
async fn test_rename_bookdown_section_crossref_with_hyphenated_slug() {
    let server = TestLspServer::new();
    let content = r#"# Heading

A ref to \@ref(heading).

## Heading 2

A ref to \@ref(heading-2).
"#;
    server
        .open_document("file:///test.Rmd", content, "rmarkdown")
        .await;

    let edit = server
        .rename("file:///test.Rmd", 6, 16, "renamed-section")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.Rmd".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.new_text == "renamed-section"),
        "expected rename edits to use new key"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 6),
        "expected bookdown section crossref edit"
    );
    assert!(
        edits
            .iter()
            .any(|e| e.range.start.line == 4 && e.new_text == " {#renamed-section}"),
        "expected explicit heading id insertion for implicit heading slug"
    );
}

#[tokio::test]
async fn test_rename_executable_chunk_label_updates_crossref_and_definition() {
    let server = TestLspServer::new();
    let content = r#"See @fig-my-label.

```{r fig-my-label}
plot(1, 1)
```
"#;
    server
        .open_document("file:///test.qmd", content, "quarto")
        .await;

    let edit = server
        .rename("file:///test.qmd", 2, 10, "fig-renamed")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.qmd".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.new_text == "fig-renamed"),
        "expected rename edits to use new key"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected crossref reference edit"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 2),
        "expected chunk label definition edit"
    );
}

#[tokio::test]
async fn test_rename_hashpipe_label_with_hyphen_updates_from_value_cursor() {
    let server = TestLspServer::new();
    let content = r#"See @fig-my-other-label.

```{r}
#| label: fig-my-other-label
plot(1, 1)
```
"#;
    server
        .open_document("file:///test.qmd", content, "quarto")
        .await;

    let edit = server
        .rename("file:///test.qmd", 3, 15, "fig-renamed")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.qmd".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected crossref usage edit"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 3),
        "expected chunk label declaration edit"
    );
}

#[tokio::test]
async fn test_rename_chunk_option_label_with_hyphen_updates_from_value_cursor() {
    let server = TestLspServer::new();
    let content = r#"See @fig-my-other-label.

```{r, label = "fig-my-other-label"}
plot(1, 1)
```
"#;
    server
        .open_document("file:///test.qmd", content, "quarto")
        .await;

    let edit = server
        .rename("file:///test.qmd", 2, 18, "fig-renamed")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.qmd".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected crossref usage edit"
    );
    assert!(
        edits.iter().any(|e| e.range.start.line == 2),
        "expected chunk option declaration edit"
    );
}

#[tokio::test]
async fn test_rename_returns_none_inside_yaml_frontmatter() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let root = temp_dir.path();
    fs::write(root.join("_quarto.yml"), "project: default\n").unwrap();
    fs::write(
        root.join("refs.bib"),
        "@article{known,\n  title = {Known}\n}\n",
    )
    .unwrap();

    let doc_path = root.join("doc.qmd");
    fs::write(
        &doc_path,
        "---\ntitle: \"@known\"\nbibliography: refs.bib\n---\n\nSee [@known].\n",
    )
    .unwrap();

    let doc_uri = Uri::from_file_path(&doc_path).unwrap();
    let root_uri = Uri::from_file_path(root).unwrap();

    let server = TestLspServer::new();
    server.initialize(root_uri.as_str()).await;
    server
        .open_document(
            doc_uri.as_str(),
            &fs::read_to_string(&doc_path).unwrap(),
            "quarto",
        )
        .await;

    let edit = server.rename(doc_uri.as_str(), 1, 10, "renamed").await;
    assert!(
        edit.is_none(),
        "Expected no rename edits when cursor is inside YAML frontmatter"
    );
}

#[tokio::test]
async fn test_rename_heading_reference_updates_shortcut_and_hash_links() {
    let server = TestLspServer::new();
    let content = "# Heading {#heading}\n\nA ref to [heading].\n\nA ref to [foobar](#heading).\n";
    server
        .open_document("file:///test.md", content, "markdown")
        .await;

    let edit = server
        .rename("file:///test.md", 2, 11, "renamed-heading")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.md".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert!(
        edits.iter().any(|e| e.range.start.line == 0),
        "expected heading id declaration edit"
    );
    assert!(
        edits
            .iter()
            .any(|e| e.range.start.line == 2 && e.new_text == "renamed-heading"),
        "expected shortcut heading reference edit"
    );
    assert!(
        edits
            .iter()
            .any(|e| e.range.start.line == 4 && e.new_text == "renamed-heading"),
        "expected hash heading reference edit"
    );
}

#[tokio::test]
async fn test_rename_footnote_updates_references_and_definition_id() {
    let server = TestLspServer::new();
    // Blank line after the footnote def is required so `Another[^1].` is a
    // sibling paragraph rather than a lazy-continuation line of the def body
    // (pandoc-native: `[^1]` inside a footnote-def body is literal text).
    let content =
        "Simple footnote[^1] in text.\n\n[^1]: This is a simple footnote.\n\nAnother[^1].\n";
    server
        .open_document("file:///test.md", content, "markdown")
        .await;

    let edit = server
        .rename("file:///test.md", 0, 17, "note")
        .await
        .expect("rename edit");
    let changes = edit.changes.expect("changes");
    let doc_uri: Uri = "file:///test.md".parse().unwrap();
    let edits = changes.get(&doc_uri).expect("doc edits");

    assert_eq!(
        edits.iter().filter(|e| e.new_text == "note").count(),
        3,
        "expected two references plus one definition id edit"
    );
    assert!(edits.iter().any(|e| e.range.start.line == 0));
    assert!(edits.iter().any(|e| e.range.start.line == 2));
    assert!(edits.iter().any(|e| e.range.start.line == 4));
}
