use camino::{Utf8Path, Utf8PathBuf};
use protobuf::Enum;
use psql_scip::{
    core::{ddl::walk_ddl, resolver::walk_references, schema::SchemaModel},
    emit::emit_index,
    parse::split_and_parse,
};
use scip::types::{SymbolRole, symbol_information::Kind};

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_full_index() -> scip::types::Index {
    let schema_content = fixture_sql("schema_full.sql");
    let schema_path = Utf8PathBuf::from("schema_full.sql");
    // re-index the schema under a query path so reference occurrences land in their own document
    let query_path = Utf8PathBuf::from("queries/schema_full.sql");
    let (stmts, failures) = split_and_parse(&schema_content).expect("schema must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let refs = walk_references(&stmts, &schema_content, &model);
    let project_root = Utf8Path::new(".");
    emit_index(
        &schema_path,
        &schema_content,
        &failures,
        &model,
        &[(query_path, schema_content.clone(), refs, failures.clone())],
        project_root,
    )
}

fn schema_doc(index: &scip::types::Index) -> &scip::types::Document {
    index
        .documents
        .iter()
        .find(|d| d.relative_path == "schema_full.sql")
        .expect("schema document must be present in index")
}

fn ref_doc(index: &scip::types::Index) -> &scip::types::Document {
    index
        .documents
        .iter()
        .find(|d| d.relative_path == "queries/schema_full.sql")
        .expect("ref document must be present in index")
}

fn kind_label(kind: &protobuf::EnumOrUnknown<Kind>) -> String {
    format!("{:?}", kind.enum_value_or_default())
}

#[derive(serde::Serialize)]
struct SymbolRow {
    symbol: String,
    display_name: String,
    kind: String,
    enclosing_symbol: String,
    doc: String,
    relationships: Vec<String>,
}

#[derive(serde::Serialize)]
struct OccurrenceRow {
    symbol: String,
    range: Vec<i32>,
}

fn collect_symbols(index: &scip::types::Index) -> Vec<SymbolRow> {
    let doc = schema_doc(index);
    let mut rows: Vec<SymbolRow> = doc
        .symbols
        .iter()
        .map(|si| {
            let mut rels: Vec<String> = si.relationships.iter().map(|r| r.symbol.clone()).collect();
            rels.sort();
            let doc_text = si
                .signature_documentation
                .as_ref()
                .map(|d| d.text.clone())
                .unwrap_or_default();
            SymbolRow {
                symbol: si.symbol.clone(),
                display_name: si.display_name.clone(),
                kind: kind_label(&si.kind),
                enclosing_symbol: si.enclosing_symbol.clone(),
                doc: doc_text,
                relationships: rels,
            }
        })
        .collect();
    rows.sort_by(|a, b| a.symbol.cmp(&b.symbol));
    rows
}

fn collect_definition_occurrences(index: &scip::types::Index) -> Vec<OccurrenceRow> {
    let doc = schema_doc(index);
    let def_role = SymbolRole::Definition.value();
    let mut rows: Vec<OccurrenceRow> = doc
        .occurrences
        .iter()
        .filter(|o| o.symbol_roles == def_role)
        .map(|o| OccurrenceRow {
            symbol: o.symbol.clone(),
            range: o.range.clone(),
        })
        .collect();
    rows.sort_by(|a, b| a.symbol.cmp(&b.symbol));
    rows
}

fn collect_reference_occurrences(index: &scip::types::Index) -> Vec<OccurrenceRow> {
    let doc = ref_doc(index);
    let def_bit = SymbolRole::Definition.value();
    let mut rows: Vec<OccurrenceRow> = doc
        .occurrences
        .iter()
        .filter(|o| (o.symbol_roles & def_bit) == 0)
        .map(|o| OccurrenceRow {
            symbol: o.symbol.clone(),
            range: o.range.clone(),
        })
        .collect();
    rows.sort_by(|a, b| a.symbol.cmp(&b.symbol).then_with(|| a.range.cmp(&b.range)));
    rows
}

#[test]
fn schema_full_symbols() {
    let index = build_full_index();
    let symbols = collect_symbols(&index);
    insta::assert_yaml_snapshot!("schema_full_symbols", symbols);
}

#[test]
fn schema_full_definition_occurrences() {
    let index = build_full_index();
    let occurrences = collect_definition_occurrences(&index);
    insta::assert_yaml_snapshot!("schema_full_definition_occurrences", occurrences);
}

#[test]
fn schema_full_reference_occurrences() {
    let index = build_full_index();
    let occurrences = collect_reference_occurrences(&index);
    insta::assert_yaml_snapshot!("schema_full_reference_occurrences", occurrences);
}
