//! Tree-sitter Grammar Integration Tests
//!
//! These tests validate that:
//! 1. The tree-sitter grammar builds correctly
//! 2. All queries (highlights, injections, etc.) are valid
//! 3. Example files parse without errors
//! 4. The Zed extension builds successfully
//!
//! These are acceptance tests that catch integration issues between
//! the Storybook language and its tree-sitter representation.

use std::{
    path::{
        Path,
        PathBuf,
    },
    process::Command,
};

fn tree_sitter_dir() -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("tree-sitter-storybook");
    path
}

fn zed_extension_dir() -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("zed-storybook");
    path
}

fn examples_dir() -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("examples");
    path
}

// ============================================================================
// Tree-sitter Grammar Build Tests
// ============================================================================

#[test]
fn test_tree_sitter_grammar_generates() {
    let output = Command::new("tree-sitter")
        .arg("generate")
        .current_dir(tree_sitter_dir())
        .output()
        .expect("Failed to run tree-sitter generate. Is tree-sitter-cli installed?");

    if !output.status.success() {
        eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
        eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
    }

    assert!(
        output.status.success(),
        "tree-sitter generate failed. Grammar may have syntax errors."
    );

    // Verify generated files exist
    let parser_c = tree_sitter_dir().join("src").join("parser.c");
    assert!(
        parser_c.exists(),
        "parser.c not generated at {:?}",
        parser_c
    );
}

#[test]
fn test_tree_sitter_grammar_builds() {
    let output = Command::new("tree-sitter")
        .arg("generate")
        .current_dir(tree_sitter_dir())
        .output()
        .expect("Failed to run tree-sitter generate");

    if !output.status.success() {
        eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
        eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
    }

    assert!(
        output.status.success(),
        "tree-sitter generate failed. Grammar may not compile."
    );
}

// ============================================================================
// Query Validation Tests
// ============================================================================

#[test]
fn test_highlights_query_valid() {
    let output = Command::new("tree-sitter")
        .arg("test")
        .current_dir(tree_sitter_dir())
        .output()
        .expect("Failed to run tree-sitter test");

    if !output.status.success() {
        eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
        eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
    }

    assert!(
        output.status.success(),
        "tree-sitter test failed. Queries (highlights.scm, etc.) may have errors."
    );
}

#[test]
fn test_queries_reference_valid_nodes() {
    // This test verifies that all node types referenced in queries
    // actually exist in the grammar. This catches issues like:
    // - Using "any" instead of (any_type)
    // - Referencing removed or renamed nodes
    // - Typos in node names

    let highlights_scm = tree_sitter_dir().join("queries").join("highlights.scm");

    assert!(
        highlights_scm.exists(),
        "highlights.scm not found at {:?}",
        highlights_scm
    );

    let content = std::fs::read_to_string(&highlights_scm).expect("Failed to read highlights.scm");

    // Check for common mistakes
    let problematic_patterns = vec![
        (
            "\"any\"",
            "Should use (any_type) instead of \"any\" string literal",
        ),
        (
            "\"Number\"",
            "Number is not a keyword, should use a type node",
        ),
        (
            "\"Decimal\"",
            "Decimal is not a keyword, should use a type node",
        ),
        ("\"Text\"", "Text is not a keyword, should use a type node"),
        (
            "\"Boolean\"",
            "Boolean is not a keyword, should use a type node",
        ),
    ];

    for (pattern, msg) in problematic_patterns {
        if content.contains(pattern) {
            // Check if it's in a comment
            let lines: Vec<&str> = content.lines().collect();
            for line in lines {
                if line.contains(pattern) && !line.trim_start().starts_with(';') {
                    panic!(
                        "highlights.scm contains problematic pattern '{}': {}",
                        pattern, msg
                    );
                }
            }
        }
    }
}

// ============================================================================
// Example File Parsing Tests
// ============================================================================

#[test]
fn test_baker_family_example_parses() {
    let baker_family = examples_dir().join("baker-family");
    assert!(baker_family.exists(), "baker-family example not found");

    // Find all .sb files
    let sb_files = find_sb_files(&baker_family);
    assert!(
        !sb_files.is_empty(),
        "No .sb files found in baker-family example"
    );

    for file in &sb_files {
        let output = Command::new("tree-sitter")
            .arg("parse")
            .arg(file)
            .current_dir(tree_sitter_dir())
            .output()
            .expect("Failed to run tree-sitter parse");

        if !output.status.success() {
            eprintln!("Failed to parse: {:?}", file);
            eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
            eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
        }

        assert!(
            output.status.success(),
            "Failed to parse {:?} with tree-sitter",
            file
        );

        // Check for ERROR nodes in the parse tree
        let stdout = String::from_utf8_lossy(&output.stdout);
        if stdout.contains("ERROR") {
            eprintln!("Parse tree for {:?}:", file);
            eprintln!("{}", stdout);
            panic!("Parse tree contains ERROR nodes for {:?}", file);
        }
    }
}

#[test]
fn test_all_examples_parse() {
    let sb_files = find_sb_files(&examples_dir());
    assert!(!sb_files.is_empty(), "No .sb files found in examples/");

    let mut failures = Vec::new();

    for file in &sb_files {
        let output = Command::new("tree-sitter")
            .arg("parse")
            .arg(file)
            .current_dir(tree_sitter_dir())
            .output()
            .expect("Failed to run tree-sitter parse");

        if !output.status.success() {
            failures.push((file.clone(), "Parse command failed".to_string()));
            continue;
        }

        // Check for ERROR nodes
        let stdout = String::from_utf8_lossy(&output.stdout);
        if stdout.contains("ERROR") {
            failures.push((file.clone(), "Parse tree contains ERROR nodes".to_string()));
        }
    }

    if !failures.is_empty() {
        eprintln!("Failed to parse {} files:", failures.len());
        for (file, reason) in &failures {
            eprintln!("  - {:?}: {}", file, reason);
        }
        panic!("{} files failed to parse with tree-sitter", failures.len());
    }
}

// ============================================================================
// Specific Language Feature Tests
// ============================================================================

#[test]
fn test_type_system_keywords_parse() {
    let test_file = tree_sitter_dir()
        .join("test")
        .join("corpus")
        .join("type_system.txt");

    if !test_file.exists() {
        panic!(
            "Type system test corpus not found at {:?}. Please create it.",
            test_file
        );
    }

    let _test_file = tree_sitter_dir()
        .join("test")
        .join("corpus")
        .join("type_system.txt");

    let output = Command::new("tree-sitter")
        .arg("test")
        .arg("--file-name")
        .arg("type_system.txt")
        .current_dir(tree_sitter_dir())
        .output()
        .expect("Failed to run tree-sitter test");

    if !output.status.success() {
        eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
        eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
    }

    assert!(output.status.success(), "Type system test corpus failed");
}

#[test]
fn test_any_type_highlights_correctly() {
    // Create a minimal test file with any_type usage
    let test_content = r#"
definition SkillLevel {
    Novice: { skill: any },
    Expert: { skill: Tier is Expert }
}
"#;

    let temp_dir = std::env::temp_dir();
    let test_file = temp_dir.join("test_any_type.sb");
    std::fs::write(&test_file, test_content).expect("Failed to write test file");

    let output = Command::new("tree-sitter")
        .arg("parse")
        .arg(&test_file)
        .current_dir(tree_sitter_dir())
        .output()
        .expect("Failed to run tree-sitter parse");

    std::fs::remove_file(&test_file).ok(); // Clean up

    if !output.status.success() {
        eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
        eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
    }

    assert!(output.status.success(), "Failed to parse any_type usage");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("any_type"),
        "Parse tree should contain any_type node"
    );
    assert!(
        !stdout.contains("ERROR"),
        "Parse tree should not contain ERROR nodes"
    );
}

// ============================================================================
// Zed Extension Build Tests
// ============================================================================

#[test]
fn test_zed_highlights_query_not_overridden() {
    // This test ensures the Zed extension doesn't have a local highlights.scm
    // that overrides the grammar's version. Local overrides create a maintenance
    // burden and can easily diverge from the source of truth.
    //
    // The preferred approach is to let Zed use the fetched grammar's queries.
    // If a local override is needed (for Zed-specific customizations), this test
    // will fail and remind us to keep them in sync.

    let grammar_highlights = tree_sitter_dir().join("queries").join("highlights.scm");
    let zed_highlights = zed_extension_dir()
        .join("languages")
        .join("storybook")
        .join("highlights.scm");

    // Verify grammar version exists
    assert!(
        grammar_highlights.exists(),
        "Grammar highlights.scm not found at {:?}",
        grammar_highlights
    );

    // If Zed has a local override, it MUST match the grammar version exactly
    if zed_highlights.exists() {
        let grammar_content = std::fs::read_to_string(&grammar_highlights)
            .expect("Failed to read grammar highlights");
        let zed_content =
            std::fs::read_to_string(&zed_highlights).expect("Failed to read zed highlights");

        assert_eq!(
            grammar_content,
            zed_content,
            "\n\nZed extension has a local highlights.scm that differs from the grammar version.\n\
             This causes the query to fail validation against the fetched grammar.\n\
             \n\
             To fix:\n\
             1. Delete zed-storybook/languages/storybook/highlights.scm (preferred)\n\
             2. Or sync it: cp tree-sitter-storybook/queries/highlights.scm zed-storybook/languages/storybook/\n\
             \n\
             Grammar version: {:?}\n\
             Zed version: {:?}\n",
            grammar_highlights,
            zed_highlights
        );
    }
    // If no local override exists, the test passes - Zed will use the fetched
    // grammar's version
}

#[test]
#[ignore] // Only run with --ignored flag as this is slow
fn test_zed_extension_builds() {
    let build_script = zed_extension_dir().join("build-extension.sh");

    if !build_script.exists() {
        panic!("build-extension.sh not found at {:?}", build_script);
    }

    let output = Command::new("bash")
        .arg(&build_script)
        .current_dir(zed_extension_dir())
        .output()
        .expect("Failed to run build-extension.sh");

    if !output.status.success() {
        eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
        eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
    }

    assert!(output.status.success(), "build-extension.sh failed");

    // Verify the extension.wasm was created
    let wasm = zed_extension_dir().join("extension.wasm");
    assert!(wasm.exists(), "extension.wasm not created at {:?}", wasm);
}

#[test]
fn test_extension_toml_has_valid_revision() {
    let extension_toml = zed_extension_dir().join("extension.toml");
    let content = std::fs::read_to_string(&extension_toml).expect("Failed to read extension.toml");

    // Check that rev is set to a valid commit SHA
    let rev_line = content
        .lines()
        .find(|line| line.starts_with("rev = "))
        .expect("No 'rev' field found in extension.toml");

    let rev = rev_line
        .split('"')
        .nth(1)
        .expect("Could not parse rev value");

    assert_eq!(
        rev.len(),
        40,
        "Revision should be a 40-character git SHA, got: {}",
        rev
    );

    // Verify the commit exists
    let output = Command::new("git")
        .arg("cat-file")
        .arg("-t")
        .arg(rev)
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .output()
        .expect("Failed to run git cat-file");

    if !output.status.success() {
        eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
    }

    assert!(
        output.status.success(),
        "Revision {} does not exist in git history",
        rev
    );

    let obj_type = String::from_utf8_lossy(&output.stdout);
    assert_eq!(
        obj_type.trim(),
        "commit",
        "Revision {} is not a commit",
        rev
    );
}

// ============================================================================
// Helper Functions
// ============================================================================

fn find_sb_files(dir: &Path) -> Vec<PathBuf> {
    let mut sb_files = Vec::new();

    if !dir.exists() {
        return sb_files;
    }

    visit_dirs(dir, &mut |path| {
        if path.extension().and_then(|s| s.to_str()) == Some("sb") {
            sb_files.push(path.to_path_buf());
        }
    });

    sb_files
}

fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&Path)) {
    if dir.is_dir() {
        for entry in std::fs::read_dir(dir).unwrap() {
            let entry = entry.unwrap();
            let path = entry.path();
            if path.is_dir() {
                visit_dirs(&path, cb);
            } else {
                cb(&path);
            }
        }
    }
}
