//! Export passing/failing column lineage tests from the ground truth dataset.
//!
//! Reads `snowflake_ground_truth.jsonl`, evaluates each case with `column_lineage()`,
//! and writes two output files:
//!   - `snowflake_passing.jsonl` — lenient match (all expected cols correct, may have extras)
//!   - `snowflake_failing.jsonl` — everything else (mismatches, partial, parse errors)
//! Plus a `MANIFEST.json` with metadata.
//!
//! Gated behind `EXPORT_LINEAGE=1` so it never runs in CI.
//!
//! Run with:
//!   EXPORT_LINEAGE=1 cargo test --release --test export_passing_lineage -- --nocapture

use altimate_core::lineage::column_lineage;
use altimate_core::schema::types::SqlDialect;
use serde::Deserialize;
use std::collections::HashMap;
use std::io::{BufRead, Write};
use std::path::Path;

#[derive(Debug, Deserialize)]
struct GroundTruthCase {
    #[allow(dead_code)]
    name: String,
    sql: String,
    schema: serde_json::Value,
    #[allow(dead_code)]
    dialect: String,
    #[allow(dead_code)]
    mode: String,
    expected: HashMap<String, Vec<String>>,
    #[allow(dead_code)]
    query_hash: Option<String>,
    #[allow(dead_code)]
    source_query_type: Option<String>,
    #[allow(dead_code)]
    database_name: Option<String>,
    #[allow(dead_code)]
    schema_name: Option<String>,
}

fn schema_from_json(value: &serde_json::Value) -> HashMap<String, serde_json::Value> {
    match value {
        serde_json::Value::Object(map) => map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
        _ => HashMap::new(),
    }
}

fn normalize(map: &HashMap<String, Vec<String>>) -> HashMap<String, Vec<String>> {
    map.iter()
        .map(|(k, v)| {
            let mut sorted = v.clone();
            sorted.sort();
            sorted.dedup();
            (k.clone(), sorted)
        })
        .collect()
}

fn normalize_to_short_names(map: &HashMap<String, Vec<String>>) -> HashMap<String, Vec<String>> {
    map.iter()
        .map(|(k, v)| {
            let normalized: Vec<String> = v
                .iter()
                .map(|ref_str| {
                    let parts: Vec<&str> =
                        ref_str.split('.').map(|p| p.trim_matches('"')).collect();
                    if parts.len() >= 3 {
                        let table = parts[parts.len() - 2];
                        let col = parts[parts.len() - 1];
                        format!("\"{table}\".\"{col}\"")
                    } else {
                        ref_str.clone()
                    }
                })
                .collect();
            let mut sorted = normalized;
            sorted.sort();
            sorted.dedup();
            (k.clone(), sorted)
        })
        .collect()
}

/// Returns true if the case passes lenient match:
/// all expected columns have correct sources (extra output columns are tolerated).
fn is_lenient_match(
    actual: &HashMap<String, Vec<String>>,
    expected: &HashMap<String, Vec<String>>,
) -> bool {
    for (col, exp_sources) in expected {
        match actual.get(col) {
            Some(act_sources) if act_sources == exp_sources => {}
            _ => return false,
        }
    }
    true
}

#[test]
fn export_passing_lineage() {
    if std::env::var("EXPORT_LINEAGE").unwrap_or_default() != "1" {
        eprintln!("Skipping export — set EXPORT_LINEAGE=1 to run.");
        return;
    }

    let fixture_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/lineage");
    let input_path = fixture_dir.join("snowflake_ground_truth.jsonl");

    if !input_path.exists() {
        panic!("Ground-truth fixture not found: {}", input_path.display());
    }

    let passing_path = fixture_dir.join("snowflake_passing.jsonl");
    let failing_path = fixture_dir.join("snowflake_failing.jsonl");
    let manifest_path = fixture_dir.join("MANIFEST.json");

    let file = std::fs::File::open(&input_path).expect("failed to open fixture");
    let reader = std::io::BufReader::with_capacity(1024 * 1024, file);

    let mut passing_file =
        std::io::BufWriter::new(std::fs::File::create(&passing_path).expect("create passing file"));
    let mut failing_file =
        std::io::BufWriter::new(std::fs::File::create(&failing_path).expect("create failing file"));

    let dialect = SqlDialect::Snowflake;
    let mut total = 0u64;
    let mut pass_count = 0u64;
    let mut fail_count = 0u64;
    let mut exact_count = 0u64;
    let mut parse_errors = 0u64;

    for line in reader.lines() {
        let line = line.expect("failed to read line");
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }

        let case: GroundTruthCase = match serde_json::from_str(trimmed) {
            Ok(c) => c,
            Err(_) => continue,
        };
        total += 1;

        let schema = schema_from_json(&case.schema);

        let result = match column_lineage(&case.sql, dialect, &schema, None) {
            Ok(r) => r,
            Err(_) => {
                parse_errors += 1;
                fail_count += 1;
                failing_file
                    .write_all(trimmed.as_bytes())
                    .expect("write fail");
                failing_file.write_all(b"\n").expect("write newline");
                continue;
            }
        };

        let actual = normalize_to_short_names(&result.column_dict);
        let expected = normalize(&case.expected);

        if actual == expected {
            exact_count += 1;
        }

        if is_lenient_match(&actual, &expected) {
            pass_count += 1;
            passing_file
                .write_all(trimmed.as_bytes())
                .expect("write pass");
            passing_file.write_all(b"\n").expect("write newline");
        } else {
            fail_count += 1;
            failing_file
                .write_all(trimmed.as_bytes())
                .expect("write fail");
            failing_file.write_all(b"\n").expect("write newline");
        }

        if total % 2000 == 0 {
            eprintln!("  ... {total}: pass={pass_count} fail={fail_count}");
        }
    }

    passing_file.flush().expect("flush passing");
    failing_file.flush().expect("flush failing");

    let pass_rate = if total > 0 {
        pass_count as f64 / total as f64 * 100.0
    } else {
        0.0
    };
    let exact_rate = if total > 0 {
        exact_count as f64 / total as f64 * 100.0
    } else {
        0.0
    };

    // Write MANIFEST.json
    let manifest = serde_json::json!({
        "dataset": "snowflake_column_lineage_ground_truth",
        "source": "Snowflake access_history (Rakuten production)",
        "total": total,
        "passing": pass_count,
        "failing": fail_count,
        "exact_match": exact_count,
        "parse_errors": parse_errors,
        "pass_rate_lenient": format!("{pass_rate:.1}%"),
        "exact_match_rate": format!("{exact_rate:.1}%"),
        "match_criteria": "lenient — all expected output columns must have correct source columns; extra output columns in actual are tolerated",
        "dialect": "snowflake",
        "engine": "altimate-core column_lineage()",
        "generated_at": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
    });

    std::fs::write(
        &manifest_path,
        serde_json::to_string_pretty(&manifest).expect("serialize manifest"),
    )
    .expect("write manifest");

    eprintln!(
        "\n=== Export Complete ===\n\
         Total:       {total}\n\
         Passing:     {pass_count} ({pass_rate:.1}%) [lenient]\n\
         Exact match: {exact_count} ({exact_rate:.1}%)\n\
         Failing:     {fail_count}\n\
         Parse errors:{parse_errors}\n\
         \n\
         Files:\n\
         - {}\n\
         - {}\n\
         - {}",
        passing_path.display(),
        failing_path.display(),
        manifest_path.display(),
    );
}
