use super::*;

#[test]
fn test_header_double_slash() {
    let h = header(CommentStyle::DoubleSlash);
    assert!(h.contains("// This file is auto-generated by alef"));
    assert!(!h.contains("Issues & docs:"));
    assert!(!h.contains("github.com/xberg-io/alef"));
    assert!(!h.contains("sample_crate"));
}

#[test]
fn test_header_for_config_omits_issues_url_when_unconfigured() {
    let cfg: crate::core::config::NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["python"]

[[crates]]
name = "demo"
sources = ["src/lib.rs"]
"#,
    )
    .unwrap();
    let resolved = cfg.resolve().unwrap().remove(0);

    let h = header_for_config(CommentStyle::DoubleSlash, &resolved);

    assert!(!h.contains("Issues & docs:"));
    assert!(!h.contains("github.com/xberg-io/alef"));
}

#[test]
fn test_header_for_config_uses_configured_metadata() {
    let cfg: crate::core::config::NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["python"]

[workspace.generated_header]
issues_url = "https://docs.example.invalid/alef"
regenerate_command = "task generate"
verify_command = "task verify"

[[crates]]
name = "demo"
sources = ["src/lib.rs"]
"#,
    )
    .unwrap();
    let resolved = cfg.resolve().unwrap().remove(0);

    let h = header_for_config(CommentStyle::DoubleSlash, &resolved);

    assert!(h.contains("// To regenerate: task generate"));
    assert!(h.contains("// To verify freshness: task verify"));
    assert!(h.contains("// Issues & docs: https://docs.example.invalid/alef"));
}

#[test]
fn test_header_for_config_uses_package_metadata_url() {
    let cfg: crate::core::config::NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["python"]

[[crates]]
name = "demo"
sources = ["src/lib.rs"]

[crates.package_metadata]
issues = "https://issues.example.invalid/demo"
"#,
    )
    .unwrap();
    let resolved = cfg.resolve().unwrap().remove(0);

    let h = header_for_config(CommentStyle::DoubleSlash, &resolved);

    assert!(h.contains("// Issues & docs: https://issues.example.invalid/demo"));
}

#[test]
fn test_header_hash() {
    let h = header(CommentStyle::Hash);
    assert!(h.contains("# This file is auto-generated by alef"));
}

#[test]
fn test_header_block() {
    let h = header(CommentStyle::Block);
    assert!(h.starts_with("/*\n"));
    assert!(h.contains(" * This file is auto-generated by alef"));
    assert!(h.ends_with(" */\n"));
}

#[test]
fn test_inject_and_extract_rust() {
    let h = header(CommentStyle::DoubleSlash);
    let content = format!("{h}use foo;\n");
    let hash = hash_content(&content);
    let injected = inject_hash_line(&content, &hash);
    assert!(injected.contains(HASH_PREFIX));
    assert_eq!(extract_hash(&injected), Some(hash));
}

#[test]
fn test_inject_and_extract_python() {
    let h = header(CommentStyle::Hash);
    let content = format!("{h}import foo\n");
    let hash = hash_content(&content);
    let injected = inject_hash_line(&content, &hash);
    assert!(injected.contains(&format!("# {HASH_PREFIX}")));
    assert_eq!(extract_hash(&injected), Some(hash));
}

#[test]
fn test_inject_and_extract_c_block() {
    let h = header(CommentStyle::Block);
    let content = format!("{h}#include <stdio.h>\n");
    let hash = hash_content(&content);
    let injected = inject_hash_line(&content, &hash);
    assert!(injected.contains(HASH_PREFIX));
    // Hash line must use C block-comment continuation (` * `), not `//`.
    // Mixing `//` inside a `/* ... */` block produces a malformed header
    // and would (depending on formatter) break verify.
    assert!(
        injected.contains(&format!(" * {HASH_PREFIX}")),
        "expected ' * {HASH_PREFIX}' in block-comment header, got:\n{injected}"
    );
    assert!(
        !injected.contains(&format!("// {HASH_PREFIX}")),
        "block-comment header must not use '//' for the hash line, got:\n{injected}"
    );
    assert_eq!(extract_hash(&injected), Some(hash));
}

#[test]
fn test_inject_php_line2() {
    let h = header(CommentStyle::DoubleSlash);
    let content = format!("<?php\n{h}namespace Foo;\n");
    let hash = hash_content(&content);
    let injected = inject_hash_line(&content, &hash);
    let lines: Vec<&str> = injected.lines().collect();
    assert_eq!(lines[0], "<?php");
    assert!(lines[1].contains(HEADER_MARKER));
    assert!(lines.iter().any(|l| l.contains(HASH_PREFIX)));
    assert_eq!(extract_hash(&injected), Some(hash));
}

#[test]
fn test_no_header_returns_unchanged() {
    let content = "fn main() {}\n";
    let injected = inject_hash_line(content, "abc123");
    assert_eq!(injected, content);
    assert_eq!(extract_hash(&injected), None);
}

#[test]
fn test_strip_hash_line() {
    let content = "// auto-generated by alef\n// alef:hash:abc123\nuse foo;\n";
    let stripped = strip_hash_line(content);
    assert_eq!(stripped, "// auto-generated by alef\nuse foo;\n");
}

#[test]
fn test_roundtrip() {
    let h = header(CommentStyle::Hash);
    let original = format!("{h}import sys\n");
    let hash = hash_content(&original);
    let injected = inject_hash_line(&original, &hash);
    let stripped = strip_hash_line(&injected);
    assert_eq!(stripped, original);
    assert_eq!(hash_content(&stripped), hash);
}

// ----- compute_sources_hash / compute_file_hash --------------------------

use std::path::{Path, PathBuf};
use tempfile::tempdir;

fn write_file(dir: &Path, name: &str, content: &str) -> PathBuf {
    let path = dir.join(name);
    std::fs::write(&path, content).unwrap();
    path
}

#[test]
fn sources_hash_changes_when_path_changes_even_if_content_same() {
    let dir = tempdir().unwrap();
    let s_a = write_file(dir.path(), "a.rs", "fn a() {}");
    std::fs::create_dir_all(dir.path().join("moved")).unwrap();
    let s_b = write_file(dir.path(), "moved/a.rs", "fn a() {}");
    let h_a = compute_sources_hash(&[s_a]).unwrap();
    let h_b = compute_sources_hash(&[s_b]).unwrap();
    assert_ne!(
        h_a, h_b,
        "same content at a different path can produce different IR (rust_path differs)"
    );
}

#[test]
fn sources_hash_errors_on_missing_source() {
    let dir = tempdir().unwrap();
    let bogus = dir.path().join("does-not-exist.rs");
    assert!(compute_sources_hash(&[bogus]).is_err());
}

#[test]
fn sources_hash_stable_across_runs() {
    let dir = tempdir().unwrap();
    let s1 = write_file(dir.path(), "a.rs", "fn a() {}");
    let s2 = write_file(dir.path(), "b.rs", "fn b() {}");
    let sources = vec![s1, s2];
    let h1 = compute_sources_hash(&sources).unwrap();
    let h2 = compute_sources_hash(&sources).unwrap();
    assert_eq!(h1, h2);
}

#[test]
fn sources_hash_path_order_independent() {
    let dir = tempdir().unwrap();
    let s1 = write_file(dir.path(), "a.rs", "fn a() {}");
    let s2 = write_file(dir.path(), "b.rs", "fn b() {}");
    let h_forward = compute_sources_hash(&[s1.clone(), s2.clone()]).unwrap();
    let h_reverse = compute_sources_hash(&[s2, s1]).unwrap();
    assert_eq!(h_forward, h_reverse);
}

#[test]
fn sources_hash_changes_with_content() {
    let dir = tempdir().unwrap();
    let s = write_file(dir.path(), "a.rs", "fn a() {}");
    let h_before = compute_sources_hash(std::slice::from_ref(&s)).unwrap();
    std::fs::write(&s, "fn a() { let _ = 1; }").unwrap();
    let h_after = compute_sources_hash(&[s]).unwrap();
    assert_ne!(h_before, h_after);
}

#[test]
fn file_hash_idempotent_under_strip_hash_line() {
    // The defining property: hash(content with hash line) == hash(content without hash line).
    // This is what makes the verify path symmetric with the generate path.
    let sources_hash = "abc123";
    let bare = "// auto-generated by alef\nfn body() {}\n";
    let with_line = "// auto-generated by alef\n// alef:hash:deadbeef\nfn body() {}\n";

    let h1 = compute_file_hash(sources_hash, bare);
    let h2 = compute_file_hash(sources_hash, with_line);
    assert_eq!(h1, h2, "hash must ignore an existing alef:hash: line");
}

#[test]
fn file_hash_changes_when_sources_change() {
    let content = "// auto-generated by alef\nfn body() {}\n";
    let h_a = compute_file_hash("sources_a", content);
    let h_b = compute_file_hash("sources_b", content);
    assert_ne!(h_a, h_b);
}

#[test]
fn file_hash_changes_when_content_changes() {
    let sources_hash = "abc123";
    let h_a = compute_file_hash(sources_hash, "fn a() {}\n");
    let h_b = compute_file_hash(sources_hash, "fn b() {}\n");
    assert_ne!(h_a, h_b);
}

#[test]
fn file_hash_independent_of_alef_version() {
    // Idempotency property: the hash is purely a function of (sources, content).
    // Bumping the alef CLI version must not change it. Encoded by the type
    // signature — there is no version parameter — but make it explicit so
    // a future regression that re-introduces a version dimension is caught.
    let h = compute_file_hash("sources_hash", "fn a() {}\n");
    assert_eq!(h.len(), 64, "blake3 hex output is 64 chars");
}

// ----- compute_inputs_hash ------------------------------------------------

#[test]
fn inputs_hash_is_stable() {
    let h1 = compute_inputs_hash("abc", b"toml");
    let h2 = compute_inputs_hash("abc", b"toml");
    assert_eq!(h1, h2, "compute_inputs_hash must be deterministic");
    assert_eq!(h1.len(), 64, "blake3 hex output is 64 chars");
}

#[test]
fn inputs_hash_changes_when_sources_hash_changes() {
    let h1 = compute_inputs_hash("sources_a", b"toml");
    let h2 = compute_inputs_hash("sources_b", b"toml");
    assert_ne!(h1, h2);
}

#[test]
fn inputs_hash_changes_when_alef_toml_changes() {
    let h1 = compute_inputs_hash("sources", b"[workspace]\nlanguages=[\"python\"]\n");
    let h2 = compute_inputs_hash("sources", b"[workspace]\nlanguages=[\"ruby\"]\n");
    assert_ne!(h1, h2);
}

#[test]
fn inputs_hash_changes_when_alef_rev_changes() {
    // We cannot change ALEF_REV at runtime, but we can verify the hash
    // includes a non-trivial prefix by checking that an empty sources_hash
    // and empty toml still produces a non-trivial 64-char hex string.
    let h = compute_inputs_hash("", b"");
    assert_eq!(h.len(), 64);
    // Verify it differs from a plain blake3("") which would be a pure null hash.
    let plain_empty = blake3::hash(b"").to_hex().to_string();
    assert_ne!(
        h, plain_empty,
        "inputs hash must include the alef:inputs prefix and ALEF_REV"
    );
}

#[test]
fn inputs_hash_tolerates_empty_alef_toml() {
    // Empty alef_toml_bytes is valid (e.g. in unit tests without a real config).
    let h = compute_inputs_hash("some_sources_hash", b"");
    assert_eq!(h.len(), 64);
}

#[test]
fn inputs_hash_differs_from_file_hash() {
    // compute_inputs_hash and compute_file_hash must not collide — they use
    // different domain separators so the outputs are distinct even when given
    // the same source input.
    let sources = "abc";
    let content = "fn a() {}\n";
    let ih = compute_inputs_hash(sources, content.as_bytes());
    let fh = compute_file_hash(sources, content);
    assert_ne!(ih, fh, "inputs hash and file hash must not collide");
}

#[test]
fn crate_sources_hash_differs_across_crates_with_disjoint_sources() {
    use crate::core::config::resolved::ResolvedCrateConfig;

    let dir = tempdir().unwrap();
    let a = write_file(dir.path(), "a.rs", "fn a() {}");
    let b = write_file(dir.path(), "b.rs", "fn b() {}");

    // Build two minimal ResolvedCrateConfig values using the builder pattern
    // isn't available, so we construct via serde round-trip from JSON to avoid
    // requiring Default on the struct.  Instead, use helper that constructs the
    // minimal required fields directly.
    let make_cfg = |name: &str, sources: Vec<std::path::PathBuf>| ResolvedCrateConfig {
        name: name.to_string(),
        sources,
        source_crates: vec![],
        version_from: "Cargo.toml".to_string(),
        core_import: None,
        workspace_root: None,
        skip_core_import: false,
        error_type: None,
        error_constructor: None,
        features: vec![],
        path_mappings: Default::default(),
        extra_dependencies: Default::default(),
        auto_path_mappings: true,
        languages: vec![],
        python: None,
        node: None,
        ruby: None,
        php: None,
        elixir: None,
        wasm: None,
        ffi: None,
        go: None,
        java: None,
        dart: None,
        kotlin: None,
        kotlin_android: None,
        jni: None,
        swift: None,
        gleam: None,
        csharp: None,
        r: None,
        zig: None,
        exclude: Default::default(),
        include: Default::default(),
        output_paths: Default::default(),
        explicit_output: Default::default(),
        lint: Default::default(),
        test: Default::default(),
        setup: Default::default(),
        update: Default::default(),
        clean: Default::default(),
        build_commands: Default::default(),
        generate: Default::default(),
        generate_overrides: Default::default(),
        format: Default::default(),
        format_overrides: Default::default(),
        dto: Default::default(),
        tools: Default::default(),
        opaque_types: Default::default(),
        client_constructors: Default::default(),
        sync: None,
        citation: None,
        publish: None,
        e2e: None,
        adapters: vec![],
        trait_bridges: vec![],
        services: vec![],
        handler_contracts: vec![],
        scaffold: None,
        package_metadata: None,
        readme: None,
        docs: None,
        custom_files: Default::default(),
        custom_modules: Default::default(),
        custom_registrations: Default::default(),
        suppress_validation_codes: Vec::new(),
        untagged_union_text_types: vec![],
    };

    let cfg_a = make_cfg("alpha", vec![a]);
    let cfg_b = make_cfg("beta", vec![b]);

    let hash_a = compute_crate_sources_hash(&cfg_a).unwrap();
    let hash_b = compute_crate_sources_hash(&cfg_b).unwrap();

    assert_ne!(
        hash_a, hash_b,
        "crates with disjoint sources must produce different hashes"
    );
}

#[test]
fn crate_sources_hash_includes_source_crates() {
    use crate::core::config::{SourceCrate, resolved::ResolvedCrateConfig};

    let dir = tempdir().unwrap();
    let a = write_file(dir.path(), "a.rs", "fn a() {}");
    let b = write_file(dir.path(), "b.rs", "fn b() {}");

    let make_cfg =
        |sources: Vec<std::path::PathBuf>, source_crate_sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
            let source_crates = if source_crate_sources.is_empty() {
                vec![]
            } else {
                vec![SourceCrate {
                    name: "extra-crate".to_string(),
                    sources: source_crate_sources,
                    roots: vec![],
                }]
            };
            ResolvedCrateConfig {
                name: "test".to_string(),
                sources,
                source_crates,
                version_from: "Cargo.toml".to_string(),
                core_import: None,
                workspace_root: None,
                skip_core_import: false,
                error_type: None,
                error_constructor: None,
                features: vec![],
                path_mappings: Default::default(),
                extra_dependencies: Default::default(),
                auto_path_mappings: true,
                languages: vec![],
                python: None,
                node: None,
                ruby: None,
                php: None,
                elixir: None,
                wasm: None,
                ffi: None,
                go: None,
                java: None,
                dart: None,
                kotlin: None,
                kotlin_android: None,
                jni: None,
                swift: None,
                gleam: None,
                csharp: None,
                r: None,
                zig: None,
                exclude: Default::default(),
                include: Default::default(),
                output_paths: Default::default(),
                explicit_output: Default::default(),
                lint: Default::default(),
                test: Default::default(),
                setup: Default::default(),
                update: Default::default(),
                clean: Default::default(),
                build_commands: Default::default(),
                generate: Default::default(),
                generate_overrides: Default::default(),
                format: Default::default(),
                format_overrides: Default::default(),
                dto: Default::default(),
                tools: Default::default(),
                opaque_types: Default::default(),
                client_constructors: Default::default(),
                sync: None,
                citation: None,
                publish: None,
                e2e: None,
                adapters: vec![],
                trait_bridges: vec![],
                services: vec![],
                handler_contracts: vec![],
                scaffold: None,
                package_metadata: None,
                readme: None,
                docs: None,
                custom_files: Default::default(),
                custom_modules: Default::default(),
                custom_registrations: Default::default(),
                suppress_validation_codes: Vec::new(),
                untagged_union_text_types: vec![],
            }
        };

    let cfg_without_extra = make_cfg(vec![a.clone()], vec![]);
    let cfg_with_extra = make_cfg(vec![a.clone()], vec![b.clone()]);

    let hash_without = compute_crate_sources_hash(&cfg_without_extra).unwrap();
    let hash_with = compute_crate_sources_hash(&cfg_with_extra).unwrap();

    assert_ne!(
        hash_without, hash_with,
        "adding a source_crate source file must change the hash"
    );
}

#[test]
fn compute_crate_sources_hash_dedupes_overlapping_paths() {
    use crate::core::config::{SourceCrate, resolved::ResolvedCrateConfig};
    // A source path appearing in both `sources` and a `source_crates` entry
    // (or repeated within `sources`) is hashed once: the hash equals the
    // hash of the same crate config with the duplicates removed.
    let dir = tempdir().unwrap();
    let a = write_file(dir.path(), "a.rs", "fn a() {}");
    let b = write_file(dir.path(), "b.rs", "fn b() {}");

    let make_cfg =
        |sources: Vec<std::path::PathBuf>, source_crate_sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
            let source_crates = if source_crate_sources.is_empty() {
                vec![]
            } else {
                vec![SourceCrate {
                    name: "extra-crate".to_string(),
                    sources: source_crate_sources,
                    roots: vec![],
                }]
            };
            ResolvedCrateConfig {
                name: "test".to_string(),
                sources,
                source_crates,
                version_from: "Cargo.toml".to_string(),
                core_import: None,
                workspace_root: None,
                skip_core_import: false,
                error_type: None,
                error_constructor: None,
                features: vec![],
                path_mappings: Default::default(),
                extra_dependencies: Default::default(),
                auto_path_mappings: true,
                languages: vec![],
                python: None,
                node: None,
                ruby: None,
                php: None,
                elixir: None,
                wasm: None,
                ffi: None,
                go: None,
                java: None,
                dart: None,
                kotlin: None,
                kotlin_android: None,
                jni: None,
                swift: None,
                gleam: None,
                csharp: None,
                r: None,
                zig: None,
                exclude: Default::default(),
                include: Default::default(),
                output_paths: Default::default(),
                explicit_output: Default::default(),
                lint: Default::default(),
                test: Default::default(),
                setup: Default::default(),
                update: Default::default(),
                clean: Default::default(),
                build_commands: Default::default(),
                generate: Default::default(),
                generate_overrides: Default::default(),
                format: Default::default(),
                format_overrides: Default::default(),
                dto: Default::default(),
                tools: Default::default(),
                opaque_types: Default::default(),
                client_constructors: Default::default(),
                sync: None,
                citation: None,
                publish: None,
                e2e: None,
                adapters: vec![],
                trait_bridges: vec![],
                services: vec![],
                handler_contracts: vec![],
                scaffold: None,
                package_metadata: None,
                readme: None,
                docs: None,
                custom_files: Default::default(),
                custom_modules: Default::default(),
                custom_registrations: Default::default(),
                suppress_validation_codes: Vec::new(),
                untagged_union_text_types: vec![],
            }
        };

    // `sources` lists `a` twice and `source_crates` also references `a`.
    let cfg_with_dupes = make_cfg(vec![a.clone(), a.clone(), b.clone()], vec![a.clone()]);
    let cfg_unique = make_cfg(vec![a.clone(), b.clone()], vec![]);

    let hash_dup = compute_crate_sources_hash(&cfg_with_dupes).unwrap();
    let hash_unique = compute_crate_sources_hash(&cfg_unique).unwrap();
    assert_eq!(
        hash_dup, hash_unique,
        "duplicate source paths must not affect the per-crate sources hash"
    );
}

#[test]
fn compute_crate_sources_hash_is_order_independent() {
    use crate::core::config::resolved::ResolvedCrateConfig;
    // Reordering `sources` (or the entries inside a `source_crates` entry)
    // does not change the per-crate sources hash.
    let dir = tempdir().unwrap();
    let a = write_file(dir.path(), "a.rs", "fn a() {}");
    let b = write_file(dir.path(), "b.rs", "fn b() {}");
    let c = write_file(dir.path(), "c.rs", "fn c() {}");

    let make_cfg = |sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
        ResolvedCrateConfig {
            name: "test".to_string(),
            sources,
            source_crates: vec![],
            version_from: "Cargo.toml".to_string(),
            core_import: None,
            workspace_root: None,
            skip_core_import: false,
            error_type: None,
            error_constructor: None,
            features: vec![],
            path_mappings: Default::default(),
            extra_dependencies: Default::default(),
            auto_path_mappings: true,
            languages: vec![],
            python: None,
            node: None,
            ruby: None,
            php: None,
            elixir: None,
            wasm: None,
            ffi: None,
            go: None,
            java: None,
            dart: None,
            kotlin: None,
            kotlin_android: None,
            jni: None,
            swift: None,
            gleam: None,
            csharp: None,
            r: None,
            zig: None,
            exclude: Default::default(),
            include: Default::default(),
            output_paths: Default::default(),
            explicit_output: Default::default(),
            lint: Default::default(),
            test: Default::default(),
            setup: Default::default(),
            update: Default::default(),
            clean: Default::default(),
            build_commands: Default::default(),
            generate: Default::default(),
            generate_overrides: Default::default(),
            format: Default::default(),
            format_overrides: Default::default(),
            dto: Default::default(),
            tools: Default::default(),
            opaque_types: Default::default(),
            client_constructors: Default::default(),
            sync: None,
            citation: None,
            publish: None,
            e2e: None,
            adapters: vec![],
            trait_bridges: vec![],
            services: vec![],
            handler_contracts: vec![],
            scaffold: None,
            package_metadata: None,
            readme: None,
            docs: None,
            custom_files: Default::default(),
            custom_modules: Default::default(),
            custom_registrations: Default::default(),
            suppress_validation_codes: Vec::new(),
            untagged_union_text_types: vec![],
        }
    };

    let cfg1 = make_cfg(vec![a.clone(), b.clone(), c.clone()]);
    let cfg2 = make_cfg(vec![c.clone(), a.clone(), b.clone()]);
    let cfg3 = make_cfg(vec![b.clone(), c.clone(), a.clone()]);

    let h1 = compute_crate_sources_hash(&cfg1).unwrap();
    let h2 = compute_crate_sources_hash(&cfg2).unwrap();
    let h3 = compute_crate_sources_hash(&cfg3).unwrap();
    assert_eq!(h1, h2, "reordering sources must not change the hash");
    assert_eq!(h2, h3, "reordering sources must not change the hash");
}

#[test]
fn file_hash_round_trip_via_inject_extract() {
    // Simulate the full generate/verify cycle:
    // 1. generate: compute hash from stripped content, inject into header
    // 2. verify: read back, extract hash, recompute from content, compare
    let sources_hash = "abc123";
    let raw = "// auto-generated by alef\nfn body() {}\n";
    let file_hash = compute_file_hash(sources_hash, raw);
    let on_disk = inject_hash_line(raw, &file_hash);

    let extracted = extract_hash(&on_disk).expect("hash line should be present");
    let recomputed = compute_file_hash(sources_hash, &on_disk);
    assert_eq!(extracted, file_hash);
    assert_eq!(recomputed, file_hash);
    assert_eq!(extracted, recomputed, "verify must reproduce the embedded hash");
}
