Documentation
use indoc::indoc;
use liwe::model::config::{Configuration, LibraryOptions, MarkdownOptions};
use std::fs::{create_dir_all, read_to_string, write};
use std::path::Path;
use std::process::{Command, Output};
use tempfile::TempDir;

fn setup(docs: Vec<(&str, &str)>) -> TempDir {
    let temp_dir = TempDir::new().expect("tempdir");
    let temp_path = temp_dir.path();
    create_dir_all(temp_path.join(".iwe")).expect("mkdir .iwe");
    let config = Configuration {
        library: LibraryOptions {
            path: "".to_string(),
            ..Default::default()
        },
        markdown: MarkdownOptions {
            refs_extension: "".to_string(),
            ..Default::default()
        },
        ..Default::default()
    };
    write(
        temp_path.join(".iwe").join("config.toml"),
        toml::to_string(&config).expect("config"),
    )
    .expect("write config");
    for (key, content) in docs {
        write(temp_path.join(format!("{}.md", key)), content).expect("write doc");
    }
    temp_dir
}

fn run_update(work_dir: &Path, args: &[&str]) -> Output {
    let mut command = Command::new(crate::common::get_iwe_binary_path());
    command.arg("update").current_dir(work_dir);
    for arg in args {
        command.arg(arg);
    }
    command.output().expect("run iwe update")
}

#[test]
fn replace_text_and_set_apply_atomically() {
    let temp = setup(vec![(
        "roadmap",
        indoc! {"
            # Roadmap

            ## Status

            old status
        "},
    )]);
    let output = run_update(
        temp.path(),
        &[
            "-k",
            "roadmap",
            "--replace-text",
            r#"{ $paragraph: "old status", from: "old status", to: reviewed, expect: 1 }"#,
            "--set",
            "reviewed=true",
        ],
    );
    assert!(output.status.success());
    assert_eq!(
        read_to_string(temp.path().join("roadmap.md")).unwrap(),
        indoc! {"
            ---
            reviewed: true
            ---

            # Roadmap

            ## Status

            reviewed
        "}
    );
}

#[test]
fn append_under_header() {
    let temp = setup(vec![(
        "notes",
        indoc! {"
            # Status

            existing
        "},
    )]);
    let output = run_update(
        temp.path(),
        &[
            "-k",
            "notes",
            "--append",
            r#"{ $header: Status, content: "Reviewed." }"#,
        ],
    );
    assert!(output.status.success());
    assert_eq!(
        read_to_string(temp.path().join("notes.md")).unwrap(),
        indoc! {"
            # Status

            existing

            Reviewed.
        "}
    );
}

#[test]
fn expect_violation_aborts_without_writing() {
    let original = indoc! {"
        # Doc

        drop

        drop
    "};
    let temp = setup(vec![("multi", original)]);
    let output = run_update(
        temp.path(),
        &[
            "-k",
            "multi",
            "--delete",
            r#"{ $paragraph: { $text: drop }, expect: 1 }"#,
        ],
    );
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert_eq!(
        stderr,
        indoc! {"
            error: $delete expects 1 block, selected 2
              multi › \"drop\"
              multi › \"drop\"
            hint: narrow with $within or $matches, or raise expect
        "}
    );
    assert_eq!(
        read_to_string(temp.path().join("multi.md")).unwrap(),
        original
    );
}

#[test]
fn repeatable_key_updates_multiple() {
    let temp = setup(vec![
        ("a", "# A\n\ntodo item\n"),
        ("b", "# B\n\ntodo item\n"),
        ("c", "# C\n\nleft alone\n"),
    ]);
    let output = run_update(
        temp.path(),
        &[
            "-k",
            "a",
            "-k",
            "b",
            "--replace-text",
            r#"{ $paragraph: "todo item", from: todo, to: done }"#,
        ],
    );
    assert!(output.status.success());
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        "Updated 2 document(s)\n"
    );
    assert_eq!(
        read_to_string(temp.path().join("a.md")).unwrap(),
        "# A\n\ndone item\n"
    );
    assert_eq!(
        read_to_string(temp.path().join("b.md")).unwrap(),
        "# B\n\ndone item\n"
    );
    assert_eq!(
        read_to_string(temp.path().join("c.md")).unwrap(),
        "# C\n\nleft alone\n"
    );
}

#[test]
fn noop_reports_honestly_and_does_not_rewrite() {
    let original = "# C\n\nkeep\n";
    let temp = setup(vec![("c", original)]);
    let file = temp.path().join("c.md");
    let before = std::fs::metadata(&file).unwrap().modified().unwrap();
    let output = run_update(
        temp.path(),
        &[
            "-k",
            "c",
            "--delete",
            r#"{ $paragraph: absent, expect: 0 }"#,
        ],
    );
    assert!(output.status.success());
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        "Matched 1 document(s), 0 changed\n"
    );
    assert_eq!(read_to_string(&file).unwrap(), original);
    let after = std::fs::metadata(&file).unwrap().modified().unwrap();
    assert_eq!(before, after, "no-op must not rewrite the file");
}

#[test]
fn dry_run_does_not_write() {
    let original = indoc! {"
        # Doc

        para
    "};
    let temp = setup(vec![("d", original)]);
    let output = run_update(
        temp.path(),
        &[
            "-k",
            "d",
            "--append",
            r#"{ $header: Doc, content: added }"#,
            "--dry-run",
        ],
    );
    assert!(output.status.success());
    assert_eq!(read_to_string(temp.path().join("d.md")).unwrap(), original);
}

#[test]
fn document_expect_violation_aborts_without_writing() {
    let a = "# Alpha\n\nkeep\n";
    let b = "# Beta\n\nkeep\n";
    let temp = setup(vec![("a", a), ("b", b)]);
    let output = run_update(
        temp.path(),
        &["--filter", "{}", "--set", "reviewed=true", "--expect", "1"],
    );
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert_eq!(
        stderr,
        indoc! {"
            error: update expects 1 document, matched 2
              a › Alpha
              b › Beta
            hint: adjust the filter or raise expect
        "}
    );
    assert_eq!(read_to_string(temp.path().join("a.md")).unwrap(), a);
    assert_eq!(read_to_string(temp.path().join("b.md")).unwrap(), b);
}

#[test]
fn strict_without_guards_aborts_without_writing() {
    let original = "# Doc\n\npara\n";
    let temp = setup(vec![("d", original)]);
    let output = run_update(
        temp.path(),
        &[
            "--filter",
            "{}",
            "--delete",
            r#"{ $paragraph: {} }"#,
            "--strict",
        ],
    );
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert_eq!(
        stderr,
        indoc! {"
            error: --strict requires an expect guard on every mutating application; missing: document-level --expect, $delete expect
            hint: state the expected count — 1 for a precision edit, '{ min: 1 }' for a bulk edit that must match, '{ min: 0 }' when zero is acceptable
        "}
    );
    assert_eq!(read_to_string(temp.path().join("d.md")).unwrap(), original);
}

#[test]
fn strict_with_all_guards_applies() {
    let temp = setup(vec![("d", "# Doc\n\npara\n")]);
    let output = run_update(
        temp.path(),
        &[
            "--filter",
            "{}",
            "--delete",
            r#"{ $paragraph: {}, expect: 1 }"#,
            "--strict",
            "--expect",
            "1",
        ],
    );
    assert!(output.status.success());
    assert_eq!(read_to_string(temp.path().join("d.md")).unwrap(), "# Doc\n");
}