//! Full-pipeline end-to-end tests over a committed, fully-synthetic RF2 snapshot
//! (`tests/fixtures/rf2/`, generated by `generate.py` - not real SNOMED content,
//! so licence-free). Each test runs the real `sct ndjson` and `sct sqlite`
//! commands over the fixture, then asserts across every feature: active
//! filtering, GB/US dialect preferred terms, hierarchy, typed relationships,
//! CTV3 maps, refset membership, FTS5 search, the transitive-closure table, and
//! ECL (hierarchy / boolean / refset / attribute refinement).

use rusqlite::Connection;
use sct_rs::commands::ndjson::{self, RefsetMode};
use sct_rs::commands::sqlite;
use sct_rs::ecl;
use sct_rs::schema::ConceptRecord;
use std::path::{Path, PathBuf};

const EXAMPLE_REFSET: &str = "991381000000107";

fn fixture_dir() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/rf2/SnomedCT_SyntheticTest_PRODUCTION_20260101T120000Z")
}

/// Build NDJSON (at `locale`) and a SQLite DB (with the transitive-closure
/// table) from the fixture into a fresh tempdir. Returns the guard plus paths.
fn build(locale: &str) -> (tempfile::TempDir, PathBuf, PathBuf) {
    let dir = tempfile::tempdir().unwrap();
    let ndjson = dir.path().join("syn.ndjson");
    let db = dir.path().join("syn.db");

    ndjson::run(ndjson::Args {
        rf2_dirs: vec![fixture_dir()],
        locale: locale.to_string(),
        output: Some(ndjson.clone()),
        include_inactive: false,
        refsets: RefsetMode::Simple,
    })
    .unwrap();

    sqlite::run(sqlite::Args {
        input: ndjson.clone(),
        output: db.clone(),
        transitive_closure: true,
        include_self: false,
    })
    .unwrap();

    (dir, ndjson, db)
}

fn read_records(path: &Path) -> Vec<ConceptRecord> {
    std::fs::read_to_string(path)
        .unwrap()
        .lines()
        .filter(|l| !l.trim().is_empty() && !l.contains("sct_provenance"))
        .map(|l| serde_json::from_str(l).unwrap())
        .collect()
}

fn record<'a>(records: &'a [ConceptRecord], id: &str) -> &'a ConceptRecord {
    records.iter().find(|r| r.id == id).expect("record present")
}

/// Evaluate ECL and return the matching SCTIDs, sorted for comparison.
fn ecl(db: &Path, expr: &str) -> Vec<String> {
    let mut v = ecl::expand_path(db, expr).unwrap();
    v.sort();
    v
}

#[test]
fn pipeline_filters_inactive_concepts() {
    let (_d, ndjson, _db) = build("en-GB");
    let records = read_records(&ndjson);
    // 23 concepts in the fixture, 1 inactive (9468002) -> 22 active records.
    assert_eq!(records.len(), 22);
    assert!(records.iter().all(|r| r.id != "9468002"));
}

#[test]
fn dialect_preferred_terms() {
    let (_g, gb_ndjson, _gdb) = build("en-GB");
    assert_eq!(
        record(&read_records(&gb_ndjson), "80146002").preferred_term,
        "Appendicectomy"
    );

    let (_u, us_ndjson, _udb) = build("en-US");
    assert_eq!(
        record(&read_records(&us_ndjson), "80146002").preferred_term,
        "Appendectomy"
    );
}

#[test]
fn builder_record_shape() {
    let (_d, ndjson, _db) = build("en-GB");
    let records = read_records(&ndjson);

    let mi = record(&records, "22298006");
    assert_eq!(mi.preferred_term, "Myocardial infarction");
    assert_eq!(mi.hierarchy, "Clinical finding");
    assert!(mi.synonyms.contains(&"Heart attack".to_string()));
    assert!(mi.ctv3_codes.contains(&"X200".to_string()));

    // Typed relationships preserved with type SCTID + group (for ECL).
    let has = |t: &str, d: &str| {
        mi.relationships
            .iter()
            .any(|r| r.type_id == t && r.destination_id == d && r.group == 1)
    };
    assert!(has("363698007", "74281007"), "finding site");
    assert!(has("116676008", "55641003"), "associated morphology");

    // Refset membership surfaced on the member records.
    assert!(record(&records, "46635009")
        .refsets
        .contains(&EXAMPLE_REFSET.to_string()));
}

#[test]
fn ecl_hierarchy_boolean_and_refset() {
    let (_d, _ndjson, db) = build("en-GB");

    assert_eq!(ecl(&db, "<<73211009"), ["44054006", "46635009", "73211009"]);
    assert_eq!(ecl(&db, "<73211009"), ["44054006", "46635009"]);
    assert_eq!(
        ecl(&db, ">>46635009"),
        ["138875005", "404684003", "46635009", "73211009"]
    );
    assert_eq!(ecl(&db, "<!73211009"), ["44054006", "46635009"]);
    assert_eq!(ecl(&db, "^991381000000107"), ["44054006", "46635009"]);
    assert_eq!(
        ecl(&db, "<<73211009 MINUS 46635009"),
        ["44054006", "73211009"]
    );
    assert_eq!(ecl(&db, "195967001 OR 22298006"), ["195967001", "22298006"]);
}

#[test]
fn ecl_attribute_refinement() {
    let (_d, _ndjson, db) = build("en-GB");
    // Clinical findings whose finding site is (a descendant-or-self of) Myocardium.
    assert_eq!(
        ecl(&db, "<<404684003 : 363698007 = <<74281007"),
        ["22298006"]
    );
    // Wrong value -> no match.
    assert!(ecl(&db, "<<404684003 : 363698007 = 55641003").is_empty());
}

#[test]
fn sqlite_fts5_maps_relationships_and_tct() {
    let (_d, _ndjson, db) = build("en-GB");
    let conn = Connection::open(&db).unwrap();

    // FTS5 keyword search: "diabetes" hits the parent + both subtypes.
    let mut stmt = conn
        .prepare(
            "SELECT c.id FROM concepts_fts JOIN concepts c ON concepts_fts.rowid = c.rowid
             WHERE concepts_fts MATCH '\"diabetes\"' ORDER BY c.id",
        )
        .unwrap();
    let mut ids: Vec<String> = stmt
        .query_map([], |r| r.get::<_, String>(0))
        .unwrap()
        .map(|r| r.unwrap())
        .collect();
    ids.sort();
    assert_eq!(ids, ["44054006", "46635009", "73211009"]);

    // CTV3 reverse map.
    let mapped: String = conn
        .query_row(
            "SELECT concept_id FROM concept_maps WHERE code = 'X200' AND terminology = 'ctv3'",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(mapped, "22298006");

    // Typed relationships table (schema v4): MI has its 2 attributes.
    let rel_count: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM concept_relationships WHERE source_id = '22298006'",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(rel_count, 2);

    // Transitive closure: Type 1 DM has 3 ancestors (DM, Clinical finding, root).
    let anc_count: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM concept_ancestors WHERE descendant_id = '46635009'",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(anc_count, 3);

    // Refset membership.
    let mut stmt = conn
        .prepare("SELECT referenced_component_id FROM refset_members WHERE refset_id = ?1 ORDER BY referenced_component_id")
        .unwrap();
    let members: Vec<String> = stmt
        .query_map([EXAMPLE_REFSET], |r| r.get::<_, String>(0))
        .unwrap()
        .map(|r| r.unwrap())
        .collect();
    assert_eq!(members, ["44054006", "46635009"]);
}
