use camino::Utf8PathBuf;
use psql_scip::{
    core::{ddl::walk_ddl, resolver::walk_references, schema::SchemaModel},
    emit::{emit_index, read_file, write_index},
    errors::EmitError,
    parse::split_and_parse,
};
use scip::types::SymbolRole;

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_path(name: &str) -> Utf8PathBuf {
    manifest_dir().join("tests/fixtures").join(name)
}

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

fn build_emit_index() -> scip::types::Index {
    let schema_path = fixture_path("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 = fixture_path("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);

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

#[test]
fn index_has_two_documents() {
    let index = build_emit_index();
    assert_eq!(index.documents.len(), 2, "must have schema doc + query doc");
}

#[test]
fn schema_document_has_definition_occurrences() {
    let index = build_emit_index();
    let schema_doc = &index.documents[0];
    assert!(
        schema_doc
            .occurrences
            .iter()
            .any(|o| o.symbol_roles == SymbolRole::Definition as i32),
        "schema document must contain definition occurrences"
    );
}

#[test]
fn query_document_has_reference_occurrences() {
    let index = build_emit_index();
    let query_doc = &index.documents[1];
    assert!(
        !query_doc.occurrences.is_empty(),
        "query document must contain reference occurrences"
    );
    assert!(
        query_doc.occurrences.iter().all(|o| o.symbol_roles == 0),
        "query document occurrences must have role 0 (reference)"
    );
}

#[test]
fn no_occurrence_has_empty_symbol() {
    let index = build_emit_index();
    for doc in &index.documents {
        for occ in &doc.occurrences {
            // Diagnostic-only occurrences (for parse failures) have empty symbols.
            if !occ.diagnostics.is_empty() {
                continue;
            }
            assert!(
                !occ.symbol.is_empty(),
                "occurrence must have non-empty symbol"
            );
        }
    }
}

#[test]
fn all_occurrences_have_valid_ranges() {
    let index = build_emit_index();
    for doc in &index.documents {
        for occ in &doc.occurrences {
            assert!(
                occ.range.len() == 3 || occ.range.len() == 4,
                "range must have 3 or 4 elements, got {}",
                occ.range.len()
            );
            assert!(
                occ.range.iter().all(|&v| v >= 0),
                "all range values must be >= 0"
            );
        }
    }
}

#[test]
fn table_symbol_info_has_doc() {
    let index = build_emit_index();
    let schema_doc = &index.documents[0];
    let items_symbol = schema_doc
        .symbols
        .iter()
        .find(|s| s.symbol.contains("public/items") && !s.symbol.contains('#'))
        .expect("must find items symbol info");
    assert!(
        !items_symbol.documentation.is_empty() && !items_symbol.documentation[0].is_empty(),
        "items symbol must have non-empty documentation"
    );
}

#[test]
fn column_symbol_info_includes_type() {
    let index = build_emit_index();
    let schema_doc = &index.documents[0];
    let id_sym = schema_doc
        .symbols
        .iter()
        .find(|s| s.symbol.contains("public/items#id"))
        .expect("must find items#id symbol info");
    // The DDL walker normalises `bigint` to the pg-internal name `int8`.
    assert!(
        id_sym
            .documentation
            .iter()
            .any(|d| d.contains("int8") || d.contains("bigint")),
        "id column doc must contain integer type name; got: {:?}",
        id_sym.documentation
    );
}

#[test]
fn not_null_column_doc_says_not_null() {
    let index = build_emit_index();
    let schema_doc = &index.documents[0];
    let id_sym = schema_doc
        .symbols
        .iter()
        .find(|s| s.symbol.contains("public/items#id"))
        .expect("must find items#id symbol info");
    assert!(
        id_sym.documentation.iter().any(|d| d.contains("NOT NULL")),
        "id column doc must contain 'NOT NULL'; got: {:?}",
        id_sym.documentation
    );
}

#[test]
fn nullable_column_doc_does_not_say_not_null() {
    let index = build_emit_index();
    let schema_doc = &index.documents[0];
    let label_sym = schema_doc
        .symbols
        .iter()
        .find(|s| s.symbol.contains("public/items#label"))
        .expect("must find items#label symbol info");
    assert!(
        !label_sym
            .documentation
            .iter()
            .any(|d| d.contains("NOT NULL")),
        "label column doc must not contain 'NOT NULL'; got: {:?}",
        label_sym.documentation
    );
}

#[test]
fn write_index_roundtrip() {
    let index = build_emit_index();
    let tmp = tempfile::NamedTempFile::new().expect("must create temp file");
    let path =
        Utf8PathBuf::from_path_buf(tmp.path().to_owned()).expect("temp file path must be UTF-8");
    write_index(index, &path).expect("write_index must succeed");
    let bytes = std::fs::read(tmp.path()).expect("must read temp file");
    assert!(!bytes.is_empty(), "written index file must be non-empty");
}

#[test]
fn emit_index_with_absolute_paths_produces_relative_doc_paths() {
    // Build a temp directory so absolute paths are well-defined.
    let tmp = tempfile::TempDir::new().expect("must create tempdir");
    let root =
        Utf8PathBuf::from_path_buf(tmp.path().to_owned()).expect("tempdir path must be UTF-8");

    let schema_sql = "CREATE TABLE public.t (id bigint NOT NULL);";
    let query_sql = "SELECT id FROM public.t;";

    let schema_path = root.join("schema.sql");
    let query_path = root.join("query.sql");

    std::fs::write(schema_path.as_std_path(), schema_sql).expect("write schema");
    std::fs::write(query_path.as_std_path(), query_sql).expect("write query");

    let (stmts, schema_failures) = split_and_parse(schema_sql).expect("schema must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);

    let (query_stmts, query_failures) = split_and_parse(query_sql).expect("query must parse");
    let refs = walk_references(&query_stmts, query_sql, &model);

    // Pass absolute paths; emit_index must relativize them using project_root.
    let index = emit_index(
        &schema_path,
        schema_sql,
        &schema_failures,
        &model,
        &[(query_path, query_sql.to_owned(), refs, query_failures)],
        &root,
    );

    assert_eq!(index.documents.len(), 2, "must have 2 documents");
    for doc in &index.documents {
        assert!(
            !doc.relative_path.starts_with('/'),
            "emit_index must relativize absolute paths; got: {:?}",
            doc.relative_path,
        );
    }
}

#[test]
fn read_file_missing_returns_read_error() {
    let path = Utf8PathBuf::from("/tmp/psql_scip_test_nonexistent_file_abc123.sql");
    let result = read_file(&path);
    assert!(result.is_err(), "read_file on missing path must fail");
    let err = result.unwrap_err();
    assert!(
        matches!(err, EmitError::Read { .. }),
        "error must be EmitError::Read, got: {err:?}"
    );
    let msg = err.to_string();
    assert!(
        msg.contains("psql_scip_test_nonexistent_file_abc123"),
        "error message must include path: {msg}"
    );
}
