use super::*;

const GRADLE_BUILD_SAMPLE: &str = r#"import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
  `java-library`
  kotlin("jvm") version "2.3.21"
  `maven-publish`
  id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}

group = "dev.example"
version = "0.15.6-rc.2"

repositories {
  mavenCentral()
}

dependencies {
  api("net.java.dev.jna:jna:5.14.0")
}

ktlint {
  version.set("1.0.1")
}
"#;

#[test]
fn replace_gradle_project_version_bumps_only_project_version() {
    let out = replace_gradle_project_version(GRADLE_BUILD_SAMPLE, "0.15.6-rc.3").expect("project version bumped");
    assert!(
        out.contains("version = \"0.15.6-rc.3\""),
        "project version must be bumped:\n{out}"
    );
    // Plugin version strings must be untouched.
    assert!(
        out.contains(r#"kotlin("jvm") version "2.3.21""#),
        "kotlin plugin version must not change:\n{out}"
    );
    assert!(
        out.contains(r#"id("org.jlleitschuh.gradle.ktlint") version "12.1.0""#),
        "ktlint plugin version must not change:\n{out}"
    );
    // ktlint extension version must be untouched.
    assert!(
        out.contains(r#"version.set("1.0.1")"#),
        "ktlint extension version must not change:\n{out}"
    );
    // Dependency coordinate must be untouched.
    assert!(
        out.contains(r#"api("net.java.dev.jna:jna:5.14.0")"#),
        "jna coordinate must not change:\n{out}"
    );
    assert!(!out.contains("0.15.6-rc.2"), "old version must be gone:\n{out}");
}

#[test]
fn replace_gradle_project_version_is_idempotent() {
    let first = replace_gradle_project_version(GRADLE_BUILD_SAMPLE, "0.15.6-rc.3").unwrap();
    assert!(
        replace_gradle_project_version(&first, "0.15.6-rc.3").is_none(),
        "second call with same version must return None"
    );
}

#[test]
fn replace_gradle_project_version_no_project_version_returns_none() {
    // A build file with only plugin/extension version constructs — no
    // top-level `version = "..."` assignment to bump.
    let content = "plugins {\n  kotlin(\"jvm\") version \"2.3.21\"\n}\n";
    assert!(replace_gradle_project_version(content, "1.0.0").is_none());
}

// --- NIF Cargo.lock path-source versions ------------------------------

const NIF_CARGO_LOCK_SAMPLE: &str = r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "example_core"
version = "0.15.6-rc.2"
dependencies = [
 "serde",
]

[[package]]
name = "example_nif"
version = "0.15.6-rc.2"
dependencies = [
 "example_core",
 "rustler",
]

[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
"#;

#[test]
fn sync_cargo_lock_path_versions_bumps_only_sourceless_entries() {
    let out = sync_cargo_lock_path_versions(NIF_CARGO_LOCK_SAMPLE, "0.15.6-rc.3").expect("lock updated");
    // The lock-format preamble must be preserved verbatim.
    assert!(
        out.contains("version = 4"),
        "lock format version line must be preserved:\n{out}"
    );
    // Local/path crates (no source key) get bumped.
    assert_eq!(
        out.matches("version = \"0.15.6-rc.3\"").count(),
        2,
        "both local crates must be bumped:\n{out}"
    );
    assert!(!out.contains("0.15.6-rc.2"), "old version must be gone:\n{out}");
    // Registry dependency must be untouched.
    assert!(
        out.contains("version = \"1.0.219\""),
        "registry dep version must not change:\n{out}"
    );
    assert!(
        out.contains("source = \"registry+https://github.com/rust-lang/crates.io-index\""),
        "registry source line must be preserved:\n{out}"
    );
}

#[test]
fn sync_cargo_lock_path_versions_is_idempotent() {
    let first = sync_cargo_lock_path_versions(NIF_CARGO_LOCK_SAMPLE, "0.15.6-rc.3").unwrap();
    assert!(
        sync_cargo_lock_path_versions(&first, "0.15.6-rc.3").is_none(),
        "second call with same version must return None"
    );
}

#[test]
fn sync_cargo_lock_path_versions_preserves_no_trailing_newline() {
    let no_newline = NIF_CARGO_LOCK_SAMPLE.trim_end_matches('\n');
    let out = sync_cargo_lock_path_versions(no_newline, "0.15.6-rc.3").unwrap();
    assert!(
        !out.ends_with('\n'),
        "absence of trailing newline must be preserved:\n{out:?}"
    );
}

#[test]
fn sync_cargo_lock_path_versions_all_registry_returns_none() {
    // Lockfile where every package has a source — nothing local to bump.
    let content = "version = 4\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.219\"\nsource = \"registry+x\"\n";
    assert!(sync_cargo_lock_path_versions(content, "9.9.9").is_none());
}

// --- Docs version badge -----------------------------------------------

#[test]
fn sync_docs_version_badges_updates_api_files_only() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let dir = tmp.path();
    std::fs::write(
        dir.join("api-rust.md"),
        "## Rust API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n\nbody\n",
    )
    .expect("write api-rust.md");
    std::fs::write(
        dir.join("api-python.md"),
        "## Python API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
    )
    .expect("write api-python.md");
    // A non-api doc must not be touched even if it has a badge.
    std::fs::write(
        dir.join("configuration.md"),
        "## Configuration <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
    )
    .expect("write configuration.md");

    let updated = sync_docs_version_badges(dir, "0.15.6-rc.3");
    assert_eq!(
        updated.len(),
        2,
        "only the two api-*.md files must be updated: {updated:?}"
    );

    let rust = std::fs::read_to_string(dir.join("api-rust.md")).unwrap();
    assert!(
        rust.contains("<span class=\"version-badge\">v0.15.6-rc.3</span>"),
        "rust badge must be bumped:\n{rust}"
    );
    let config = std::fs::read_to_string(dir.join("configuration.md")).unwrap();
    assert!(
        config.contains("v0.15.6-rc.2"),
        "non-api doc must not be touched:\n{config}"
    );
}

#[test]
fn sync_docs_version_badges_is_idempotent() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let dir = tmp.path();
    std::fs::write(
        dir.join("api-go.md"),
        "## Go API Reference <span class=\"version-badge\">v1.0.0</span>\n",
    )
    .expect("write api-go.md");
    let _ = sync_docs_version_badges(dir, "1.0.0");
    let second = sync_docs_version_badges(dir, "1.0.0");
    assert!(second.is_empty(), "second call with same version must be a no-op");
}

/// End-to-end: a `sync_versions` bump must update the Kotlin package gradle
/// project version, the NIF crate's committed Cargo.lock path entries, and
/// the docs API-reference version badges — the three coverage gaps.
#[test]
fn sync_versions_bumps_kotlin_gradle_nif_lock_and_docs_badges() {
    use crate::core::config::NewAlefConfig;
    let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let original_cwd = std::env::current_dir().expect("cwd");

    let tmp = tempfile::tempdir().expect("tempdir");
    let root = tmp.path();

    std::fs::write(
        root.join("Cargo.toml"),
        "[workspace.package]\nversion = \"0.15.6-rc.3\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
    )
    .expect("write Cargo.toml");

    // Kotlin package build.gradle.kts, stale.
    std::fs::create_dir_all(root.join("packages/kotlin")).expect("mkdir packages/kotlin");
    std::fs::write(root.join("packages/kotlin/build.gradle.kts"), GRADLE_BUILD_SAMPLE).expect("write build.gradle.kts");

    // Elixir NIF crate committed Cargo.lock, stale.
    std::fs::create_dir_all(root.join("packages/elixir/native/example_nif")).expect("mkdir native");
    std::fs::write(
        root.join("packages/elixir/native/example_nif/Cargo.lock"),
        NIF_CARGO_LOCK_SAMPLE,
    )
    .expect("write Cargo.lock");

    // Docs API-reference page, stale badge.
    std::fs::create_dir_all(root.join("docs/reference")).expect("mkdir docs/reference");
    std::fs::write(
        root.join("docs/reference/api-elixir.md"),
        "## Elixir API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
    )
    .expect("write api-elixir.md");

    let alef_toml = format!(
        "[workspace]\nlanguages = [\"kotlin\", \"elixir\"]\n[[crates]]\nname = \"example\"\nsources = []\nversion_from = \"{}\"\n",
        root.join("Cargo.toml").display().to_string().replace('\\', "/")
    );
    let alef_toml_path = root.join("alef.toml");
    std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");

    let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
    let mut resolved = cfg.resolve().expect("resolve config");
    let resolved_cfg = resolved.remove(0);

    std::env::set_current_dir(root).expect("set_current_dir");
    let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true, None);
    let _ = std::env::set_current_dir(&original_cwd);
    sync_result.expect("sync_versions ok");

    let gradle = std::fs::read_to_string(root.join("packages/kotlin/build.gradle.kts")).expect("read gradle");
    assert!(
        gradle.contains("version = \"0.15.6-rc.3\""),
        "kotlin gradle project version must be bumped:\n{gradle}"
    );
    assert!(
        gradle.contains(r#"kotlin("jvm") version "2.3.21""#),
        "kotlin plugin version must not change:\n{gradle}"
    );

    let lock = std::fs::read_to_string(root.join("packages/elixir/native/example_nif/Cargo.lock")).expect("read lock");
    assert_eq!(
        lock.matches("version = \"0.15.6-rc.3\"").count(),
        2,
        "both local NIF lock entries must be bumped:\n{lock}"
    );
    assert!(
        lock.contains("version = \"1.0.219\""),
        "registry dep in lock must not change:\n{lock}"
    );

    let badge = std::fs::read_to_string(root.join("docs/reference/api-elixir.md")).expect("read api-elixir.md");
    assert!(
        badge.contains("<span class=\"version-badge\">v0.15.6-rc.3</span>"),
        "docs version badge must be bumped:\n{badge}"
    );
}

// -----------------------------------------------------------------------
// render_registry_version unit tests
// -----------------------------------------------------------------------

// -----------------------------------------------------------------------
// update_zig_package_hash unit tests
// -----------------------------------------------------------------------

#[test]
fn update_zig_package_hash_rc_prerelease() {
    // Zig hash format: `<name>-<version>-<base64sha>`
    // When syncing from rc.50 to rc.53, only the version part should change.
    let existing = "sample_pkg-1.4.0-rc.50-Jfgk_HsxAQAl3_LX7NCs1l27EHcYVF9dieEDCVAwUxK9";
    let result = update_zig_package_hash(existing, "1.4.0-rc.50", "1.4.0-rc.53");
    assert_eq!(
        result,
        Some("sample_pkg-1.4.0-rc.53-Jfgk_HsxAQAl3_LX7NCs1l27EHcYVF9dieEDCVAwUxK9".to_string()),
        "rc prerelease version must be substituted in hash"
    );
}

#[test]
fn update_zig_package_hash_release_version() {
    // Zig hash from rc.53 to stable 1.4.0
    let existing = "sample_pkg-1.4.0-rc.53-AbCd_XyZ123456789";
    let result = update_zig_package_hash(existing, "1.4.0-rc.53", "1.4.0");
    assert_eq!(
        result,
        Some("sample_pkg-1.4.0-AbCd_XyZ123456789".to_string()),
        "release version must substitute prerelease"
    );
}

#[test]
fn update_zig_package_hash_same_version_is_none() {
    // No change → return None
    let existing = "mylib-0.1.0-rc.1-SomeBase64Hash";
    let result = update_zig_package_hash(existing, "0.1.0-rc.1", "0.1.0-rc.1");
    assert_eq!(result, None, "same version must return None");
}

#[test]
fn update_zig_package_hash_malformed_hash_is_none() {
    // Malformed hash (too few dashes) → return None gracefully
    let existing = "notenoughparts";
    let result = update_zig_package_hash(existing, "0.1.0", "0.2.0");
    assert_eq!(result, None, "malformed hash must return None");
}

// -----------------------------------------------------------------------
// render_registry_version unit tests
// -----------------------------------------------------------------------

#[test]
fn render_registry_version_python_pep440_rc_prerelease() {
    // PEP 440: ">=0.1.0rc9" → ">=0.3.0rc28"
    let result = render_registry_version("python", "0.3.0-rc.28", ">=0.1.0rc9");
    assert_eq!(result, Some(">=0.3.0rc28".to_string()));
}

#[test]
fn render_registry_version_python_pep440_release() {
    let result = render_registry_version("python", "1.0.0", ">=0.9.0");
    assert_eq!(result, Some(">=1.0.0".to_string()));
}

#[test]
fn render_registry_version_python_already_current_is_none() {
    let result = render_registry_version("python", "0.3.0-rc.28", ">=0.3.0rc28");
    assert_eq!(result, None);
}

#[test]
fn render_registry_version_node_semver_rc() {
    // npm: "^0.1.0-rc.9" → "^0.3.0-rc.28"
    let result = render_registry_version("node", "0.3.0-rc.28", "^0.1.0-rc.9");
    assert_eq!(result, Some("^0.3.0-rc.28".to_string()));
}

#[test]
fn render_registry_version_elixir_hex_constraint() {
    // Hex: "~> 0.1.0-rc.9" → "~> 0.3.0-rc.28"
    let result = render_registry_version("elixir", "0.3.0-rc.28", "~> 0.1.0-rc.9");
    assert_eq!(result, Some("~> 0.3.0-rc.28".to_string()));
}

#[test]
fn render_registry_version_ruby_rubygems_prerelease() {
    // RubyGems: ">= 0.1.0.pre.rc.9" → ">= 0.3.0.pre.rc.28"
    let result = render_registry_version("ruby", "0.3.0-rc.28", ">= 0.1.0.pre.rc.9");
    assert_eq!(result, Some(">= 0.3.0.pre.rc.28".to_string()));
}

#[test]
fn render_registry_version_ruby_already_current_is_none() {
    let result = render_registry_version("ruby", "0.3.0-rc.28", ">= 0.3.0.pre.rc.28");
    assert_eq!(result, None);
}

#[test]
fn render_registry_version_go_module_version() {
    // Go: "v0.1.0-rc.9" → "v0.3.0-rc.28"
    let result = render_registry_version("go", "0.3.0-rc.28", "v0.1.0-rc.9");
    assert_eq!(result, Some("v0.3.0-rc.28".to_string()));
}

#[test]
fn render_registry_version_rust_bare_semver() {
    // crates.io: "0.1.0-rc.9" → "0.3.0-rc.28"
    let result = render_registry_version("rust", "0.3.0-rc.28", "0.1.0-rc.9");
    assert_eq!(result, Some("0.3.0-rc.28".to_string()));
}

#[test]
fn render_registry_version_php_composer_range() {
    // Composer: ">=0.1.0-rc.9" → ">=0.3.0-rc.28"
    let result = render_registry_version("php", "0.3.0-rc.28", ">=0.1.0-rc.9");
    assert_eq!(result, Some(">=0.3.0-rc.28".to_string()));
}

// -----------------------------------------------------------------------
// sync_registry_package_versions integration tests
// -----------------------------------------------------------------------

#[test]
fn sync_registry_package_versions_rewrites_all_language_entries() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let alef_toml_path = tmp.path().join("alef.toml");
    std::fs::write(
        &alef_toml_path,
        concat!(
            "[workspace]\nalef_version = \"0.19.0\"\nlanguages = []\n\n",
            "[[crates]]\nname = \"mylib\"\nsources = []\n\n",
            "[crates.e2e.registry]\noutput = \"test_apps\"\n\n",
            "[crates.e2e.registry.packages.python]\n",
            "name = \"mylib\"\n",
            "version = \">=0.1.0rc9\"\n\n",
            "[crates.e2e.registry.packages.node]\n",
            "name = \"@myorg/mylib\"\n",
            "version = \"^0.1.0-rc.9\"\n\n",
            "[crates.e2e.registry.packages.elixir]\n",
            "name = \"mylib\"\n",
            "version = \"~> 0.1.0-rc.9\"\n\n",
            "[crates.e2e.registry.packages.ruby]\n",
            "name = \"mylib\"\n",
            "version = \">= 0.1.0.pre.rc.9\"\n",
        ),
    )
    .expect("write alef.toml");

    let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
    assert!(changed, "must report at least one change");

    let updated = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");

    assert!(
        updated.contains("version = \">=0.3.0rc28\""),
        "python version must be PEP 440 formatted: {updated}"
    );
    assert!(
        updated.contains("version = \"^0.3.0-rc.28\""),
        "node version must preserve ^ prefix: {updated}"
    );
    assert!(
        updated.contains("version = \"~> 0.3.0-rc.28\""),
        "elixir version must preserve ~> prefix: {updated}"
    );
    assert!(
        updated.contains("version = \">= 0.3.0.pre.rc.28\""),
        "ruby version must be RubyGems formatted: {updated}"
    );
    // Names must be untouched.
    assert!(updated.contains("name = \"mylib\""), "package names must be preserved");
    assert!(
        updated.contains("name = \"@myorg/mylib\""),
        "node name must be preserved"
    );
}

#[test]
fn sync_registry_package_versions_skips_entries_without_version_field() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let alef_toml_path = tmp.path().join("alef.toml");
    let original = concat!(
        "[workspace]\nlanguages = []\n\n",
        "[[crates]]\nname = \"mylib\"\nsources = []\n\n",
        "[crates.e2e.registry.packages.go]\n",
        "module = \"github.com/myorg/mylib\"\n",
        // No version field here — must not be inserted.
    );
    std::fs::write(&alef_toml_path, original).expect("write");

    let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
    assert!(!changed, "no version field → no change");

    let content = std::fs::read_to_string(&alef_toml_path).expect("read");
    assert!(!content.contains("version"), "version must not be inserted: {content}");
}

#[test]
fn sync_registry_package_versions_is_idempotent() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let alef_toml_path = tmp.path().join("alef.toml");
    std::fs::write(
        &alef_toml_path,
        concat!(
            "[workspace]\nlanguages = []\n\n",
            "[[crates]]\nname = \"mylib\"\nsources = []\n\n",
            "[crates.e2e.registry.packages.python]\n",
            "version = \">=0.3.0rc28\"\n",
        ),
    )
    .expect("write");

    // First call: already current → no change.
    let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
    assert!(!changed, "already-current version must be a no-op");
}

#[test]
fn sync_registry_package_versions_preserves_toml_comments_and_order() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let alef_toml_path = tmp.path().join("alef.toml");
    let original = concat!(
        "# Top-level comment\n",
        "[workspace]\nlanguages = []\n\n",
        "[[crates]]\nname = \"mylib\"\nsources = []\n\n",
        "# Registry section comment\n",
        "[crates.e2e.registry.packages.python]\n",
        "name = \"mylib\"\n",
        "version = \">=0.1.0rc9\"\n",
    );
    std::fs::write(&alef_toml_path, original).expect("write");

    sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");

    let updated = std::fs::read_to_string(&alef_toml_path).expect("read");
    assert!(
        updated.contains("# Top-level comment"),
        "top-level comment must be preserved: {updated}"
    );
    assert!(
        updated.contains("# Registry section comment"),
        "registry section comment must be preserved: {updated}"
    );
    // name must appear before version (key order preserved).
    let name_pos = updated.find("name = ").expect("name field present");
    let ver_pos = updated.find("version = ").expect("version field present");
    assert!(name_pos < ver_pos, "name must appear before version in output");
}

/// `sync_registry_package_versions` must update all language entries with
/// both a plain prefix-less version and a prefixed constraint in a single call,
/// covering a production alef.toml shape with mixed version constraints.
#[test]
fn sync_registry_package_versions_handles_go_and_bare_semver_langs() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let alef_toml_path = tmp.path().join("alef.toml");
    std::fs::write(
        &alef_toml_path,
        concat!(
            "[workspace]\nlanguages = []\n\n",
            "[[crates]]\nname = \"mylib\"\nsources = []\n\n",
            "[crates.e2e.registry.packages.go]\n",
            "module = \"github.com/myorg/mylib\"\n",
            "version = \"v0.1.0-rc.9\"\n\n",
            "[crates.e2e.registry.packages.rust]\n",
            "name = \"mylib\"\n",
            "version = \"0.1.0-rc.9\"\n\n",
            "[crates.e2e.registry.packages.php]\n",
            "name = \"myorg/mylib\"\n",
            "version = \">=0.1.0-rc.9\"\n",
        ),
    )
    .expect("write alef.toml");

    let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
    assert!(changed, "must report at least one change");

    let updated = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");
    assert!(
        updated.contains("version = \"v0.3.0-rc.28\""),
        "go version must have v prefix: {updated}"
    );
    assert!(
        updated.contains("version = \"0.3.0-rc.28\""),
        "rust bare semver must be updated: {updated}"
    );
    assert!(
        updated.contains("version = \">=0.3.0-rc.28\""),
        "php composer constraint must be updated: {updated}"
    );
}

// -----------------------------------------------------------------------
// sync_e2e_java_pom tests
// -----------------------------------------------------------------------
