use super::*;

#[test]
fn to_pep440_no_prerelease_passthrough() {
    assert_eq!(to_pep440("1.2.3"), "1.2.3");
    assert_eq!(to_pep440("0.1.0"), "0.1.0");
}

#[test]
fn to_pep440_rc_prerelease() {
    assert_eq!(to_pep440("0.1.0-rc.1"), "0.1.0rc1");
    assert_eq!(to_pep440("4.10.0-rc.9"), "4.10.0rc9");
}

#[test]
fn to_pep440_alpha_beta_prerelease() {
    assert_eq!(to_pep440("1.0.0-alpha.2"), "1.0.0a2");
    assert_eq!(to_pep440("1.0.0-beta.3"), "1.0.0b3");
}

#[test]
fn to_pep440_strips_internal_dots() {
    assert_eq!(to_pep440("0.1.0-rc.1.2"), "0.1.0rc12");
}

#[test]
fn zon_version_regex_anchors_to_dot_version_only() {
    // The regex used by validate_versions + sync_all_manifests to rewrite
    // `.version = "X.Y.Z"` in build.zig.zon. Must NOT match
    // `.minimum_zig_version = "..."` which sits on the same file.
    let re = regex::Regex::new(r#"(?m)^\s*\.version\s*=\s*"([^"]*)""#).expect("valid regex");
    let zon = r#".{
    .name = .my_pkg,
    .version = "1.9.0-rc.1",
    .fingerprint = 0x6f52c41163f42c8c,
    .minimum_zig_version = "0.16.0",
}
"#;
    let captures: Vec<_> = re.captures_iter(zon).collect();
    assert_eq!(
        captures.len(),
        1,
        "regex must match exactly one line, not .minimum_zig_version"
    );
    assert_eq!(&captures[0][1], "1.9.0-rc.1");
}

#[test]
fn matched_version_equals_treats_quote_style_uniformly() {
    assert!(matched_version_equals("VERSION = '1.0.0'", "1.0.0"));
    assert!(matched_version_equals("VERSION = \"1.0.0\"", "1.0.0"));
    assert!(!matched_version_equals("VERSION = '1.0.0'", "2.0.0"));
    assert!(matched_version_equals("<version>1.0.0</version>", "1.0.0"));
    assert!(matched_version_equals("Version: 1.0.0", "1.0.0"));
}

fn citation_author_person() -> CitationAuthor {
    CitationAuthor {
        family_names: Some("Hirschfeld".to_string()),
        given_names: Some("Na'aman".to_string()),
        name: None,
        email: Some("naaman@sample_crate.dev".to_string()),
        orcid: Some("https://orcid.org/0009-0000-2247-5072".to_string()),
    }
}

fn citation_author_entity() -> CitationAuthor {
    CitationAuthor {
        family_names: None,
        given_names: None,
        name: Some("SampleCrate, Inc.".to_string()),
        email: None,
        orcid: None,
    }
}

fn citation_config_mit() -> CitationConfig {
    CitationConfig {
        title: "sample-markup".to_string(),
        abstract_: "Fast markup conversion converter.".to_string(),
        authors: vec![citation_author_person()],
        message: "If you use this software, please cite it using the metadata below.".to_string(),
        repository_code: "https://github.com/sample_crate-dev/sample-markup".to_string(),
        url: Some("https://sample_crate.dev".to_string()),
        license: Some("MIT".to_string()),
        date_released: Some("2026-05-17".to_string()),
        doi: None,
    }
}

#[test]
fn render_citation_cff_mit_full_round_trip() {
    let rendered = render_citation_cff(&citation_config_mit(), "3.5.0", None, "2026-06-13");
    let expected = r#"# This file is generated by alef sync-versions; do not edit by hand.
# Source: [workspace.citation] in alef.toml + workspace version in Cargo.toml.
cff-version: 1.2.0
message: "If you use this software, please cite it using the metadata below."
title: sample-markup
abstract: "Fast markup conversion converter."
authors:
  - family-names: Hirschfeld
    given-names: "Na'aman"
    email: "naaman@sample_crate.dev"
    orcid: "https://orcid.org/0009-0000-2247-5072"
repository-code: "https://github.com/sample_crate-dev/sample-markup"
url: "https://sample_crate.dev"
license: MIT
version: 3.5.0
date-released: 2026-05-17
"#;
    assert_eq!(rendered, expected);
}

#[test]
fn render_citation_cff_elv2_with_entity_author() {
    let mut config = citation_config_mit();
    config.title = "sample_crate".to_string();
    config.repository_code = "https://github.com/sample_crate-dev/sample_crate".to_string();
    config.license = Some("Elastic-2.0".to_string());
    config.authors = vec![citation_author_person(), citation_author_entity()];
    let rendered = render_citation_cff(&config, "5.0.0-rc.1", None, "2026-06-13");
    assert!(rendered.contains("  - family-names: Hirschfeld\n    given-names: \"Na'aman\""));
    assert!(rendered.contains("  - name: \"SampleCrate, Inc.\"\n"));
    assert!(rendered.contains("license: Elastic-2.0\n"));
    assert!(rendered.contains("version: 5.0.0-rc.1\n"));
}

#[test]
fn render_citation_cff_falls_back_to_cargo_license() {
    let mut config = citation_config_mit();
    config.license = None;
    let rendered = render_citation_cff(&config, "1.0.0", Some("Apache-2.0"), "2026-06-13");
    assert!(rendered.contains("license: Apache-2.0\n"));
}

#[test]
fn render_citation_cff_omits_optional_fields_when_unset() {
    let config = CitationConfig {
        title: "tiny".to_string(),
        abstract_: "Tiny library.".to_string(),
        authors: vec![citation_author_person()],
        message: "Cite me.".to_string(),
        repository_code: "https://example.com/tiny".to_string(),
        url: None,
        license: None,
        date_released: None,
        doi: None,
    };
    let rendered = render_citation_cff(&config, "0.1.0", None, "2026-06-13");
    assert!(!rendered.contains("url:"));
    assert!(!rendered.contains("license:"));
    assert!(!rendered.contains("doi:"));
    // `date-released` is always emitted: when [workspace.citation].date-released
    // is absent (this test), the caller-supplied system date is stamped in.
    assert!(rendered.contains("date-released: 2026-06-13\n"));
}

#[test]
fn render_citation_cff_auto_stamps_today_when_config_omits_date() {
    // When `alef.toml.[workspace.citation].date-released` is unset, the
    // renderer must stamp the caller-supplied system date verbatim — this is
    // the path `alef sync-versions` takes so consumers do not hand-edit
    // alef.toml per release.
    let mut config = citation_config_mit();
    config.date_released = None;
    let rendered = render_citation_cff(&config, "3.5.0", None, "2030-01-15");
    assert!(rendered.contains("date-released: 2030-01-15\n"));
    assert!(!rendered.contains("date-released: 2026-05-17\n"));
}

#[test]
fn render_citation_cff_honors_config_date_as_override() {
    // When `alef.toml.[workspace.citation].date-released` is set, the
    // renderer must honor it verbatim and ignore the system date — this
    // preserves the existing override escape hatch for backports / replays.
    let config = citation_config_mit(); // date_released = Some("2026-05-17")
    let rendered = render_citation_cff(&config, "3.5.0", None, "2030-01-15");
    assert!(rendered.contains("date-released: 2026-05-17\n"));
    assert!(!rendered.contains("date-released: 2030-01-15\n"));
}

#[test]
fn render_citation_cff_idempotent_for_unchanged_version() {
    let config = citation_config_mit();
    let first = render_citation_cff(&config, "3.5.0", None, "2026-06-13");
    let second = render_citation_cff(&config, "3.5.0", None, "2026-06-13");
    assert_eq!(first, second);
}

#[test]
fn replace_citation_version_unquoted_scalar() {
    let content = "cff-version: 1.2.0\ntitle: example\nversion: 1.0.0\n";
    let new = replace_citation_version(content, "2.0.0").expect("regex matched");
    assert!(new.contains("version: 2.0.0\n"));
    assert!(new.contains("title: example\n"));
    assert!(!new.contains("1.0.0"));
}

#[test]
fn replace_citation_version_double_quoted_preserves_quotes() {
    let content = "version: \"1.0.0\"\n";
    let new = replace_citation_version(content, "2.0.0").expect("regex matched");
    assert_eq!(new, "version: \"2.0.0\"\n");
}

#[test]
fn replace_citation_version_single_quoted_preserves_quotes() {
    let content = "version: '1.0.0'\n";
    let new = replace_citation_version(content, "2.0.0").expect("regex matched");
    assert_eq!(new, "version: '2.0.0'\n");
}

#[test]
fn replace_citation_version_rc_suffix_passes_through() {
    let content = "version: 5.0.0-rc.1\n";
    let new = replace_citation_version(content, "5.0.0-rc.2").expect("regex matched");
    assert_eq!(new, "version: 5.0.0-rc.2\n");
}

#[test]
fn replace_citation_version_no_op_when_already_current() {
    let content = "version: 1.0.0\n";
    assert!(replace_citation_version(content, "1.0.0").is_none());
}

#[test]
fn replace_citation_version_ignores_nested_version_keys() {
    // CFF allows nested `version:` keys inside references blocks (indented).
    // Only the top-level one must change.
    let content = "version: 1.0.0\nreferences:\n  - type: software\n    version: 9.9.9\n";
    let new = replace_citation_version(content, "2.0.0").expect("regex matched");
    assert!(new.starts_with("version: 2.0.0\n"));
    assert!(new.contains("    version: 9.9.9\n"));
}

#[test]
fn test_replace_version_pattern_ruby_version() {
    let content = r#"# This file is auto-generated by alef
module SampleCrate
  VERSION = "1.0.0"
end
"#;

    let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
    assert!(result.is_some());

    let new_content = result.unwrap();
    assert_eq!(
        new_content,
        r#"# This file is auto-generated by alef
module SampleCrate
  VERSION = "2.0.0"
end
"#
    );
}

#[test]
fn test_replace_version_pattern_ruby_version_single_quotes() {
    let content = "VERSION = '1.5.2'";

    let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
    assert!(result.is_some());

    let new_content = result.unwrap();
    // rubocop Style/StringLiterals: output normalised to double quotes (rubocop default).
    assert_eq!(new_content, "VERSION = \"2.0.0\"");
}

#[test]
fn test_replace_version_pattern_ruby_version_double_quotes() {
    let content = "VERSION = \"1.5.2\"";

    let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "3.0.0");
    assert!(result.is_some());

    let new_content = result.unwrap();
    // rubocop Style/StringLiterals: output normalised to double quotes regardless of input.
    assert_eq!(new_content, "VERSION = \"3.0.0\"");
}

#[test]
fn test_replace_version_pattern_ruby_in_module() {
    let content = r#"module MyGem
  VERSION = "0.5.0"
end"#;

    let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "1.0.0");
    assert!(result.is_some());

    let new_content = result.unwrap();
    assert!(new_content.contains("VERSION = \"1.0.0\""));
    assert!(!new_content.contains("0.5.0"));
}

#[test]
fn test_replace_version_pattern_no_match() {
    let content = "NOTHING = \"1.0.0\"";

    let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
    assert!(result.is_none());
}

#[test]
fn test_replace_version_pattern_preserves_other_content() {
    let content = r#"# frozen_string_literal: true
module SampleCrate
  VERSION = "1.0.0"
  # Other stuff
  CONST = "something"
end"#;

    let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
    assert!(result.is_some());

    let new_content = result.unwrap();
    assert!(new_content.contains("# frozen_string_literal: true"));
    assert!(new_content.contains("CONST = \"something\""));
    assert!(new_content.contains("VERSION = \"2.0.0\""));
}

/// Verify that `finalize_hashes` updates the alef:hash line in a file that
/// carries the alef header marker. This exercises the mechanism used by
/// `sync_versions` to refresh stale hashes after rewriting version strings.
#[test]
fn test_finalize_hashes_updates_alef_hash_line() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("version.rb");

    // Simulate a version.rb that was written by alef with an alef header
    // but no hash line yet (as written by write_scaffold_files_with_overwrite
    // before finalize_hashes runs).
    let content = "# This file is auto-generated by alef — do not edit manually.\n# frozen_string_literal: true\n\nmodule MyGem\n  VERSION = '2.0.0'\nend\n";
    std::fs::write(&path, content).expect("write");

    let paths: std::collections::HashSet<std::path::PathBuf> = std::iter::once(path.clone()).collect();
    let alef_toml_bytes = b"[workspace]\nlanguages = [\"ruby\"]\n";
    let n = generate::finalize_hashes(&paths, "test-sources-hash", alef_toml_bytes).expect("finalize ok");
    assert_eq!(n, 1, "finalize_hashes must update the file with the alef:hash line");

    let updated = std::fs::read_to_string(&path).expect("read");
    assert!(
        updated.contains("alef:hash:"),
        "file must contain alef:hash: after finalize_hashes, got:\n{updated}"
    );
}

/// Verify that `finalize_hashes` is idempotent: running it twice on the same
/// file must not change the hash line on the second run.
#[test]
fn test_finalize_hashes_is_idempotent() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("version.rb");

    let content =
        "# This file is auto-generated by alef — do not edit manually.\n\nmodule MyGem\n  VERSION = '2.0.0'\nend\n";
    std::fs::write(&path, content).expect("write");

    let paths: std::collections::HashSet<std::path::PathBuf> = std::iter::once(path.clone()).collect();
    let alef_toml_bytes = b"[workspace]\nlanguages = [\"ruby\"]\n";

    let _ = generate::finalize_hashes(&paths, "sources", alef_toml_bytes).expect("first finalize");
    let after_first = std::fs::read_to_string(&path).expect("read after first");

    let n2 = generate::finalize_hashes(&paths, "sources", alef_toml_bytes).expect("second finalize");
    assert_eq!(n2, 0, "second finalize_hashes must be a no-op (same inputs hash)");

    let after_second = std::fs::read_to_string(&path).expect("read after second");
    assert_eq!(after_first, after_second, "content must not change on second finalize");
}

const GEMFILE_LOCK_SAMPLE: &str = "\
PATH
  remote: .
  specs:
    sample_crate (4.10.0.pre.rc.13)
      rb_sys (~> 0.9)

GEM
  remote: https://rubygems.org/
  specs:
    rake (13.4.2)

PLATFORMS
  ruby

DEPENDENCIES
  sample_crate!

CHECKSUMS
  sample_crate (4.10.0.pre.rc.13)
  rake (13.4.2) sha256=abcdef

BUNDLED WITH
  4.0.7
";

#[test]
fn sync_gemfile_lock_updates_both_occurrences() {
    let result = sync_gemfile_lock(GEMFILE_LOCK_SAMPLE, "4.10.0.pre.rc.14");
    assert!(result.is_some(), "expected Some when version changes");
    let new = result.unwrap();
    // PATH > specs entry updated
    assert!(
        new.contains("    sample_crate (4.10.0.pre.rc.14)"),
        "PATH specs entry not updated:\n{new}"
    );
    // CHECKSUMS entry updated
    assert!(
        new.contains("  sample_crate (4.10.0.pre.rc.14)"),
        "CHECKSUMS entry not updated:\n{new}"
    );
    // Other gem versions are unchanged
    assert!(
        new.contains("rake (13.4.2)"),
        "non-path gem version must not change:\n{new}"
    );
    // Old version must be gone
    assert!(!new.contains("4.10.0.pre.rc.13"), "old version must be removed:\n{new}");
}

#[test]
fn sync_gemfile_lock_is_idempotent() {
    let first = sync_gemfile_lock(GEMFILE_LOCK_SAMPLE, "4.10.0.pre.rc.14").unwrap();
    let second = sync_gemfile_lock(&first, "4.10.0.pre.rc.14");
    assert!(
        second.is_none(),
        "second call with same version must return None (already in sync)"
    );
}

#[test]
fn sync_gemfile_lock_preserves_trailing_newline() {
    let with_newline = format!("{GEMFILE_LOCK_SAMPLE}\n");
    let result = sync_gemfile_lock(&with_newline, "4.10.0.pre.rc.99").unwrap();
    assert!(result.ends_with('\n'), "trailing newline must be preserved");
}

#[test]
fn sync_gemfile_lock_no_path_gem_returns_none() {
    // A Gemfile.lock with no PATH block — nothing to sync.
    let content = "GEM\n  remote: https://rubygems.org/\n  specs:\n    rake (13.4.2)\n";
    let result = sync_gemfile_lock(content, "1.0.0");
    assert!(result.is_none(), "no PATH gem means nothing to update");
}

#[test]
fn restore_gleam_dep_ranges_repairs_corrupted_workspace_version_ranges() {
    let corrupted = "name = \"sample_crate\"\nversion = \"5.0.0-rc.1\"\ntarget = \"erlang\"\n\n[dependencies]\ngleam_stdlib = \">= 5.0.0-rc.1 and < 5.0.0-rc.1\"\n\n[dev-dependencies]\ngleeunit = \">= 5.0.0-rc.1 and < 5.0.0-rc.1\"\n";
    let healed = restore_gleam_dep_ranges(corrupted);
    assert!(
        healed.contains("gleam_stdlib = \">= 0.34.0 and < 2.0.0\""),
        "gleam_stdlib should be restored to canonical range, got:\n{healed}"
    );
    assert!(
        healed.contains("gleeunit = \">= 1.0.0 and < 2.0.0\""),
        "gleeunit should be restored to canonical range, got:\n{healed}"
    );
    // The package version line itself must not be touched.
    assert!(
        healed.contains("version = \"5.0.0-rc.1\""),
        "package version must not be rewritten, got:\n{healed}"
    );
}

#[test]
fn restore_gleam_dep_ranges_is_idempotent_on_healthy_input() {
    let healthy = "name = \"sample_crate\"\nversion = \"5.0.0-rc.1\"\n\n[dependencies]\ngleam_stdlib = \">= 0.34.0 and < 2.0.0\"\n\n[dev-dependencies]\ngleeunit = \">= 1.0.0 and < 2.0.0\"\n";
    let healed = restore_gleam_dep_ranges(healthy);
    assert_eq!(healed, healthy, "healthy gleam.toml must not be rewritten");
}

/// Root `package.json` is a private "root" pnpm-workspace bookkeeping
/// manifest. It carries its own top-level `"version"` that must track the
/// canonical Cargo.toml version so `validate-versions` does not flag a
/// drift on every release. The replacement must not touch nested
/// `"version"` fields inside `devDependencies` / `pnpm.overrides` / etc.
#[test]
fn test_replace_version_pattern_root_package_json_only_top_level() {
    let content = r#"{
  "name": "sample_crate-root",
  "version": "4.9.5",
  "private": true,
  "devDependencies": {
    "@vitest/coverage-v8": "^4.1.5",
    "tsx": "^4.21.0",
    "typescript": "^6.0.3"
  },
  "pnpm": {
    "overrides": {
      "glob": "10.5.0"
    }
  }
}
"#;
    let new_content = replace_version_pattern(content, r#""version":\s*"[^"]*""#, "5.0.0-rc.1")
        .expect("root package.json version must update");
    assert!(
        new_content.contains(r#""version": "5.0.0-rc.1""#),
        "top-level version must be rewritten, got:\n{new_content}"
    );
    assert!(
        !new_content.contains(r#""version": "4.9.5""#),
        "old version must be removed, got:\n{new_content}"
    );
    // Nested dependency versions and pnpm overrides must remain intact.
    assert!(
        new_content.contains("\"@vitest/coverage-v8\": \"^4.1.5\""),
        "devDependency version specs must not be touched, got:\n{new_content}"
    );
    assert!(
        new_content.contains("\"glob\": \"10.5.0\""),
        "pnpm overrides must not be touched, got:\n{new_content}"
    );
}
