//! Comprehensive tests for the dbt integration module.

use altimate_core::dbt::parser::parse_dbt_project;
use std::path::Path;

// ─── Helpers ─────────────────────────────────────────────────────

fn dbt_fixture_dir() -> std::path::PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/dbt")
}

// ─── Project parsing ─────────────────────────────────────────────

#[test]
fn test_parse_dbt_project_succeeds() {
    let r = parse_dbt_project(&dbt_fixture_dir());
    assert!(r.is_ok(), "Failed to parse dbt project: {:?}", r.err());
}

#[test]
fn test_project_name() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    assert_eq!(project.name, "test_project");
}

#[test]
fn test_discovers_all_models() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let names: Vec<&str> = project.models.iter().map(|m| m.name.as_str()).collect();
    assert!(names.contains(&"users"));
    assert!(names.contains(&"orders"));
    assert!(names.contains(&"order_summary"));
}

#[test]
fn test_model_count() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    assert_eq!(project.models.len(), 3);
}

// ─── ref() resolution ────────────────────────────────────────────

#[test]
fn test_orders_refs_users() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let orders = project.models.iter().find(|m| m.name == "orders").unwrap();
    assert!(orders.refs.contains(&"users".to_string()));
}

#[test]
fn test_order_summary_refs_orders() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let summary = project
        .models
        .iter()
        .find(|m| m.name == "order_summary")
        .unwrap();
    assert!(summary.refs.contains(&"orders".to_string()));
}

#[test]
fn test_users_has_no_refs() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let users = project.models.iter().find(|m| m.name == "users").unwrap();
    assert!(users.refs.is_empty());
}

// ─── source() resolution ─────────────────────────────────────────

#[test]
fn test_users_sources() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let users = project.models.iter().find(|m| m.name == "users").unwrap();
    assert!(!users.sources.is_empty());
    assert_eq!(users.sources[0].source_name, "raw");
    assert_eq!(users.sources[0].table_name, "users_raw");
}

#[test]
fn test_orders_sources() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let orders = project.models.iter().find(|m| m.name == "orders").unwrap();
    assert!(orders.sources.iter().any(|s| s.table_name == "orders_raw"));
}

#[test]
fn test_order_summary_has_no_sources() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let summary = project
        .models
        .iter()
        .find(|m| m.name == "order_summary")
        .unwrap();
    assert!(summary.sources.is_empty());
}

// ─── Jinja stripping ─────────────────────────────────────────────

#[test]
fn test_jinja_ref_stripped() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let orders = project.models.iter().find(|m| m.name == "orders").unwrap();
    // The stripped SQL should not contain {{ ref('...') }}
    assert!(!orders.sql.contains("{{"));
    assert!(!orders.sql.contains("}}"));
}

#[test]
fn test_jinja_source_stripped() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let users = project.models.iter().find(|m| m.name == "users").unwrap();
    assert!(!users.sql.contains("{{"));
    assert!(!users.sql.contains("}}"));
}

#[test]
fn test_raw_sql_preserved() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let orders = project.models.iter().find(|m| m.name == "orders").unwrap();
    // raw_sql should still have Jinja
    assert!(orders.raw_sql.contains("ref('users')"));
}

#[test]
fn test_ref_extracted_into_refs_field() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let orders = project.models.iter().find(|m| m.name == "orders").unwrap();
    // The ref('users') is captured in the refs field
    assert!(orders.refs.contains(&"users".to_string()));
}

#[test]
fn test_source_extracted_into_sources_field() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let users = project.models.iter().find(|m| m.name == "users").unwrap();
    // The source('raw', 'users_raw') is captured in the sources field
    assert!(
        users
            .sources
            .iter()
            .any(|s| s.source_name == "raw" && s.table_name == "users_raw")
    );
}

// ─── Build order ─────────────────────────────────────────────────

#[test]
fn test_build_order_includes_all_models() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    assert_eq!(project.build_order.len(), 3);
}

#[test]
fn test_build_order_users_before_orders() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let users_pos = project
        .build_order
        .iter()
        .position(|m| m == "users")
        .expect("users in build order");
    let orders_pos = project
        .build_order
        .iter()
        .position(|m| m == "orders")
        .expect("orders in build order");
    assert!(users_pos < orders_pos, "users should build before orders");
}

#[test]
fn test_build_order_orders_before_summary() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let orders_pos = project
        .build_order
        .iter()
        .position(|m| m == "orders")
        .expect("orders in build order");
    let summary_pos = project
        .build_order
        .iter()
        .position(|m| m == "order_summary")
        .expect("order_summary in build order");
    assert!(
        orders_pos < summary_pos,
        "orders should build before order_summary"
    );
}

// ─── Schema metadata ─────────────────────────────────────────────

#[test]
fn test_model_description_from_schema_yml() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let users = project.models.iter().find(|m| m.name == "users").unwrap();
    assert_eq!(
        users.description,
        Some("Active users from raw data".to_string())
    );
}

#[test]
fn test_model_materialization_from_schema_yml() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    let users = project.models.iter().find(|m| m.name == "users").unwrap();
    assert_eq!(users.materialization, Some("view".to_string()));

    let orders = project.models.iter().find(|m| m.name == "orders").unwrap();
    assert_eq!(orders.materialization, Some("table".to_string()));
}

// ─── Model paths ─────────────────────────────────────────────────

#[test]
fn test_model_path_is_relative() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    for model in &project.models {
        assert!(model.path.starts_with("models/"));
    }
}

// ─── Error handling ──────────────────────────────────────────────

#[test]
fn test_nonexistent_dir_returns_error() {
    let r = parse_dbt_project(Path::new("/nonexistent/path"));
    assert!(r.is_err());
}

#[test]
fn test_missing_dbt_project_yml_returns_error() {
    let tmp = std::env::temp_dir().join("altimate_dbt_test_empty");
    std::fs::create_dir_all(&tmp).ok();
    let r = parse_dbt_project(&tmp);
    assert!(r.is_err());
    std::fs::remove_dir_all(&tmp).ok();
}

// ─── Warnings ────────────────────────────────────────────────────

#[test]
fn test_no_warnings_for_valid_project() {
    let project = parse_dbt_project(&dbt_fixture_dir()).unwrap();
    // There may be warnings about missing ref targets, but no circular dependency warning
    assert!(!project.warnings.iter().any(|w| w.contains("Circular")));
}
