//! Batch 3: syntax_kind, enclosing_range, diagnostics, signature_documentation.
use camino::Utf8PathBuf;
use psql_scip::{
    core::{ddl::walk_ddl, resolver::walk_references, schema::SchemaModel},
    emit::emit_index,
    parse::{ParseFailure, split_and_parse},
};
use scip::types::{SyntaxKind, symbol_information::Kind};

// scip 0.5.2 lacks IdentifierDefinition/IdentifierReference; use closest equivalents
const DEF_KIND: SyntaxKind = SyntaxKind::IdentifierFunctionDefinition;
const REF_KIND: SyntaxKind = SyntaxKind::Identifier;

fn manifest_dir() -> Utf8PathBuf {
    let base =
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by cargo test");
    Utf8PathBuf::from(base)
}

fn fixture_sql(name: &str) -> String {
    let path = manifest_dir().join("tests/fixtures").join(name);
    std::fs::read_to_string(path.as_std_path())
        .unwrap_or_else(|_| panic!("fixture {name} must be readable"))
}

fn build_batch3_index() -> scip::types::Index {
    let schema_path = manifest_dir().join("tests/fixtures/schema_for_emit.sql");
    let schema_content = fixture_sql("schema_for_emit.sql");
    let (stmts, schema_failures) = split_and_parse(&schema_content).expect("schema must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);

    let query_path = manifest_dir().join("tests/fixtures/query_for_emit.sql");
    let query_content = fixture_sql("query_for_emit.sql");
    let (query_stmts, query_failures) = split_and_parse(&query_content).expect("query must parse");
    let refs = walk_references(&query_stmts, &query_content, &model);

    emit_index(
        &schema_path,
        &schema_content,
        &schema_failures,
        &model,
        &[(query_path, query_content, refs, query_failures)],
        &manifest_dir(),
    )
}

#[test]
fn definition_occurrences_have_identifier_definition_syntax_kind() {
    let index = build_batch3_index();
    let schema_doc = &index.documents[0];
    let def_occs: Vec<_> = schema_doc
        .occurrences
        .iter()
        .filter(|o| o.symbol_roles != 0)
        .collect();
    assert!(
        !def_occs.is_empty(),
        "schema document must have definition occurrences"
    );
    for occ in &def_occs {
        assert_eq!(
            occ.syntax_kind.enum_value(),
            Ok(DEF_KIND),
            "def occurrence must have DEF_KIND syntax_kind; symbol={}",
            occ.symbol
        );
    }
}

#[test]
fn reference_occurrences_have_identifier_reference_syntax_kind() {
    let index = build_batch3_index();
    let query_doc = &index.documents[1];
    assert!(
        !query_doc.occurrences.is_empty(),
        "query document must have reference occurrences"
    );
    for occ in &query_doc.occurrences {
        // diagnostics-only occurrences have UnspecifiedSyntaxKind — skip those
        if occ.symbol.is_empty() {
            continue;
        }
        assert_eq!(
            occ.syntax_kind.enum_value(),
            Ok(REF_KIND),
            "ref occurrence must have REF_KIND syntax_kind; symbol={}",
            occ.symbol
        );
    }
}

#[test]
fn reference_occurrences_have_enclosing_range() {
    let index = build_batch3_index();
    let query_doc = &index.documents[1];
    let ref_occs: Vec<_> = query_doc
        .occurrences
        .iter()
        .filter(|o| !o.symbol.is_empty())
        .collect();
    assert!(
        !ref_occs.is_empty(),
        "query document must have reference occurrences"
    );
    let with_enclosing = ref_occs
        .iter()
        .filter(|o| !o.enclosing_range.is_empty())
        .count();
    assert!(
        with_enclosing > 0,
        "at least one ref occurrence must have enclosing_range; got 0 of {}",
        ref_occs.len()
    );
}

#[test]
fn enclosing_range_is_wider_than_identifier_range() {
    let index = build_batch3_index();
    let query_doc = &index.documents[1];
    for occ in &query_doc.occurrences {
        if occ.enclosing_range.is_empty() || occ.symbol.is_empty() {
            continue;
        }
        // enclosing_range must not equal range — otherwise it is meaningless
        assert_ne!(
            occ.enclosing_range, occ.range,
            "enclosing_range must differ from range for symbol={}",
            occ.symbol
        );
    }
}

#[test]
fn resolved_ref_has_statement_range_field() {
    // Structural test: ResolvedRef must compile with statement_range field.
    use psql_scip::core::resolver::ResolvedRef;
    use text_size::TextRange;
    let r = ResolvedRef {
        symbol: "public/items".to_owned(),
        range: TextRange::default(),
        statement_range: TextRange::default(),
    };
    assert_eq!(r.symbol, "public/items");
    let _ = r.statement_range; // field must be accessible
}

// scanner silently drops unrecognizable tokens; ParseFailure only when
// scanner passes a statement the parser rejects ("SELECT 1 FROM": valid tokens, invalid syntax)

#[test]
fn split_and_parse_returns_failure_for_invalid_statement() {
    // "SELECT 1 FROM" is returned by the scanner but rejected by the parser.
    let (stmts, failures) =
        split_and_parse("SELECT 1; SELECT 1 FROM; SELECT 2;").expect("must not return Err");
    assert_eq!(stmts.len(), 2, "must have two parseable statements");
    assert_eq!(failures.len(), 1, "must have one failure");
    assert!(
        !failures[0].message.is_empty(),
        "failure message must be non-empty"
    );
}

#[test]
fn split_and_parse_empty_still_returns_err() {
    use psql_scip::errors::ParseError;
    let result = split_and_parse("   ");
    assert!(
        matches!(result, Err(ParseError::EmptyInput)),
        "blank input must still return EmptyInput"
    );
}

#[test]
fn parse_failure_struct_has_range_and_message() {
    // "SELECT 1 FROM" fails to parse — verifies ParseFailure fields.
    let (_, failures) =
        split_and_parse("SELECT 1; SELECT 1 FROM; SELECT 2;").expect("must not return Err");
    assert_eq!(failures.len(), 1, "must have exactly one failure");
    let f: &ParseFailure = &failures[0];
    assert!(
        u32::from(f.range.start()) > 0,
        "failure range must start after offset 0"
    );
    assert!(!f.message.is_empty(), "failure message must be non-empty");
}

#[test]
fn index_with_invalid_statement_has_diagnostic_occurrence() {
    // Build an index where the schema has one valid + one invalid statement.
    // "SELECT 1 FROM" is returned by the scanner but rejected by the parser.
    let schema_sql = "CREATE TABLE public.items (id bigint NOT NULL); SELECT 1 FROM;";
    let schema_path = manifest_dir().join("tests/fixtures/schema_for_emit.sql");
    let (stmts, schema_failures) = split_and_parse(schema_sql).expect("must not return Err");
    assert_eq!(schema_failures.len(), 1, "must have one failure");

    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);

    let index = emit_index(
        &schema_path,
        schema_sql,
        &schema_failures,
        &model,
        &[],
        &manifest_dir(),
    );

    let schema_doc = &index.documents[0];
    let diag_occs: Vec<_> = schema_doc
        .occurrences
        .iter()
        .filter(|o| !o.diagnostics.is_empty())
        .collect();
    assert_eq!(diag_occs.len(), 1, "must have one diagnostic occurrence");
    assert!(
        !diag_occs[0].diagnostics[0].message.is_empty(),
        "diagnostic message must be non-empty"
    );
}

#[test]
fn table_symbol_has_signature_documentation() {
    let index = build_batch3_index();
    let schema_doc = &index.documents[0];
    let table_si = schema_doc
        .symbols
        .iter()
        .find(|s| s.symbol.contains("public/items") && !s.symbol.contains('#'))
        .expect("must find items symbol");
    let sig = table_si
        .signature_documentation
        .as_ref()
        .expect("table must have signature_documentation");
    assert_eq!(sig.language, "SQL", "signature doc language must be SQL");
    assert!(
        sig.text.contains("TABLE"),
        "table signature must contain TABLE; got: {:?}",
        sig.text
    );
}

#[test]
fn column_symbol_has_signature_documentation() {
    let index = build_batch3_index();
    let schema_doc = &index.documents[0];
    let col_si = schema_doc
        .symbols
        .iter()
        .find(|s| s.symbol.contains("public/items#id"))
        .expect("must find items#id symbol");
    let sig = col_si
        .signature_documentation
        .as_ref()
        .expect("column must have signature_documentation");
    assert_eq!(sig.language, "SQL");
    // column signature is display_doc() — contains the type name
    assert!(
        !sig.text.is_empty(),
        "column signature text must be non-empty"
    );
}

#[test]
fn function_symbol_has_signature_documentation() {
    let index = build_batch3_index();
    let schema_doc = &index.documents[0];
    let fn_si = schema_doc.symbols.iter().find(|s| s.symbol.contains("()"));
    if let Some(fn_si) = fn_si {
        let sig = fn_si
            .signature_documentation
            .as_ref()
            .expect("function must have signature_documentation");
        assert_eq!(sig.language, "SQL");
        assert!(
            sig.text.contains("FUNCTION"),
            "function signature must contain FUNCTION; got: {:?}",
            sig.text
        );
    }
    // If no function in fixture, test trivially passes — that's acceptable.
}

#[test]
fn signature_doc_language_is_sql_for_all_symbols() {
    let index = build_batch3_index();
    let schema_doc = &index.documents[0];
    for si in &schema_doc.symbols {
        if let Some(sig) = si.signature_documentation.as_ref() {
            assert_eq!(
                sig.language, "SQL",
                "signature_documentation language must be SQL for symbol {}",
                si.symbol
            );
        }
    }
}

#[test]
fn all_table_symbols_have_signature_doc() {
    let index = build_batch3_index();
    let schema_doc = &index.documents[0];
    for si in &schema_doc.symbols {
        if si.kind.enum_value() == Ok(Kind::Struct) && !si.symbol.contains('#') {
            assert!(
                si.signature_documentation.is_some(),
                "table symbol {} must have signature_documentation",
                si.symbol
            );
        }
    }
}
