use tree_sitter::Node;

use super::{
    DocCommentSpec, SymbolSink, collect_symbols, find_first_named_child, push_named_symbol,
    walk_children,
};

pub(super) const DOC_SPEC: DocCommentSpec = DocCommentSpec {
    comment_node_types: &["comment"],
    doc_prefixes: Some(&["///"]),
    custom_doc_check: None,
};
use crate::domain::{SymbolKind, SymbolRecord};

pub fn extract_symbols(node: &Node, source: &str) -> Vec<SymbolRecord> {
    collect_symbols(node, source, walk_node)
}

fn walk_node(
    node: &Node,
    source: &str,
    depth: u32,
    sort_order: &mut u32,
    symbols: &mut Vec<SymbolRecord>,
) {
    let kind = match node.kind() {
        "class_declaration" => Some(SymbolKind::Class),
        "struct_declaration" => Some(SymbolKind::Struct),
        "interface_declaration" => Some(SymbolKind::Interface),
        "enum_declaration" => Some(SymbolKind::Enum),
        "method_declaration" => Some(SymbolKind::Method),
        "constructor_declaration" => Some(SymbolKind::Function),
        "local_function_statement" => Some(SymbolKind::Function),
        "namespace_declaration" | "file_scoped_namespace_declaration" => Some(SymbolKind::Module),
        "property_declaration" => Some(SymbolKind::Variable),
        "global_statement" => {
            // Top-level statements in C# — recurse into children to find
            // local_function_statement and expression_statement nodes.
            walk_children(node, source, depth, sort_order, symbols, None, walk_node);
            return;
        }
        _ => None,
    };

    {
        let mut sink = SymbolSink::new(source, sort_order, symbols, &DOC_SPEC);
        push_named_symbol(
            node,
            depth,
            kind,
            |node, source, _| find_name(node, source),
            &mut sink,
        );
    }
    walk_children(node, source, depth, sort_order, symbols, kind, walk_node);
}

fn find_name(node: &Node, source: &str) -> Option<String> {
    // Prefer the grammar's "name" field — this correctly skips return-type
    // identifiers in method_declaration nodes (e.g. `async Task Foo(...)` where
    // the first identifier child is `Task`, not `Foo`).
    if let Some(name_node) = node.child_by_field_name("name") {
        return Some(
            name_node
                .utf8_text(source.as_bytes())
                .unwrap_or("")
                .to_string(),
        );
    }
    find_first_named_child(node, source, &["identifier"])
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::{FileOutcome, LanguageId, SymbolKind};
    use crate::parsing::process_file;
    use tree_sitter::Parser;

    fn parse_csharp(source: &str) -> Vec<SymbolRecord> {
        let mut parser = Parser::new();
        let lang: tree_sitter::Language = tree_sitter_c_sharp::LANGUAGE.into();
        parser.set_language(&lang).expect("set C# language");
        let tree = parser.parse(source, None).expect("parse C# source");
        extract_symbols(&tree.root_node(), source)
    }

    #[test]
    fn test_csharp_class_declaration() {
        let source = "public class Greeter { }";
        let symbols = parse_csharp(source);
        let cls = symbols.iter().find(|s| s.kind == SymbolKind::Class);
        assert!(cls.is_some(), "should extract class, got: {:?}", symbols);
        assert_eq!(cls.unwrap().name, "Greeter");
    }

    #[test]
    fn test_csharp_interface_declaration() {
        let source = "public interface IRunnable { void Run(); }";
        let symbols = parse_csharp(source);
        let iface = symbols.iter().find(|s| s.kind == SymbolKind::Interface);
        assert!(
            iface.is_some(),
            "should extract interface, got: {:?}",
            symbols
        );
        assert_eq!(iface.unwrap().name, "IRunnable");
    }

    #[test]
    fn test_csharp_enum_declaration() {
        let source = "public enum Color { Red, Green, Blue }";
        let symbols = parse_csharp(source);
        let e = symbols.iter().find(|s| s.kind == SymbolKind::Enum);
        assert!(e.is_some(), "should extract enum, got: {:?}", symbols);
        assert_eq!(e.unwrap().name, "Color");
    }

    #[test]
    fn test_csharp_top_level_statements() {
        let source = r#"
using System;

Console.WriteLine("Hello");

void Greet(string name) {
    Console.WriteLine($"Hi, {name}!");
}

int Add(int a, int b) => a + b;
"#;
        let symbols = parse_csharp(source);
        let greet = symbols.iter().find(|s| s.name == "Greet");
        assert!(
            greet.is_some(),
            "should extract top-level local function Greet, got: {:?}",
            symbols
        );
        assert_eq!(greet.unwrap().kind, SymbolKind::Function);
        let add = symbols.iter().find(|s| s.name == "Add");
        assert!(
            add.is_some(),
            "should extract top-level local function Add, got: {:?}",
            symbols
        );
        assert_eq!(add.unwrap().kind, SymbolKind::Function);
    }

    #[test]
    fn test_csharp_async_task_method_name() {
        // Regression: `public async Task Foo(...)` was indexed as "Task" instead
        // of "Foo" because find_name picked the first `identifier` child (the
        // return type) rather than the grammar's "name" field.
        let source = r#"
public class Svc {
    public async Task FlushBufferAsync(int x) { }
    public async Task<string> SearchAsync(string q) { return q; }
    public async Task DoWorkAsync() { }
    public void Sync() { }
    public async void FireAndForget() { }
}
"#;
        let symbols = parse_csharp(source);
        let methods: Vec<&SymbolRecord> = symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Method)
            .collect();

        let names: Vec<&str> = methods.iter().map(|s| s.name.as_str()).collect();
        assert!(
            names.contains(&"FlushBufferAsync"),
            "async Task method: got {:?}",
            names
        );
        assert!(
            names.contains(&"SearchAsync"),
            "async Task<T> method: got {:?}",
            names
        );
        assert!(
            names.contains(&"DoWorkAsync"),
            "async Task no-args method: got {:?}",
            names
        );
        assert!(names.contains(&"Sync"), "void method: got {:?}", names);
        assert!(
            names.contains(&"FireAndForget"),
            "async void method: got {:?}",
            names
        );
        assert!(
            !names.contains(&"Task"),
            "should NOT index 'Task' as a method name: got {:?}",
            names
        );
    }

    #[test]
    fn test_csharp_process_file_returns_processed() {
        let source = b"public class Foo { public void Bar() { } }";
        let result = process_file("test.cs", source, LanguageId::CSharp);
        assert_eq!(
            result.outcome,
            FileOutcome::Processed,
            "outcome: {:?}",
            result.outcome
        );
        assert!(!result.symbols.is_empty(), "should have symbols");
    }
}
