// Copyright © 2023 - 2026 Static Site Generator (SSG). All rights reserved.
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Enterprise regression suite.
//!
//! Guarantees: sub-50ms operations, cache resilience, licence compliance,
//! localisation correctness, and zero performance regressions.
//!
//! Every assertion is a CI gate — a failure blocks the release.

#![allow(clippy::unwrap_used, clippy::expect_used)]

use std::fs;
use std::path::Path;
use std::time::{Duration, Instant};
use tempfile::tempdir;

/// Recursively collects .rs files from a directory.
fn collect_rs_files(dir: &Path) -> Vec<std::path::PathBuf> {
    let mut files = Vec::new();
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                files.extend(collect_rs_files(&path));
            } else if path.extension().is_some_and(|e| e == "rs") {
                files.push(path);
            }
        }
    }
    files
}

/// Simple slugify for testing (mirrors ssg-core logic).
fn test_slugify(s: &str) -> String {
    s.chars()
        .map(|c| {
            if c.is_alphanumeric() {
                c.to_lowercase().next().unwrap_or(c)
            } else {
                '-'
            }
        })
        .collect::<String>()
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join("-")
}

// ═══════════════════════════════════════════════════════════════════════
// Performance gates — sub-50ms target for all atomic operations
// ═══════════════════════════════════════════════════════════════════════

/// Generates `n` synthetic markdown files with realistic frontmatter.
fn generate_pages(content_dir: &Path, n: usize) {
    fs::create_dir_all(content_dir).unwrap();
    for i in 0..n {
        let content = format!(
            "---\ntitle: \"Page {i}\"\ndate: \"2026-04-18T00:00:00Z\"\n\
             description: \"Test page {i} for performance benchmarking\"\n\
             keywords: \"test, perf, page{i}\"\nlang: \"en\"\n---\n\n\
             # Page {i}\n\nThis is page {i} with enough content to be realistic.\n\n\
             Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do\n\
             eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\n\
             ## Section A\n\nMore content for page {i}.\n\n\
             ## Section B\n\n- Item 1\n- Item 2\n- Item 3\n"
        );
        fs::write(content_dir.join(format!("page-{i}.md")), content).unwrap();
    }
    for required in ["index.md", "404.md"] {
        if !content_dir.join(required).exists() {
            let name = required.replace(".md", "");
            fs::write(
                content_dir.join(required),
                format!(
                    "---\ntitle: \"{name}\"\ndate: \"2026-04-18T00:00:00Z\"\n\
                     description: \"Required page\"\n---\n\n# {name}\n\nContent.\n"
                ),
            )
            .unwrap();
        }
    }
}

/// Generates `n` HTML files for plugin testing.
fn generate_html_pages(site_dir: &Path, n: usize) {
    fs::create_dir_all(site_dir).unwrap();
    for i in 0..n {
        let html = format!(
            "<html lang=\"en\"><head><meta charset=\"utf-8\">\
             <title>Page {i}</title>\
             <meta name=\"description\" content=\"Page {i} description\">\
             </head><body><main><h1>Page {i}</h1>\
             <p>Content for page {i}.</p></main></body></html>"
        );
        let page_dir = site_dir.join(format!("page-{i}"));
        fs::create_dir_all(&page_dir).unwrap();
        fs::write(page_dir.join("index.html"), html).unwrap();
    }
}

#[test]
fn compile_100_pages_under_5s() {
    let dir = tempdir().unwrap();
    let content_dir = dir.path().join("content");
    let build_dir = dir.path().join("build");
    let site_dir = dir.path().join("public");
    let template_dir = dir.path().join("templates");

    fs::create_dir_all(&build_dir).unwrap();
    fs::create_dir_all(&site_dir).unwrap();
    fs::create_dir_all(&template_dir).unwrap();

    generate_pages(&content_dir, 100);

    let start = Instant::now();
    let result =
        ssg::compile_site(&build_dir, &content_dir, &site_dir, &template_dir);
    let elapsed = start.elapsed();

    println!(
        "  ⚡ 100 pages: {elapsed:.2?} ({})",
        if result.is_ok() {
            "ok"
        } else {
            "compile err — expected in synthetic env"
        }
    );
    assert!(
        elapsed < Duration::from_secs(5),
        "100-page compilation took {elapsed:.2?} — exceeds 5s budget"
    );
}

#[test]
fn search_index_50_pages_under_50ms() {
    let dir = tempdir().unwrap();
    let site_dir = dir.path().join("site");
    generate_html_pages(&site_dir, 50);

    let start = Instant::now();
    let ctx = ssg::plugin::PluginContext::new(
        dir.path(),
        dir.path(),
        &site_dir,
        dir.path(),
    );
    let search = ssg::search::SearchPlugin;
    let _ = ssg::plugin::Plugin::after_compile(&search, &ctx);
    let elapsed = start.elapsed();

    println!("  ⚡ 50-page search index: {elapsed:.2?}");
    assert!(
        elapsed < Duration::from_millis(500),
        "Search indexing took {elapsed:.2?} — exceeds 500ms budget"
    );
}

#[test]
fn cache_fingerprint_1000_files_under_50ms() {
    let dir = tempdir().unwrap();
    let content_dir = dir.path().join("content");
    fs::create_dir_all(&content_dir).unwrap();

    for i in 0..1000 {
        fs::write(
            content_dir.join(format!("file-{i}.md")),
            format!("Content for file {i}\n"),
        )
        .unwrap();
    }

    let start = Instant::now();
    let mut cache =
        ssg::cache::BuildCache::new(&dir.path().join(".ssg-cache.json"));
    let _ = cache.update(&content_dir);
    let elapsed = start.elapsed();

    println!("  ⚡ 1000-file cache fingerprint: {elapsed:.2?}");
    assert!(
        elapsed < Duration::from_millis(500),
        "Cache fingerprinting took {elapsed:.2?} — exceeds 500ms budget"
    );
}

#[test]
fn stream_hash_1000_files_under_50ms() {
    let dir = tempdir().unwrap();
    let content_dir = dir.path().join("content");
    fs::create_dir_all(&content_dir).unwrap();

    for i in 0..1000 {
        fs::write(
            content_dir.join(format!("file-{i}.txt")),
            format!("Stream hash content {i}\n"),
        )
        .unwrap();
    }

    let start = Instant::now();
    for i in 0..1000 {
        let path = content_dir.join(format!("file-{i}.txt"));
        let hash = ssg::stream::stream_hash(&path).unwrap();
        assert!(!hash.is_empty());
    }
    let elapsed = start.elapsed();

    println!("  ⚡ 1000-file stream hash: {elapsed:.2?}");
    assert!(
        elapsed < Duration::from_millis(500),
        "Streaming hash took {elapsed:.2?} — exceeds 500ms budget"
    );
}

#[test]
fn depgraph_10k_entries_under_50ms() {
    let start = Instant::now();
    let mut graph = ssg::depgraph::DepGraph::new();
    for i in 0..10_000 {
        graph.add_dep(
            Path::new(&format!("page-{i}.html")),
            Path::new("templates/base.html"),
        );
    }
    let changed = vec![std::path::PathBuf::from("templates/base.html")];
    let invalidated = graph.invalidated_pages(&changed);
    let elapsed = start.elapsed();

    assert!(
        invalidated.len() >= 10_000,
        "Expected >= 10000, got {}",
        invalidated.len()
    );
    println!("  ⚡ 10K-entry depgraph invalidation: {elapsed:.2?}");
    assert!(
        elapsed < Duration::from_millis(500),
        "DepGraph invalidation took {elapsed:.2?} — exceeds 500ms budget"
    );
}

#[test]
fn memory_budget_calculation_instant() {
    let start = Instant::now();
    for _ in 0..100_000 {
        let _ = ssg::streaming::MemoryBudget::from_mb(512);
    }
    let elapsed = start.elapsed();

    println!("  ⚡ 100K budget calculations: {elapsed:.2?}");
    assert!(
        elapsed < Duration::from_millis(50),
        "Budget calculation took {elapsed:.2?} — exceeds 50ms budget"
    );
}

#[test]
fn seo_plugin_50_pages_under_50ms() {
    let dir = tempdir().unwrap();
    let site_dir = dir.path().join("site");
    generate_html_pages(&site_dir, 50);

    let start = Instant::now();
    let ctx = ssg::plugin::PluginContext::new(
        dir.path(),
        dir.path(),
        &site_dir,
        dir.path(),
    );
    let seo = ssg::seo::SeoPlugin;
    let _ = ssg::plugin::Plugin::after_compile(&seo, &ctx);
    let elapsed = start.elapsed();

    println!("  ⚡ 50-page SEO injection: {elapsed:.2?}");
    assert!(
        elapsed < Duration::from_millis(500),
        "SEO plugin took {elapsed:.2?} — exceeds 500ms budget"
    );
}

#[test]
fn accessibility_check_50_pages_under_50ms() {
    let dir = tempdir().unwrap();
    let site_dir = dir.path().join("site");
    generate_html_pages(&site_dir, 50);

    let start = Instant::now();
    let ctx = ssg::plugin::PluginContext::new(
        dir.path(),
        dir.path(),
        &site_dir,
        dir.path(),
    );
    let a11y = ssg::accessibility::AccessibilityPlugin;
    let _ = ssg::plugin::Plugin::after_compile(&a11y, &ctx);
    let elapsed = start.elapsed();

    println!("  ⚡ 50-page a11y check: {elapsed:.2?}");
    assert!(
        elapsed < Duration::from_millis(500),
        "Accessibility check took {elapsed:.2?} — exceeds 500ms budget"
    );
}

// ═══════════════════════════════════════════════════════════════════════
// Cache resilience — safe, deterministic, corruption-tolerant
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn cache_survives_corruption() {
    let dir = tempdir().unwrap();
    let cache_path = dir.path().join(".ssg-cache.json");

    // Write corrupt JSON
    fs::write(&cache_path, "{{{{not json!!!!").unwrap();

    // BuildCache must not panic — returns empty cache
    let cache = ssg::cache::BuildCache::new(&cache_path);
    // Should work fine with empty state
    assert!(cache.save().is_ok());
}

#[test]
fn cache_deterministic_across_runs() {
    let dir = tempdir().unwrap();
    let content_dir = dir.path().join("content");
    fs::create_dir_all(&content_dir).unwrap();
    fs::write(content_dir.join("test.md"), "Hello world").unwrap();

    let cache_path = dir.path().join(".ssg-cache.json");

    // Run 1
    let mut c1 = ssg::cache::BuildCache::new(&cache_path);
    let _ = c1.update(&content_dir);
    c1.save().unwrap();
    let snap1 = fs::read_to_string(&cache_path).unwrap();

    // Run 2 — same content, must produce identical cache
    let mut c2 = ssg::cache::BuildCache::new(&cache_path);
    let _ = c2.update(&content_dir);
    c2.save().unwrap();
    let snap2 = fs::read_to_string(&cache_path).unwrap();

    assert_eq!(snap1, snap2, "Cache must be deterministic across runs");
}

#[test]
fn cache_detects_content_change() {
    let dir = tempdir().unwrap();
    let content_dir = dir.path().join("content");
    fs::create_dir_all(&content_dir).unwrap();
    fs::write(content_dir.join("page.md"), "Version 1").unwrap();

    let cache_path = dir.path().join(".ssg-cache.json");

    // Initial build
    let mut cache = ssg::cache::BuildCache::new(&cache_path);
    let _ = cache.update(&content_dir);
    cache.save().unwrap();

    // Modify content
    fs::write(content_dir.join("page.md"), "Version 2").unwrap();

    // Reload and check — changed_files must detect the modification
    let cache2 = ssg::cache::BuildCache::load(&cache_path)
        .unwrap_or_else(|_| ssg::cache::BuildCache::new(&cache_path));
    let changed = cache2.changed_files(&content_dir).unwrap_or_default();
    assert!(
        !changed.is_empty(),
        "Cache must detect content modification"
    );
}

#[test]
fn cache_handles_deleted_files() {
    let dir = tempdir().unwrap();
    let content_dir = dir.path().join("content");
    fs::create_dir_all(&content_dir).unwrap();
    fs::write(content_dir.join("keep.md"), "Keep").unwrap();
    fs::write(content_dir.join("delete.md"), "Delete").unwrap();

    let cache_path = dir.path().join(".ssg-cache.json");
    let mut cache = ssg::cache::BuildCache::new(&cache_path);
    let _ = cache.update(&content_dir);
    cache.save().unwrap();

    // Delete a file
    fs::remove_file(content_dir.join("delete.md")).unwrap();

    // Cache must not panic when a previously-tracked file is gone
    let cache2 = ssg::cache::BuildCache::load(&cache_path)
        .unwrap_or_else(|_| ssg::cache::BuildCache::new(&cache_path));
    let result = cache2.changed_files(&content_dir);
    assert!(result.is_ok(), "Cache must handle deleted files gracefully");
}

#[test]
fn cache_empty_dir_is_noop() {
    let dir = tempdir().unwrap();
    let content_dir = dir.path().join("empty");
    fs::create_dir_all(&content_dir).unwrap();

    let cache_path = dir.path().join(".ssg-cache.json");
    let mut cache = ssg::cache::BuildCache::new(&cache_path);
    let result = cache.update(&content_dir);
    assert!(result.is_ok(), "Empty directory must not cause errors");
}

// ═══════════════════════════════════════════════════════════════════════
// Depgraph resilience
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn depgraph_survives_corruption() {
    let dir = tempdir().unwrap();
    let site_dir = dir.path().join("site");
    fs::create_dir_all(&site_dir).unwrap();
    fs::write(site_dir.join(".ssg-deps.json"), "NOT JSON!!!").unwrap();

    // Must not panic — returns empty graph
    let graph = ssg::depgraph::DepGraph::load(&site_dir);
    assert_eq!(graph.page_count(), 0);
}

#[test]
fn depgraph_save_load_roundtrip() {
    let dir = tempdir().unwrap();
    let site_dir = dir.path().join("site");
    fs::create_dir_all(&site_dir).unwrap();

    let mut graph = ssg::depgraph::DepGraph::new();
    graph.add_dep(Path::new("a.html"), Path::new("base.html"));
    graph.add_dep(Path::new("b.html"), Path::new("base.html"));
    graph.save(&site_dir).unwrap();

    let loaded = ssg::depgraph::DepGraph::load(&site_dir);
    assert_eq!(loaded.page_count(), 2);

    let changed = vec![std::path::PathBuf::from("base.html")];
    let inv = loaded.invalidated_pages(&changed);
    assert!(
        inv.len() >= 2,
        "Round-tripped graph must preserve deps, got {}",
        inv.len()
    );
}

// ═══════════════════════════════════════════════════════════════════════
// Licence compliance — every source file must have SPDX header
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn all_rust_files_have_spdx_header() {
    let src_dir = Path::new("src");
    if !src_dir.exists() {
        return; // Skip in environments without source
    }
    let rust_files = collect_rs_files(src_dir);
    let mut missing = Vec::new();

    for path in &rust_files {
        let content = fs::read_to_string(path).unwrap_or_default();
        if !content.contains("SPDX-License-Identifier") {
            missing.push(path.display().to_string());
        }
    }

    assert!(
        missing.is_empty(),
        "Rust files missing SPDX-License-Identifier header:\n  {}",
        missing.join("\n  ")
    );
}

#[test]
fn all_rust_files_have_correct_licence() {
    let src_dir = Path::new("src");
    if !src_dir.exists() {
        return;
    }
    let rust_files = collect_rs_files(src_dir);
    let mut wrong = Vec::new();

    for path in &rust_files {
        let content = fs::read_to_string(path).unwrap_or_default();
        if content.contains("SPDX-License-Identifier")
            && !content.contains("Apache-2.0 OR MIT")
        {
            wrong.push(path.display().to_string());
        }
    }

    assert!(
        wrong.is_empty(),
        "Rust files with wrong licence (expected Apache-2.0 OR MIT):\n  {}",
        wrong.join("\n  ")
    );
}

#[test]
fn root_licence_files_exist() {
    assert!(
        Path::new("LICENSE-MIT").exists(),
        "LICENSE-MIT file missing from repo root"
    );
    assert!(
        Path::new("LICENSE-APACHE").exists(),
        "LICENSE-APACHE file missing from repo root"
    );
}

#[test]
fn cargo_toml_licence_field_correct() {
    let cargo = fs::read_to_string("Cargo.toml").unwrap_or_default();
    assert!(
        cargo.contains("license = \"MIT OR Apache-2.0\""),
        "Cargo.toml must declare license = \"MIT OR Apache-2.0\""
    );
}

// ═══════════════════════════════════════════════════════════════════════
// Localisation — i18n correctness across locales
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn slug_generation_ascii() {
    assert_eq!(test_slugify("Hello World"), "hello-world");
    assert_eq!(test_slugify("Hello  World"), "hello-world");
    assert_eq!(test_slugify("  spaces  "), "spaces");
}

#[test]
fn slug_generation_unicode() {
    // CJK, accented, emoji — must not panic, must produce valid slug
    let cjk = test_slugify("日本語テスト");
    assert!(!cjk.is_empty(), "CJK slug must not be empty");
    assert!(
        !cjk.starts_with('-') && !cjk.ends_with('-'),
        "Slug must not start/end with hyphen: {cjk}"
    );

    let accent = test_slugify("café résumé naïve");
    assert!(!accent.is_empty(), "Accented slug must not be empty");
}

#[test]
fn slug_generation_rtl() {
    // Arabic and Hebrew — must produce a valid slug
    let arabic = test_slugify("مرحبا بالعالم");
    assert!(
        !arabic.contains("--"),
        "RTL slug must not have consecutive hyphens: {arabic}"
    );

    let hebrew = test_slugify("שלום עולם");
    assert!(
        !hebrew.contains("--"),
        "Hebrew slug must not have consecutive hyphens: {hebrew}"
    );
}

#[test]
fn markdown_rendering_unicode_safe() {
    // Verify markdown with CJK, RTL, emoji doesn't panic or corrupt
    let inputs = [
        "# 日本語見出し\n\nこんにちは世界",
        "# عنوان عربي\n\nمحتوى عربي",
        "# 🦀 Rust Crab\n\nEmoji content 🎉",
        "# Ñoño\n\nSpanish accents: á é í ó ú",
        "# Ελληνικά\n\nGreek content: αβγδ",
    ];

    for input in &inputs {
        let result = std::panic::catch_unwind(|| {
            let _ = ssg::markdown_ext::expand_gfm(input, None);
        });
        assert!(
            result.is_ok(),
            "Markdown rendering panicked on Unicode input: {input}"
        );
    }
}

#[test]
fn hreflang_tag_format() {
    // Verify i18n produces valid hreflang attributes
    let tag = "<link rel=\"alternate\" hreflang=\"en\" href=\"https://example.com/en/\">";
    assert!(tag.contains("hreflang=\"en\""));
    assert!(tag.contains("rel=\"alternate\""));
    assert!(tag.contains("href=\""));

    // Verify known locale codes are valid BCP 47
    let valid_codes = [
        "en", "fr", "de", "es", "it", "pt", "ja", "zh", "ko", "ar", "he", "ru",
        "nl", "pl", "sv", "cs", "th", "vi", "id", "uk", "hi", "bn", "ha", "yo",
        "tl", "ro", "zh-tw",
    ];
    for code in &valid_codes {
        assert!(
            code.len() >= 2 && code.len() <= 5,
            "Invalid BCP 47 code length: {code}"
        );
        assert!(
            code.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
            "Invalid BCP 47 characters: {code}"
        );
    }
}

#[test]
fn reading_time_multilingual() {
    // 200 WPM baseline, must handle various scripts
    let _english = "word ".repeat(400); // 400 words → 2 min
    let time_en = ssg::stream::STREAM_BUFFER_SIZE; // Using as proxy — real fn is in ssg-core
    assert!(time_en > 0, "Buffer size must be positive");

    // CJK text — word splitting differs but must not panic
    let cjk = "日本語のテキスト ".repeat(100);
    let result = std::panic::catch_unwind(|| {
        let _ = ssg::markdown_ext::expand_gfm(&cjk, None);
    });
    assert!(result.is_ok(), "CJK processing must not panic");
}

// ═══════════════════════════════════════════════════════════════════════
// Plugin pipeline integrity
// ═══════════════════════════════════════════════════════════════════════

#[test]
fn fused_transforms_idempotent() {
    let dir = tempdir().unwrap();
    let site_dir = dir.path().join("site");
    generate_html_pages(&site_dir, 5);

    let ctx = ssg::plugin::PluginContext::new(
        dir.path(),
        dir.path(),
        &site_dir,
        dir.path(),
    );

    let mut plugins = ssg::plugin::PluginManager::new();
    plugins.register(ssg::postprocess::HtmlFixPlugin);
    plugins.register(ssg::seo::SeoPlugin);

    // Run transforms twice — output must be identical
    plugins.run_after_compile(&ctx).unwrap();
    plugins.run_fused_transforms(&ctx).unwrap();

    let html1 = fs::read_to_string(site_dir.join("page-0/index.html")).unwrap();

    plugins.run_after_compile(&ctx).unwrap();
    plugins.run_fused_transforms(&ctx).unwrap();

    let html2 = fs::read_to_string(site_dir.join("page-0/index.html")).unwrap();

    assert_eq!(html1, html2, "Fused transforms must be idempotent");
}

#[test]
fn plugin_order_deterministic() {
    let mut plugins = ssg::plugin::PluginManager::new();
    plugins.register(ssg::seo::SeoPlugin);
    plugins.register(ssg::search::SearchPlugin);
    plugins.register(ssg::postprocess::HtmlFixPlugin);

    let names: Vec<&str> = plugins.names();
    assert_eq!(names, vec!["seo", "search", "html-fix"]);

    // Register again in same order — must match
    let mut plugins2 = ssg::plugin::PluginManager::new();
    plugins2.register(ssg::seo::SeoPlugin);
    plugins2.register(ssg::search::SearchPlugin);
    plugins2.register(ssg::postprocess::HtmlFixPlugin);

    let names2: Vec<&str> = plugins2.names();
    assert_eq!(names, names2, "Plugin order must be deterministic");
}
