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

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Write a `.hyalo.toml` with a `[schema]` block into `dir`.
fn write_schema_toml(dir: &std::path::Path, content: &str) {
    std::fs::write(dir.join(".hyalo.toml"), content).unwrap();
}

/// Set up a minimal vault for lint tests.
fn setup_vault_with_schema() -> TempDir {
    let tmp = TempDir::new().unwrap();

    // Write schema
    write_schema_toml(
        tmp.path(),
        r#"dir = "."

[schema.default]
required = ["title"]

[schema.types.note]
required = ["title", "date"]

[schema.types.note.properties.date]
type = "date"

[schema.types.note.properties.status]
type = "enum"
values = ["draft", "published"]
"#,
    );

    // Clean file
    write_md(
        tmp.path(),
        "clean.md",
        md!(r"
---
title: Clean Note
type: note
date: 2026-04-13
tags:
  - test
---
Body.
"),
    );

    // File missing required property
    write_md(
        tmp.path(),
        "missing_date.md",
        md!(r"
---
title: Missing Date
type: note
tags:
  - test
---
Body.
"),
    );

    // File with invalid enum value
    write_md(
        tmp.path(),
        "bad_status.md",
        md!(r"
---
title: Bad Status
type: note
date: 2026-04-13
status: wip
tags:
  - test
---
Body.
"),
    );

    tmp
}

// ---------------------------------------------------------------------------
// Basic lint tests
// ---------------------------------------------------------------------------

#[test]
fn lint_no_schema_exits_zero() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "a.md", "---\ntitle: Hello\n---\nBody\n");

    hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint"])
        .assert()
        .success()
        .code(0);
}

#[test]
fn lint_clean_vault_exits_zero() {
    let tmp = TempDir::new().unwrap();
    write_schema_toml(
        tmp.path(),
        "dir = \".\"\n[schema.default]\nrequired = [\"title\"]\n",
    );
    write_md(
        tmp.path(),
        "clean.md",
        "---\ntitle: Hello\ntype: note\ntags:\n  - test\n---\nBody\n",
    );

    hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint"])
        .assert()
        .success()
        .code(0);
}

#[test]
fn lint_exits_one_when_errors_found() {
    let tmp = setup_vault_with_schema();

    hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint"])
        .assert()
        .code(1);
}

#[test]
fn lint_text_output_shows_missing_required() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "missing_date.md"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    assert!(
        stdout.contains("missing_date.md"),
        "expected filename in output"
    );
    assert!(
        stdout.contains("missing required property"),
        "expected error message"
    );
    assert!(stdout.contains("date"), "expected property name");
}

#[test]
fn lint_text_output_shows_enum_violation() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "bad_status.md"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    assert!(stdout.contains("bad_status.md"), "expected filename");
    assert!(stdout.contains("wip"), "expected bad value in output");
    assert!(stdout.contains("not in"), "expected enum violation message");
}

#[test]
fn lint_suggests_nearest_enum_value() {
    let tmp = TempDir::new().unwrap();
    write_schema_toml(
        tmp.path(),
        r#"dir = "."
[schema.types.note.properties.status]
type = "enum"
values = ["planned", "in-progress", "completed"]
"#,
    );
    write_md(
        tmp.path(),
        "a.md",
        "---\ntitle: A\ntype: note\nstatus: planed\ntags:\n  - test\n---\nBody\n",
    );

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "a.md"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    assert!(
        stdout.contains("planned"),
        "expected suggestion 'planned' for misspelling 'planed'"
    );
}

#[test]
fn lint_single_file_positional() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--format", "text", "clean.md"])
        .output()
        .unwrap();

    let exit = output.status.code().unwrap();
    assert_eq!(exit, 0, "clean file should exit 0");
    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    assert!(
        stdout.contains("no issues"),
        "expected no issues message: {stdout}"
    );
}

#[test]
fn lint_single_file_flag() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--file", "clean.md"])
        .output()
        .unwrap();

    let exit = output.status.code().unwrap();
    assert_eq!(exit, 0, "clean file should exit 0");
}

#[test]
fn lint_glob_flag() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--glob", "*.md"])
        .output()
        .unwrap();

    // vault has errors so exit 1
    let exit = output.status.code().unwrap();
    assert_eq!(exit, 1, "glob over errored vault should exit 1");
}

#[test]
fn lint_json_output() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--format", "json", "missing_date.md"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let val: serde_json::Value = serde_json::from_str(stdout)
        .unwrap_or_else(|e| panic!("expected JSON output, got: {stdout}\nerr: {e}"));

    // The pipeline wraps the lint output in the standard envelope:
    // {"results": {"files": [...], "total": N}, "hints": [...]}
    let inner = &val["results"];
    assert!(inner.is_object(), "expected results object in envelope");
    assert!(inner["files"].is_array(), "expected files array");
    assert!(inner["total"].is_number(), "expected total field");

    let files = inner["files"].as_array().unwrap();
    assert!(!files.is_empty());
    let first = &files[0];
    assert!(first["file"].is_string(), "expected file field");
    assert!(first["violations"].is_array(), "expected violations array");

    let violations = first["violations"].as_array().unwrap();
    assert!(!violations.is_empty(), "expected at least one violation");
    let v = &violations[0];
    assert!(v["severity"].is_string(), "expected severity field");
    assert!(v["message"].is_string(), "expected message field");
}

#[test]
fn lint_no_type_property_warn() {
    let tmp = TempDir::new().unwrap();
    write_schema_toml(
        tmp.path(),
        r#"dir = "."
[schema.default]
required = ["title"]

[schema.types.note]
required = ["title"]
"#,
    );
    write_md(tmp.path(), "a.md", "---\ntitle: Hello\n---\nBody\n");

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "a.md"])
        .output()
        .unwrap();

    // Has warnings but no errors -> exit 0
    let exit = output.status.code().unwrap();
    assert_eq!(exit, 0, "warnings only should exit 0");
    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    assert!(
        stdout.contains("no 'type' property") || stdout.contains("warn"),
        "expected warning about missing type"
    );
}

#[test]
fn lint_unknown_type_uses_default_schema() {
    let tmp = TempDir::new().unwrap();
    write_schema_toml(
        tmp.path(),
        r#"dir = "."
[schema.default]
required = ["title"]

[schema.types.note]
required = ["title", "date"]
"#,
    );
    // File with type "unknown" — should only validate against default (title required)
    write_md(
        tmp.path(),
        "a.md",
        "---\ntitle: Hello\ntype: unknown\ntags:\n  - test\n---\nBody\n",
    );

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "a.md"])
        .output()
        .unwrap();

    // "date" is only required for type "note", not for "unknown"
    // So this should pass with exit 0 (title is present)
    let exit = output.status.code().unwrap();
    assert_eq!(exit, 0, "unknown type should validate against default only");
}

#[test]
fn lint_date_format_error() {
    let tmp = TempDir::new().unwrap();
    write_schema_toml(
        tmp.path(),
        r#"dir = "."
[schema.types.note.properties.date]
type = "date"
"#,
    );
    write_md(
        tmp.path(),
        "a.md",
        "---\ntitle: A\ntype: note\ndate: April 9\ntags:\n  - test\n---\nBody\n",
    );

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "a.md"])
        .output()
        .unwrap();

    let exit = output.status.code().unwrap();
    assert_eq!(exit, 1, "invalid date format should produce error");
    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    assert!(
        stdout.contains("expected date"),
        "expected date error message in output"
    );
}

#[test]
fn lint_string_pattern_error() {
    let tmp = TempDir::new().unwrap();
    write_schema_toml(
        tmp.path(),
        r#"dir = "."
[schema.types.note.properties.branch]
type = "string"
pattern = "^iter-\\d+/"
"#,
    );
    write_md(
        tmp.path(),
        "a.md",
        "---\ntitle: A\ntype: note\nbranch: feature/foo\ntags:\n  - test\n---\nBody\n",
    );

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "a.md"])
        .output()
        .unwrap();

    let exit = output.status.code().unwrap();
    assert_eq!(exit, 1, "pattern mismatch should produce error");
}

// ---------------------------------------------------------------------------
// Summary integration
// ---------------------------------------------------------------------------

#[test]
fn summary_shows_lint_count_when_schema_configured() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["--format", "json", "summary"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let val: serde_json::Value =
        serde_json::from_str(stdout).unwrap_or_else(|e| panic!("JSON parse: {e}\n{stdout}"));

    // When a schema is configured, results.schema should be present
    let schema_field = &val["results"]["schema"];
    assert!(
        !schema_field.is_null(),
        "expected schema field in summary when schema is configured"
    );
    assert!(
        schema_field["errors"].is_number(),
        "expected errors count in schema summary"
    );
    assert!(
        schema_field["warnings"].is_number(),
        "expected warnings count in schema summary"
    );
    assert!(
        schema_field["files_with_issues"].is_number(),
        "expected files_with_issues in schema summary"
    );
}

#[test]
fn summary_no_schema_field_without_config() {
    let tmp = TempDir::new().unwrap();
    write_md(tmp.path(), "a.md", "---\ntitle: Hello\n---\nBody\n");

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["--format", "json", "summary"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let val: serde_json::Value =
        serde_json::from_str(stdout).unwrap_or_else(|e| panic!("JSON parse: {e}\n{stdout}"));

    // No schema configured → schema field should be absent (null in JSON)
    assert!(
        val["results"]["schema"].is_null(),
        "schema field should be absent when no schema is configured"
    );
}

// ---------------------------------------------------------------------------
// Bug regression: lint JSON total counts violations, not files
// ---------------------------------------------------------------------------

#[test]
fn lint_json_total_counts_violations_not_files() {
    // Use a type-specific schema so we can have a clean file (no warnings) and
    // two files with exactly one error each.  The "no type property" warning is
    // suppressed by giving every file a `type` property.
    let tmp = TempDir::new().unwrap();
    write_schema_toml(
        tmp.path(),
        r#"dir = "."
[schema.default]
required = ["title"]

[schema.types.note]
required = ["title", "date"]

[schema.types.note.properties.date]
type = "date"
"#,
    );
    // Clean file: has both title and date → zero violations
    write_md(
        tmp.path(),
        "clean.md",
        "---\ntitle: OK\ntype: note\ndate: 2026-01-01\ntags:\n  - x\n---\nBody\n",
    );
    // Two files missing required 'date' → 1 error each, 0 warnings (type present)
    write_md(
        tmp.path(),
        "bad1.md",
        "---\ntitle: Bad One\ntype: note\ntags:\n  - x\n---\nBody\n",
    );
    write_md(
        tmp.path(),
        "bad2.md",
        "---\ntitle: Bad Two\ntype: note\ntags:\n  - x\n---\nBody\n",
    );

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--format", "json"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let val: serde_json::Value =
        serde_json::from_str(stdout).unwrap_or_else(|e| panic!("JSON parse: {e}\n{stdout}"));

    let results = &val["results"];
    let total = results["total"].as_u64().expect("total should be a number");
    let files_checked = results["files_checked"]
        .as_u64()
        .expect("files_checked should be a number");

    // 2 violations (one error per bad file), 3 files checked
    assert_eq!(
        total, 2,
        "total should count violations, not files: {results}"
    );
    assert_eq!(
        files_checked, 3,
        "files_checked should count all scanned files: {results}"
    );
    // Sanity: they must be different (this was the original bug)
    assert_ne!(
        total, files_checked,
        "total (violations) and files_checked must differ in this fixture"
    );
}

// ---------------------------------------------------------------------------
// Filter and limit tests
// ---------------------------------------------------------------------------

#[test]
fn lint_json_excludes_clean_files() {
    let tmp = setup_vault_with_schema();

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--format", "json"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let val: serde_json::Value = serde_json::from_str(stdout)
        .unwrap_or_else(|e| panic!("expected JSON output, got: {stdout}\nerr: {e}"));

    let inner = &val["results"];
    let files = inner["files"].as_array().unwrap();

    // Every file in the output should have at least one violation.
    for f in files {
        let violations = f["violations"].as_array().unwrap();
        assert!(
            !violations.is_empty(),
            "clean files should not appear in output: {}",
            f["file"]
        );
    }
}

#[test]
fn lint_limit_caps_output() {
    let tmp = setup_vault_with_schema();
    // setup_vault_with_schema already has missing_date.md and bad_status.md (2 files with violations)
    // Add a third to ensure we have more than 1 violated file.
    write_md(
        tmp.path(),
        "extra_bad.md",
        "---\ntitle: Extra Bad\ntype: note\n---\n",
    );

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--format", "json", "--limit", "1"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let val: serde_json::Value = serde_json::from_str(stdout)
        .unwrap_or_else(|e| panic!("expected JSON output, got: {stdout}\nerr: {e}"));

    let inner = &val["results"];
    let files = inner["files"].as_array().unwrap();
    assert!(
        files.len() <= 1,
        "expected at most 1 file in output, got {}",
        files.len()
    );
    // total should still reflect ALL violations (not just the limited output)
    assert!(
        inner["total"].as_u64().unwrap() >= 1,
        "total should reflect all violations"
    );
    // limited flag should be present and true
    assert_eq!(
        inner["limited"].as_bool(),
        Some(true),
        "expected limited=true when output was truncated"
    );
    // errors/warnings/files_with_issues should reflect all files, not just the limited slice
    assert!(
        inner["errors"].as_u64().is_some(),
        "expected errors field in LintOutput"
    );
    assert!(
        inner["warnings"].as_u64().is_some(),
        "expected warnings field in LintOutput"
    );
    let files_with_issues = inner["files_with_issues"].as_u64().unwrap();
    assert!(
        files_with_issues > 1,
        "expected files_with_issues > 1 (full count, not limited), got {files_with_issues}"
    );
}

#[test]
fn lint_limit_text_format_shows_truncation_notice() {
    let tmp = setup_vault_with_schema();
    write_md(
        tmp.path(),
        "extra_bad.md",
        "---\ntitle: Extra Bad\ntype: note\n---\n",
    );

    let output = hyalo_no_hints()
        .current_dir(tmp.path())
        .args(["lint", "--format", "text", "--limit", "1"])
        .output()
        .unwrap();

    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    assert!(
        stdout.contains("showing 1 of"),
        "expected truncation notice in text output, got:\n{stdout}"
    );
    // Summary should reflect all files_with_issues, not just the 1 shown
    // e.g. "X files checked, N with issues (..."
    assert!(
        stdout.contains("with issues"),
        "expected 'with issues' summary in text output, got:\n{stdout}"
    );
}
