use super::common::{hyalo_no_hints, md, write_md};
use std::fs;
use tempfile::TempDir;

// ---------------------------------------------------------------------------
// Helper: run `hyalo append` and return (status, parsed JSON, stderr)
// ---------------------------------------------------------------------------

fn append_json(
    tmp: &TempDir,
    extra_args: &[&str],
) -> (std::process::ExitStatus, serde_json::Value, String) {
    let mut cmd = hyalo_no_hints();
    cmd.args(["--dir", tmp.path().to_str().unwrap()]);
    cmd.arg("append");
    cmd.args(extra_args);
    let output = cmd.output().unwrap();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    let json: serde_json::Value = if output.status.success() {
        serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
            let stdout = String::from_utf8_lossy(&output.stdout);
            panic!("invalid JSON: {e}\nstdout: {stdout}\nstderr: {stderr}")
        })
    } else {
        serde_json::Value::Null
    };
    (output.status, json, stderr)
}

// ---------------------------------------------------------------------------
// --property K=V: create new list when property is absent
// ---------------------------------------------------------------------------

#[test]
fn append_property_creates_new_list() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "note.md",
        md!(r"
---
title: Note
---
"),
    );

    let (status, json, stderr) = append_json(
        &tmp,
        &["--property", "aliases=my-note", "--file", "note.md"],
    );
    assert!(status.success(), "stderr: {stderr}");

    assert_eq!(json["results"]["property"], "aliases");
    assert_eq!(json["results"]["value"], "my-note");
    assert_eq!(json["results"]["modified"].as_array().unwrap().len(), 1);
    assert_eq!(json["results"]["skipped"].as_array().unwrap().len(), 0);

    let content = fs::read_to_string(tmp.path().join("note.md")).unwrap();
    assert!(content.contains("my-note"), "value not written:\n{content}");
    // Should be serialized as a YAML list
    assert!(content.contains("- "), "expected list syntax:\n{content}");
}

// ---------------------------------------------------------------------------
// --property K=V: append to existing list
// ---------------------------------------------------------------------------

#[test]
fn append_property_appends_to_existing_list() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "note.md",
        md!(r"
---
aliases:
  - old-name
---
"),
    );

    let (status, json, stderr) = append_json(
        &tmp,
        &["--property", "aliases=new-name", "--file", "note.md"],
    );
    assert!(status.success(), "stderr: {stderr}");

    assert_eq!(json["results"]["modified"].as_array().unwrap().len(), 1);

    let content = fs::read_to_string(tmp.path().join("note.md")).unwrap();
    assert!(
        content.contains("old-name"),
        "existing value removed:\n{content}"
    );
    assert!(
        content.contains("new-name"),
        "new value not appended:\n{content}"
    );
}

// ---------------------------------------------------------------------------
// --property K=V: skip duplicate in existing list
// ---------------------------------------------------------------------------

#[test]
fn append_property_skips_duplicate_in_list() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "note.md",
        md!(r"
---
aliases:
  - my-note
---
"),
    );

    let (status, json, stderr) = append_json(
        &tmp,
        &["--property", "aliases=my-note", "--file", "note.md"],
    );
    assert!(status.success(), "stderr: {stderr}");

    assert_eq!(json["results"]["modified"].as_array().unwrap().len(), 0);
    assert_eq!(json["results"]["skipped"].as_array().unwrap().len(), 1);
}

// ---------------------------------------------------------------------------
// --property K=V: promote scalar to list
// ---------------------------------------------------------------------------

#[test]
fn append_property_promotes_scalar_to_list() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "note.md",
        md!(r"
---
author: Alice
---
"),
    );

    let (status, json, stderr) =
        append_json(&tmp, &["--property", "author=Bob", "--file", "note.md"]);
    assert!(status.success(), "stderr: {stderr}");

    assert_eq!(json["results"]["modified"].as_array().unwrap().len(), 1);

    let content = fs::read_to_string(tmp.path().join("note.md")).unwrap();
    assert!(
        content.contains("Alice"),
        "original scalar removed:\n{content}"
    );
    assert!(
        content.contains("Bob"),
        "new value not appended:\n{content}"
    );
    assert!(
        content.contains("- "),
        "expected list syntax after promotion:\n{content}"
    );
}

// ---------------------------------------------------------------------------
// --where-property / --where-tag filter tests
// ---------------------------------------------------------------------------

#[test]
fn append_where_property_match() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "target.md",
        md!(r"
---
title: Target
status: review
aliases:
  - original-name
---
"),
    );
    write_md(
        tmp.path(),
        "skip.md",
        md!(r"
---
title: Skip
status: draft
aliases:
  - other-name
---
"),
    );

    let (status, json, stderr) = append_json(
        &tmp,
        &[
            "--property",
            "aliases=canonical-name",
            "--where-property",
            "status=review",
            "--glob",
            "**/*.md",
        ],
    );
    assert!(status.success(), "stderr: {stderr}");

    assert_eq!(
        json["results"]["modified"].as_array().unwrap().len(),
        1,
        "expected 1 modified: {json}"
    );
    assert_eq!(
        json["results"]["skipped"].as_array().unwrap().len(),
        0,
        "expected 0 skipped (skip.md was filtered out, not skipped): {json}"
    );

    let target_content = fs::read_to_string(tmp.path().join("target.md")).unwrap();
    assert!(
        target_content.contains("canonical-name"),
        "target.md should have alias appended:\n{target_content}"
    );
    assert!(
        target_content.contains("original-name"),
        "target.md should retain original alias:\n{target_content}"
    );

    let skip_content = fs::read_to_string(tmp.path().join("skip.md")).unwrap();
    assert!(
        !skip_content.contains("canonical-name"),
        "skip.md should be untouched:\n{skip_content}"
    );
}

#[test]
fn append_where_tag_match() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "tagged.md",
        md!(r"
---
title: Tagged
tags:
  - backend
---
"),
    );
    write_md(
        tmp.path(),
        "untagged.md",
        md!(r"
---
title: Untagged
---
"),
    );

    let (status, json, stderr) = append_json(
        &tmp,
        &[
            "--property",
            "aliases=backend-note",
            "--where-tag",
            "backend",
            "--glob",
            "**/*.md",
        ],
    );
    assert!(status.success(), "stderr: {stderr}");

    assert_eq!(
        json["results"]["modified"].as_array().unwrap().len(),
        1,
        "expected only tagged file to be modified: {json}"
    );

    let tagged_content = fs::read_to_string(tmp.path().join("tagged.md")).unwrap();
    assert!(
        tagged_content.contains("backend-note"),
        "tagged.md should have alias appended:\n{tagged_content}"
    );

    let untagged_content = fs::read_to_string(tmp.path().join("untagged.md")).unwrap();
    assert!(
        !untagged_content.contains("backend-note"),
        "untagged.md should be untouched:\n{untagged_content}"
    );
}

// ---------------------------------------------------------------------------
// Guard: --file or --glob required
// ---------------------------------------------------------------------------

#[test]
fn append_requires_file_or_glob() {
    let tmp = TempDir::new().unwrap();
    let mut cmd = hyalo_no_hints();
    cmd.args(["--dir", tmp.path().to_str().unwrap()]);
    cmd.args(["append", "--property", "aliases=x"]);
    let output = cmd.output().unwrap();
    assert!(!output.status.success());
    assert_eq!(output.status.code(), Some(1));
}

// ---------------------------------------------------------------------------
// Guard: at least one --property required
// Note: `--property` is a required clap argument for `append`, so the CLI
// exits with code 2 (clap usage error) before reaching the application guard.
// ---------------------------------------------------------------------------

#[test]
fn append_requires_at_least_one_property() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let mut cmd = hyalo_no_hints();
    cmd.args(["--dir", tmp.path().to_str().unwrap()]);
    cmd.args(["append", "--file", "note.md"]);
    let output = cmd.output().unwrap();
    assert!(!output.status.success());
}

// ---------------------------------------------------------------------------
// Guard: invalid K=V (no `=`) returns error
// ---------------------------------------------------------------------------

#[test]
fn append_invalid_kv_no_equals_returns_error() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let mut cmd = hyalo_no_hints();
    cmd.args(["--dir", tmp.path().to_str().unwrap()]);
    cmd.args([
        "append",
        "--property",
        "no-equals-sign",
        "--file",
        "note.md",
    ]);
    let output = cmd.output().unwrap();
    assert!(!output.status.success());
    assert_eq!(output.status.code(), Some(1));
}

// ---------------------------------------------------------------------------
// Guard: invalid K=V (empty key) returns error
// ---------------------------------------------------------------------------

#[test]
fn append_empty_property_name_returns_error() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let mut cmd = hyalo_no_hints();
    cmd.args(["--dir", tmp.path().to_str().unwrap()]);
    cmd.args(["append", "--property", "=value", "--file", "note.md"]);
    let output = cmd.output().unwrap();
    assert!(!output.status.success());
    assert_eq!(output.status.code(), Some(1));
}

// ---------------------------------------------------------------------------
// Body content is preserved after append
// ---------------------------------------------------------------------------

#[test]
fn append_preserves_file_body() {
    let tmp = TempDir::new().unwrap();
    let body = "# Heading\n\nSome body content here.\n";
    write_md(
        tmp.path(),
        "note.md",
        &format!("---\ntitle: Note\n---\n{body}"),
    );

    let (status, _json, stderr) = append_json(
        &tmp,
        &["--property", "aliases=my-note", "--file", "note.md"],
    );
    assert!(status.success(), "stderr: {stderr}");

    let content = fs::read_to_string(tmp.path().join("note.md")).unwrap();
    assert!(content.contains(body), "body was corrupted:\n{content}");
}

// ---------------------------------------------------------------------------
// Append on file with no frontmatter (creates frontmatter)
// ---------------------------------------------------------------------------

#[test]
fn append_on_file_with_no_frontmatter_creates_frontmatter() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "bare.md",
        "# No frontmatter\n\nJust body text.\n",
    );

    let (status, json, stderr) =
        append_json(&tmp, &["--property", "aliases=test", "--file", "bare.md"]);
    assert!(status.success(), "stderr: {stderr}");

    assert_eq!(
        json["results"]["modified"]
            .as_array()
            .expect("field 'modified' should be an array")
            .len(),
        1,
        "expected 1 modified: {json}"
    );

    let content = fs::read_to_string(tmp.path().join("bare.md")).unwrap();
    // Should have created frontmatter with the list property
    assert!(
        content.contains("---"),
        "expected frontmatter delimiters:\n{content}"
    );
    assert!(
        content.contains("test"),
        "expected appended value in frontmatter:\n{content}"
    );
    // Body should be preserved
    assert!(
        content.contains("# No frontmatter"),
        "body should be preserved:\n{content}"
    );
    assert!(
        content.contains("Just body text."),
        "body content should be preserved:\n{content}"
    );
}

// ---------------------------------------------------------------------------
// Multi-file --file targeting
// ---------------------------------------------------------------------------

#[test]
fn append_multi_file_modifies_all() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "a.md", "---\ntitle: A\n---\n");
    write_md(tmp.path(), "b.md", "---\ntitle: B\n---\n");

    let mut cmd = hyalo_no_hints();
    cmd.args(["--dir", tmp.path().to_str().unwrap()]);
    cmd.args([
        "append",
        "--property",
        "aliases=alias1",
        "--file",
        "a.md",
        "--file",
        "b.md",
    ]);
    let output = cmd.output().unwrap();
    assert!(output.status.success());

    let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(json["results"]["modified"].as_array().unwrap().len(), 2);
}

// ---------------------------------------------------------------------------
// --format text produces structured mutation output
// ---------------------------------------------------------------------------

#[test]
fn append_format_text_shows_counts() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "note.md",
        md!(r"
---
title: Note
tags:
  - rust
---
"),
    );

    let output = hyalo_no_hints()
        .args(["--dir", tmp.path().to_str().unwrap()])
        .args(["--format", "text"])
        .args(["append", "--property", "tags=cli", "--file", "note.md"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    // Should show modified count
    assert!(stdout.contains("1/1 modified"), "counts: {stdout}");
    // Should list the file
    assert!(stdout.contains("note.md"), "modified file: {stdout}");
}

// ---------------------------------------------------------------------------
// Filter guard: reject operator suffixes in --property
// ---------------------------------------------------------------------------

#[test]
fn append_rejects_gte_filter_in_property() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let (status, _, stderr) =
        append_json(&tmp, &["--property", "priority>=3", "--file", "note.md"]);
    assert!(!status.success(), "should fail; stderr: {stderr}");
    assert!(
        stderr.contains("--where-property"),
        "hint missing; stderr: {stderr}"
    );
}

#[test]
fn append_rejects_lte_filter_in_property() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let (status, _, stderr) =
        append_json(&tmp, &["--property", "priority<=3", "--file", "note.md"]);
    assert!(!status.success(), "should fail; stderr: {stderr}");
    assert!(
        stderr.contains("--where-property"),
        "hint missing; stderr: {stderr}"
    );
}

#[test]
fn append_rejects_neq_filter_in_property() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let (status, _, stderr) =
        append_json(&tmp, &["--property", "status!=draft", "--file", "note.md"]);
    assert!(!status.success(), "should fail; stderr: {stderr}");
    assert!(
        stderr.contains("--where-property"),
        "hint missing; stderr: {stderr}"
    );
}

#[test]
fn append_rejects_regex_filter_in_property() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let (status, _, stderr) =
        append_json(&tmp, &["--property", "name~=pattern", "--file", "note.md"]);
    assert!(!status.success(), "should fail; stderr: {stderr}");
    assert!(
        stderr.contains("--where-property"),
        "hint missing; stderr: {stderr}"
    );
}

#[test]
fn append_accepts_plain_kv_property() {
    // Ensure normal K=V still works after guard
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let (status, json, stderr) = append_json(
        &tmp,
        &["--property", "aliases=my-note", "--file", "note.md"],
    );
    assert!(status.success(), "stderr: {stderr}");
    assert_eq!(json["results"]["property"], "aliases");
}

#[test]
fn append_accepts_list_property() {
    // Ensure K=[a,b] still works
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");
    let (status, json, stderr) =
        append_json(&tmp, &["--property", "tags=[a, b]", "--file", "note.md"]);
    assert!(status.success(), "stderr: {stderr}");
    assert_eq!(json["results"]["property"], "tags");
}

// ---------------------------------------------------------------------------
// --dry-run: preview without modifying
// ---------------------------------------------------------------------------

#[test]
fn append_dry_run_does_not_modify() {
    let tmp = TempDir::new().unwrap();
    write_md(
        tmp.path(),
        "note.md",
        md!(r"
---
title: Note
---
"),
    );

    let (status, json, stderr) = append_json(
        &tmp,
        &[
            "--property",
            "aliases=my-note",
            "--file",
            "note.md",
            "--dry-run",
        ],
    );
    assert!(status.success(), "stderr: {stderr}");
    assert_eq!(json["results"]["dry_run"], true);
    assert_eq!(json["results"]["modified"].as_array().unwrap().len(), 1);

    // File must NOT have been modified
    let content = fs::read_to_string(tmp.path().join("note.md")).unwrap();
    assert!(
        !content.contains("aliases"),
        "file was modified despite --dry-run:\n{content}"
    );
}

#[test]
fn append_without_dry_run_has_dry_run_false() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "note.md", "---\ntitle: x\n---\n");

    let (status, json, stderr) = append_json(
        &tmp,
        &["--property", "aliases=my-note", "--file", "note.md"],
    );
    assert!(status.success(), "stderr: {stderr}");
    assert_eq!(json["results"]["dry_run"], false);

    // File should actually be modified
    let content = fs::read_to_string(tmp.path().join("note.md")).unwrap();
    assert!(
        content.contains("my-note"),
        "file was not written:\n{content}"
    );
}
