#![cfg(feature = "compiled-tests")]

use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
use oxigraph::model::{BlankNode, NamedNode, NamedOrBlankNode, Term, Triple};
use std::error::Error;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

fn workspace_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("lib crate should have a parent workspace")
        .to_path_buf()
}

fn shared_target_dir() -> PathBuf {
    workspace_root()
        .join("target")
        .join("compiler-parity-tests")
}

fn unique_temp_dir() -> Result<PathBuf, Box<dyn Error>> {
    let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
    let dir = std::env::temp_dir().join(format!("shifty_compiler_parity_{}", nanos));
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

fn compiled_test_lock() -> MutexGuard<'static, ()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

fn write_file(path: &Path, contents: &str) -> Result<(), Box<dyn Error>> {
    let mut file = fs::File::create(path)?;
    file.write_all(contents.as_bytes())?;
    Ok(())
}

fn assert_report_contains(path: &Path, needle: &str) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(path)?;
    assert!(
        contents.contains(needle),
        "report {} does not contain expected text: {}",
        path.display(),
        needle
    );
    Ok(())
}

fn run_command(cmd: &mut Command) -> Result<String, Box<dyn Error>> {
    let output = cmd.output()?;
    if !output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!(
            "command {:?} failed (status {:?})\nstdout:\n{}\nstderr:\n{}",
            cmd, output.status, stdout, stderr
        )
        .into());
    }
    Ok(String::from_utf8_lossy(&output.stdout).to_string())
}

fn run_cli_to_file(args: &[&str], output_path: &Path) -> Result<(), Box<dyn Error>> {
    let mut child = Command::new("cargo")
        .args(["run", "-p", "cli", "--features", "srcgen-compiler", "--"])
        .args(args)
        .env("CARGO_TARGET_DIR", shared_target_dir())
        .stdout(Stdio::from(fs::File::create(output_path)?))
        .spawn()?;
    let status = child.wait()?;
    if !status.success() {
        return Err(format!("cargo run cli {:?} failed with {}", args, status).into());
    }
    Ok(())
}

fn run_binary_to_file(
    binary_path: &Path,
    args: &[&str],
    output_path: &Path,
) -> Result<(), Box<dyn Error>> {
    let mut child = Command::new(binary_path)
        .args(args)
        .stdout(Stdio::from(fs::File::create(output_path)?))
        .spawn()?;
    let status = child.wait()?;
    if !status.success() {
        return Err(format!("binary {:?} {:?} failed with {}", binary_path, args, status).into());
    }
    Ok(())
}

fn run_command_capture(mut cmd: Command) -> Result<std::process::Output, Box<dyn Error>> {
    let output = cmd.output()?;
    Ok(output)
}

fn run_status_success(mut cmd: Command, context: &str) -> Result<(), Box<dyn Error>> {
    let status = cmd.status()?;
    if !status.success() {
        return Err(format!("{context} failed with {status}").into());
    }
    Ok(())
}

fn unique_temp_file(prefix: &str, suffix: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!("{prefix}-{}-{nanos}{suffix}", std::process::id()))
}

fn parse_max_rss_kib(path: &Path) -> Option<u64> {
    let raw = fs::read_to_string(path).ok()?;
    raw.trim().parse::<u64>().ok()
}

fn run_runtime_validate_with_metrics(
    runtime_cli: &Path,
    shapes: &str,
    data: &str,
) -> Result<(Duration, Option<u64>), Box<dyn Error>> {
    let start = Instant::now();
    let time_bin = Path::new("/usr/bin/time");
    let mut rss_kib = None;
    if time_bin.exists() {
        let rss_file = unique_temp_file("shifty-runtime-rss", ".txt");
        let mut cmd = Command::new(time_bin);
        cmd.args(["-f", "%M", "-o"])
            .arg(&rss_file)
            .arg("--")
            .arg(runtime_cli)
            .args([
                "validate",
                "--shapes-file",
                shapes,
                "--data-file",
                data,
                "--run-inference",
                "--format",
                "turtle",
            ])
            .stdout(Stdio::null())
            .stderr(Stdio::null());
        run_status_success(cmd, "runtime validation")?;
        rss_kib = parse_max_rss_kib(&rss_file);
        let _ = fs::remove_file(rss_file);
    } else {
        let mut cmd = Command::new(runtime_cli);
        cmd.args([
            "validate",
            "--shapes-file",
            shapes,
            "--data-file",
            data,
            "--run-inference",
            "--format",
            "turtle",
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null());
        run_status_success(cmd, "runtime validation")?;
    }
    Ok((start.elapsed(), rss_kib))
}

fn run_compiled_validate_with_metrics(
    compiled_bin: &Path,
    data: &str,
) -> Result<(Duration, Option<u64>), Box<dyn Error>> {
    let start = Instant::now();
    let time_bin = Path::new("/usr/bin/time");
    let mut rss_kib = None;
    if time_bin.exists() {
        let rss_file = unique_temp_file("shifty-compiled-rss", ".txt");
        let mut cmd = Command::new(time_bin);
        cmd.args(["-f", "%M", "-o"])
            .arg(&rss_file)
            .arg("--")
            .arg(compiled_bin)
            .args(["--run-inference=true", "--full-aot=true", data])
            .env("SHFTY_SRCGEN_FULL_AOT_STRICT", "1")
            .stdout(Stdio::null())
            .stderr(Stdio::null());
        run_status_success(cmd, "compiled validation")?;
        rss_kib = parse_max_rss_kib(&rss_file);
        let _ = fs::remove_file(rss_file);
    } else {
        let mut cmd = Command::new(compiled_bin);
        cmd.args(["--run-inference=true", "--full-aot=true", data])
            .env("SHFTY_SRCGEN_FULL_AOT_STRICT", "1")
            .stdout(Stdio::null())
            .stderr(Stdio::null());
        run_status_success(cmd, "compiled validation")?;
    }
    Ok((start.elapsed(), rss_kib))
}

fn is_skolem_iri(iri: &str) -> bool {
    iri.contains("/.sk/")
}

fn deskolemized_blank(
    iri: &str,
    bnodes: &mut std::collections::HashMap<String, BlankNode>,
) -> BlankNode {
    if let Some(existing) = bnodes.get(iri) {
        return existing.clone();
    }
    let id = format!("desk{}", bnodes.len() + 1);
    let bnode = BlankNode::new(id).expect("generated blank node id should be valid");
    bnodes.insert(iri.to_string(), bnode.clone());
    bnode
}

fn deskolemize_subject(
    subject: NamedOrBlankNode,
    bnodes: &mut std::collections::HashMap<String, BlankNode>,
) -> NamedOrBlankNode {
    match subject {
        NamedOrBlankNode::NamedNode(node) if is_skolem_iri(node.as_str()) => {
            NamedOrBlankNode::BlankNode(deskolemized_blank(node.as_str(), bnodes))
        }
        other => other,
    }
}

fn deskolemize_term(term: Term, bnodes: &mut std::collections::HashMap<String, BlankNode>) -> Term {
    match term {
        Term::NamedNode(node) if is_skolem_iri(node.as_str()) => {
            Term::BlankNode(deskolemized_blank(node.as_str(), bnodes))
        }
        other => other,
    }
}

fn normalize_report_for_comparison(path: &Path) -> Result<PathBuf, Box<dyn Error>> {
    let sh_result_message =
        NamedNode::new("http://www.w3.org/ns/shacl#resultMessage").expect("valid SHACL IRI");
    let parent = path.parent().unwrap_or_else(|| Path::new("."));
    let stem = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("report");
    let normalized_path = parent.join(format!("{stem}.deskolemized.ttl"));

    let file = fs::File::open(path)?;
    let parser = RdfParser::from_format(RdfFormat::Turtle).without_named_graphs();
    let mut bnodes: std::collections::HashMap<String, BlankNode> = std::collections::HashMap::new();
    let mut triples: Vec<Triple> = Vec::new();
    for quad in parser.for_reader(file) {
        let triple: Triple = quad?.into();
        if triple.predicate == sh_result_message {
            continue;
        }
        let subject = deskolemize_subject(triple.subject.to_owned(), &mut bnodes);
        let predicate: NamedNode = triple.predicate.to_owned();
        let object = deskolemize_term(triple.object.to_owned(), &mut bnodes);
        triples.push(Triple::new(subject, predicate, object));
    }

    let mut output = Vec::new();
    let mut serializer = RdfSerializer::from_format(RdfFormat::Turtle).for_writer(&mut output);
    for triple in triples {
        serializer.serialize_triple(triple.as_ref())?;
    }
    serializer.finish()?;
    fs::write(&normalized_path, output)?;
    Ok(normalized_path)
}

fn assert_reports_isomorphic(left: &Path, right: &Path) -> Result<(), Box<dyn Error>> {
    let left_norm = normalize_report_for_comparison(left)?;
    let right_norm = normalize_report_for_comparison(right)?;
    let iso_output = run_command(
        Command::new("cargo")
            .args(["run", "-p", "shifty-shacl", "--bin", "isomorphic", "--"])
            .env("CARGO_TARGET_DIR", shared_target_dir())
            .arg(left_norm.to_str().unwrap())
            .arg(right_norm.to_str().unwrap()),
    )?;

    assert!(
        iso_output.contains("isomorphic: true"),
        "reports are not isomorphic:\n{}",
        iso_output
    );
    Ok(())
}

fn compile_srcgen_binary(
    shapes: &Path,
    out_dir: &Path,
    bin_name: &str,
) -> Result<(), Box<dyn Error>> {
    let shifty_path = workspace_root().join("lib");
    let compile_stdout = out_dir
        .parent()
        .unwrap_or_else(|| Path::new("."))
        .join(format!("compile-{bin_name}.stdout"));
    let args = vec![
        "compile",
        "--shapes-file",
        shapes.to_str().unwrap(),
        "--backend",
        "specialized",
        "--out-dir",
        out_dir.to_str().unwrap(),
        "--bin-name",
        bin_name,
        "--shifty-path",
        shifty_path.to_str().unwrap(),
    ];
    run_cli_to_file(&args, &compile_stdout)
}

#[test]
fn compiled_variants_and_runtime_reports_are_isomorphic() -> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:PersonShape
  a sh:NodeShape ;
  sh:targetClass ex:Person ;
  sh:class ex:Person ;
  sh:property [
    sh:path ex:age ;
    sh:datatype xsd:integer ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
  ] ;
  sh:property [
    sh:path ex:nickname ;
    sh:nodeKind sh:Literal ;
    sh:minLength 3 ;
    sh:maxLength 8 ;
    sh:pattern "^[A-Z]+$" ;
  ] .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/ns#> .

ex:Alice a ex:Person ;
  ex:age "not-an-integer" ;
  ex:nickname "ab" .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(&shapes, &compiled_primary_out, "compiled-primary-bin-core")?;
    compile_srcgen_binary(
        &shapes,
        &compiled_secondary_out,
        "compiled-secondary-bin-core",
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-core"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-secondary-bin-core"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    assert_reports_isomorphic(&compiled_primary_report, &compiled_secondary_report)?;
    assert_reports_isomorphic(&compiled_primary_report, &runtime_report)?;
    assert_reports_isomorphic(&compiled_secondary_report, &runtime_report)?;

    Ok(())
}

#[test]
fn compiled_variants_and_runtime_reports_are_isomorphic_for_phase2_expanded_constraints()
-> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .

ex:DeviceShape
  a sh:NodeShape ;
  sh:targetClass ex:Device ;
  sh:property [
    sh:path ex:reading ;
    sh:minInclusive 10 ;
    sh:maxExclusive 20 ;
    sh:hasValue 15 ;
    sh:in (15 16) ;
  ] ;
  sh:property [
    sh:path ex:label ;
    sh:languageIn ("en" "fr") ;
    sh:uniqueLang true ;
  ] ;
  sh:property [
    sh:path ex:code ;
    sh:equals ex:mirrorCode ;
    sh:disjoint ex:bannedCode ;
    sh:lessThan ex:maxCode ;
    sh:lessThanOrEquals ex:maxCodeInclusive ;
  ] .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/ns#> .

ex:d1 a ex:Device ;
  ex:reading 9, 21 ;
  ex:label "hello"@en, "bonjour"@EN ;
  ex:code 5, 8 ;
  ex:mirrorCode 8 ;
  ex:bannedCode 8 ;
  ex:maxCode 7 ;
  ex:maxCodeInclusive 8 .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(
        &shapes,
        &compiled_primary_out,
        "compiled-primary-bin-expanded",
    )?;
    compile_srcgen_binary(&shapes, &compiled_secondary_out, "srcgen-bin-expanded")?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-expanded"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("srcgen-bin-expanded"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    assert_reports_isomorphic(&compiled_primary_report, &compiled_secondary_report)?;
    assert_reports_isomorphic(&compiled_primary_report, &runtime_report)?;
    assert_reports_isomorphic(&compiled_secondary_report, &runtime_report)?;

    Ok(())
}

#[test]
fn compiled_variants_and_runtime_reports_are_isomorphic_for_logical_node_constraints()
-> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .

ex:ChildShape
  a sh:NodeShape ;
  sh:targetClass ex:Child ;
  sh:property [ sh:path ex:age ; sh:minCount 1 ] .

ex:AltChildShape
  a sh:NodeShape ;
  sh:targetClass ex:AltChild ;
  sh:property [ sh:path ex:age ; sh:minCount 1 ] .

ex:ParentShape
  a sh:NodeShape ;
  sh:targetClass ex:Parent ;
  sh:property [
    sh:path ex:child ;
    sh:node ex:ChildShape ;
    sh:not ex:AltChildShape ;
    sh:and (ex:ChildShape) ;
    sh:or (ex:ChildShape ex:AltChildShape) ;
    sh:xone (ex:ChildShape ex:AltChildShape) ;
  ] .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/ns#> .

ex:p1 a ex:Parent ;
  ex:child ex:c1, ex:c2 .

ex:c1 a ex:Child ;
  ex:age 10 .

ex:c2 a ex:Child, ex:AltChild ;
  ex:age 5 .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(
        &shapes,
        &compiled_primary_out,
        "compiled-primary-bin-logical-node",
    )?;
    compile_srcgen_binary(&shapes, &compiled_secondary_out, "srcgen-bin-logical-node")?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-logical-node"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("srcgen-bin-logical-node"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    assert_reports_isomorphic(&compiled_primary_report, &compiled_secondary_report)?;
    assert_reports_isomorphic(&compiled_primary_report, &runtime_report)?;
    assert_reports_isomorphic(&compiled_secondary_report, &runtime_report)?;

    Ok(())
}

#[test]
fn compiled_variants_and_runtime_reports_are_isomorphic_for_closed_and_qualified_constraints()
-> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .

ex:FingerShape
  a sh:NodeShape ;
  sh:class ex:Finger .

ex:ThumbShape
  a sh:NodeShape ;
  sh:class ex:Thumb .

ex:HandShape
  a sh:NodeShape ;
  sh:targetClass ex:Hand ;
  sh:closed true ;
  sh:ignoredProperties (rdf:type) ;
  sh:property [
    sh:path ex:digit ;
    sh:qualifiedMinCount 1 ;
    sh:qualifiedValueShape ex:ThumbShape ;
    sh:qualifiedValueShapesDisjoint true ;
  ] ;
  sh:property [
    sh:path ex:digit ;
    sh:qualifiedMaxCount 4 ;
    sh:qualifiedValueShape ex:FingerShape ;
    sh:qualifiedValueShapesDisjoint true ;
  ] .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/ns#> .

ex:badHand a ex:Hand ;
  ex:digit ex:fingerThumb ;
  ex:illegal ex:oops .

ex:fingerThumb a ex:Finger, ex:Thumb .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(
        &shapes,
        &compiled_primary_out,
        "compiled-primary-bin-closed-qualified",
    )?;
    compile_srcgen_binary(
        &shapes,
        &compiled_secondary_out,
        "srcgen-bin-closed-qualified",
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-closed-qualified"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("srcgen-bin-closed-qualified"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    assert_reports_isomorphic(&compiled_primary_report, &compiled_secondary_report)?;
    assert_reports_isomorphic(&compiled_primary_report, &runtime_report)?;
    assert_reports_isomorphic(&compiled_secondary_report, &runtime_report)?;

    Ok(())
}

#[test]
fn compiled_variants_and_runtime_reports_are_isomorphic_for_node_datatype_and_membership_constraints()
-> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:ItemShape
  a sh:NodeShape ;
  sh:targetClass ex:Item ;
  sh:datatype xsd:string ;
  sh:hasValue ex:requiredItem ;
  sh:in (ex:requiredItem ex:alternateItem) .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/ns#> .

ex:item1 a ex:Item .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(
        &shapes,
        &compiled_primary_out,
        "compiled-primary-bin-node-membership",
    )?;
    compile_srcgen_binary(
        &shapes,
        &compiled_secondary_out,
        "srcgen-bin-node-membership",
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-node-membership"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("srcgen-bin-node-membership"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    assert_reports_isomorphic(&compiled_primary_report, &compiled_secondary_report)?;
    assert_reports_isomorphic(&compiled_primary_report, &runtime_report)?;
    assert_reports_isomorphic(&compiled_secondary_report, &runtime_report)?;

    Ok(())
}

#[test]
fn compiled_variants_and_runtime_reports_are_isomorphic_with_inference()
-> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .

ex:PersonShape
  a sh:NodeShape ;
  sh:targetClass ex:Person ;
  sh:property [
    sh:path ex:flag ;
    sh:minCount 1 ;
  ] ;
  sh:rule [
    a sh:TripleRule ;
    sh:subject sh:this ;
    sh:predicate ex:flag ;
    sh:object "true" ;
  ] .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/ns#> .

ex:Alice a ex:Person .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(&shapes, &compiled_primary_out, "compiled-primary-bin-infer")?;
    compile_srcgen_binary(&shapes, &compiled_secondary_out, "srcgen-bin-infer")?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-infer"),
        &["--run-inference=true", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir().join("debug").join("srcgen-bin-infer"),
        &["--run-inference=true", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--run-inference",
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    assert_reports_isomorphic(&compiled_primary_report, &compiled_secondary_report)?;
    assert_reports_isomorphic(&compiled_primary_report, &runtime_report)?;
    assert_reports_isomorphic(&compiled_secondary_report, &runtime_report)?;

    Ok(())
}

#[test]
fn compiled_variants_and_runtime_reports_are_isomorphic_with_shape_data_union()
-> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    // ex:refValue only has rdf:type in the shapes graph. Correct validation requires
    // querying the union of shapes+data graphs for sh:class checks.
    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .

ex:refValue a ex:ApprovedThing .

ex:PersonShape
  a sh:NodeShape ;
  sh:targetClass ex:Person ;
  sh:property [
    sh:path ex:linked ;
    sh:minCount 1 ;
    sh:class ex:ApprovedThing ;
  ] .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/ns#> .

ex:Alice a ex:Person ;
  ex:linked ex:refValue .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(&shapes, &compiled_primary_out, "compiled-primary-bin-union")?;
    compile_srcgen_binary(&shapes, &compiled_secondary_out, "srcgen-bin-union")?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-union"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir().join("debug").join("srcgen-bin-union"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    assert_reports_isomorphic(&compiled_primary_report, &compiled_secondary_report)?;
    assert_reports_isomorphic(&compiled_primary_report, &runtime_report)?;
    assert_reports_isomorphic(&compiled_secondary_report, &runtime_report)?;

    Ok(())
}

#[test]
fn compiled_variants_and_runtime_detect_domain_sparql_property_violation()
-> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_primary_out = tmp.join("compiled-primary");
    let compiled_secondary_out = tmp.join("compiled-secondary");
    let compiled_primary_report = tmp.join("compiled-primary-report.ttl");
    let compiled_secondary_report = tmp.join("compiled-secondary-report.ttl");
    let runtime_report = tmp.join("runtime-report.ttl");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/hvac#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:Prefixes
  sh:declare [
    sh:prefix "ex" ;
    sh:namespace "http://example.com/hvac#"^^xsd:anyURI ;
  ] .

ex:HvacZoneShape
  a sh:NodeShape ;
  sh:targetClass ex:HvacZone ;
  sh:property [
    sh:path ex:airFlowRate ;
    sh:sparql [
      sh:prefixes ex:Prefixes ;
      sh:select """
        SELECT $this ?value ?path
        WHERE {
          $this $PATH ?value .
          FILTER(?value < 0)
          BIND($PATH AS ?path)
        }
      """ ;
    ] ;
  ] .
"#;

    let data_ttl = r#"@prefix ex: <http://example.com/hvac#> .

ex:zoneOk a ex:HvacZone ;
  ex:airFlowRate 5 .

ex:zoneBad a ex:HvacZone ;
  ex:airFlowRate -1 .
"#;

    write_file(&shapes, shapes_ttl)?;
    write_file(&data, data_ttl)?;

    compile_srcgen_binary(
        &shapes,
        &compiled_primary_out,
        "compiled-primary-bin-domain-sparql",
    )?;
    compile_srcgen_binary(&shapes, &compiled_secondary_out, "srcgen-bin-domain-sparql")?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("compiled-primary-bin-domain-sparql"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_primary_report,
    )?;

    run_binary_to_file(
        &shared_target_dir()
            .join("debug")
            .join("srcgen-bin-domain-sparql"),
        &["--run-inference=false", data.to_str().unwrap()],
        &compiled_secondary_report,
    )?;

    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &runtime_report,
    )?;

    for report in [
        &compiled_primary_report,
        &compiled_secondary_report,
        &runtime_report,
    ] {
        assert_report_contains(report, "sh:conforms false")?;
        assert_report_contains(report, "<http://example.com/hvac#zoneBad>")?;
        assert_report_contains(report, "<http://example.com/hvac#airFlowRate>")?;
        assert_report_contains(
            report,
            "sh:sourceConstraintComponent sh:SPARQLConstraintComponent",
        )?;
        assert_report_contains(report, "sh:value -1")?;
    }

    Ok(())
}

#[test]
#[ignore = "perf gate: run manually with --ignored"]
fn srcgen_compiled_perf_smoke_gate_against_runtime_validate() -> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let tmp = unique_temp_dir()?;
    let shapes = tmp.join("shapes.ttl");
    let data = tmp.join("data.ttl");
    let compiled_secondary_out = tmp.join("compiled-secondary");

    let shapes_ttl = r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:PersonShape
  a sh:NodeShape ;
  sh:targetClass ex:Person ;
  sh:property [
    sh:path ex:age ;
    sh:datatype xsd:integer ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
  ] ;
  sh:property [
    sh:path ex:nickname ;
    sh:nodeKind sh:Literal ;
    sh:minLength 3 ;
    sh:maxLength 8 ;
    sh:pattern "^[A-Z]+$" ;
  ] .
"#;
    write_file(&shapes, shapes_ttl)?;

    let mut data_ttl = String::from("@prefix ex: <http://example.com/ns#> .\n\n");
    for i in 0..400 {
        data_ttl.push_str(&format!(
            "ex:P{0} a ex:Person ; ex:age \"{0}\" ; ex:nickname \"ABC\" .\n",
            i
        ));
    }
    write_file(&data, &data_ttl)?;

    compile_srcgen_binary(&shapes, &compiled_secondary_out, "srcgen-bin-perf")?;

    let srcgen_bin = shared_target_dir().join("debug").join("srcgen-bin-perf");
    let mut srcgen_runs: Vec<Duration> = Vec::new();
    let mut runtime_runs: Vec<Duration> = Vec::new();

    // Warmup
    run_binary_to_file(
        &srcgen_bin,
        &["--run-inference=false", data.to_str().unwrap()],
        &tmp.join("warmup-srcgen.ttl"),
    )?;
    run_cli_to_file(
        &[
            "validate",
            "--shapes-file",
            shapes.to_str().unwrap(),
            "--data-file",
            data.to_str().unwrap(),
            "--format",
            "turtle",
        ],
        &tmp.join("warmup-runtime.ttl"),
    )?;

    for idx in 0..5 {
        let start = Instant::now();
        run_binary_to_file(
            &srcgen_bin,
            &["--run-inference=false", data.to_str().unwrap()],
            &tmp.join(format!("srcgen-{idx}.ttl")),
        )?;
        srcgen_runs.push(start.elapsed());

        let start = Instant::now();
        run_cli_to_file(
            &[
                "validate",
                "--shapes-file",
                shapes.to_str().unwrap(),
                "--data-file",
                data.to_str().unwrap(),
                "--format",
                "turtle",
            ],
            &tmp.join(format!("runtime-{idx}.ttl")),
        )?;
        runtime_runs.push(start.elapsed());
    }

    srcgen_runs.sort();
    runtime_runs.sort();
    let srcgen_median = srcgen_runs[srcgen_runs.len() / 2];
    let runtime_median = runtime_runs[runtime_runs.len() / 2];

    assert!(
        srcgen_median.as_secs_f64() <= runtime_median.as_secs_f64() * 1.25,
        "srcgen perf gate failed: srcgen_median={:?}, runtime_median={:?}",
        srcgen_median,
        runtime_median
    );

    Ok(())
}

#[test]
#[ignore = "strict aot gate: run manually with --ignored"]
fn srcgen_strict_full_aot_223_invalid_case_fails() -> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let root = workspace_root();
    let mut cmd = Command::new("bash");
    cmd.arg("223test.sh")
        .current_dir(&root)
        .env("CARGO_TARGET_DIR", shared_target_dir())
        .env("FORCE_REFRESH", "false")
        .env("TEMPORARY_ONTOENV", "true")
        .env("FULL_AOT", "true")
        .env("SHFTY_SRCGEN_FULL_AOT_STRICT", "1")
        .env("DATA_FILE", "ttl/medium-223p.ttl");
    let output = run_command_capture(cmd)?;

    assert_eq!(
        output.status.code(),
        Some(2),
        "expected exit code 2 for violations, got {:?}\nstdout:\n{}\nstderr:\n{}",
        output.status.code(),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("Validation failed (violations found)."),
        "expected strict 223 run to report violations:\n{}",
        stdout
    );
    Ok(())
}

#[test]
#[ignore = "perf gate: run manually with --ignored"]
fn srcgen_strict_full_aot_perf_gate_223_and_brick() -> Result<(), Box<dyn Error>> {
    let _guard = compiled_test_lock();
    let root = workspace_root();
    let target_dir = shared_target_dir();

    let build_cli_start = Instant::now();
    run_status_success(
        {
            let mut cmd = Command::new("cargo");
            cmd.args([
                "build",
                "--release",
                "-p",
                "cli",
                "--features",
                "srcgen-compiler",
            ])
            .current_dir(&root)
            .env("CARGO_TARGET_DIR", &target_dir)
            .env("CARGO_PROFILE_RELEASE_DEBUG", "0");
            cmd
        },
        "build release cli",
    )?;
    let runtime_cli_build = build_cli_start.elapsed();

    let runtime_cli = target_dir.join("release").join("shifty");
    if !runtime_cli.exists() {
        return Err(format!("runtime cli binary missing at {}", runtime_cli.display()).into());
    }

    let env_bin_223 = std::env::var_os("SHFTY_PERF_GATE_BIN_223")
        .map(PathBuf::from)
        .filter(|path| path.exists());
    let env_bin_brick = std::env::var_os("SHFTY_PERF_GATE_BIN_BRICK")
        .map(PathBuf::from)
        .filter(|path| path.exists());

    let compiled_223_out = std::env::temp_dir().join("compiled-shacl-223p-gate");
    let compiled_brick_out = std::env::temp_dir().join("compiled-shacl-brick-gate");
    fs::create_dir_all(&compiled_223_out)?;
    fs::create_dir_all(&compiled_brick_out)?;

    let (compiled_223_bin, compile_223_elapsed) = if let Some(path) = env_bin_223 {
        (path, Duration::from_secs(0))
    } else {
        let compile_start = Instant::now();
        run_status_success(
            {
                let mut cmd = Command::new("cargo");
                cmd.args(["run", "-p", "cli", "--features", "srcgen-compiler", "--"])
                    .args([
                        "compile",
                        "--shapes-file",
                        "ttl/223p.ttl",
                        "--backend",
                        "specialized",
                        "--out-dir",
                    ])
                    .arg(&compiled_223_out)
                    .args(["--bin-name", "s223gate", "--shifty-path"])
                    .arg(root.join("lib"))
                    .args(["--release", "--temporary"])
                    .current_dir(&root)
                    .env("CARGO_TARGET_DIR", &target_dir)
                    .env("CARGO_PROFILE_RELEASE_DEBUG", "0")
                    .env("FORCE_REFRESH", "false");
                cmd
            },
            "compile 223 srcgen binary",
        )?;
        (
            compiled_223_out.join("target/release/s223gate"),
            compile_start.elapsed(),
        )
    };

    let (compiled_brick_bin, compile_brick_elapsed) = if let Some(path) = env_bin_brick {
        (path, Duration::from_secs(0))
    } else {
        let compile_start = Instant::now();
        run_status_success(
            {
                let mut cmd = Command::new("cargo");
                cmd.args(["run", "-p", "cli", "--features", "srcgen-compiler", "--"])
                    .args([
                        "compile",
                        "--shapes-file",
                        "ttl/Brick.ttl",
                        "--backend",
                        "specialized",
                        "--out-dir",
                    ])
                    .arg(&compiled_brick_out)
                    .args(["--bin-name", "brickgate", "--shifty-path"])
                    .arg(root.join("lib"))
                    .args(["--release", "--temporary"])
                    .current_dir(&root)
                    .env("CARGO_TARGET_DIR", &target_dir)
                    .env("CARGO_PROFILE_RELEASE_DEBUG", "0")
                    .env("FORCE_REFRESH", "false");
                cmd
            },
            "compile brick srcgen binary",
        )?;
        (
            compiled_brick_out.join("target/release/brickgate"),
            compile_start.elapsed(),
        )
    };

    if !compiled_223_bin.exists() {
        return Err(format!(
            "compiled 223 binary missing at {}",
            compiled_223_bin.display()
        )
        .into());
    }
    if !compiled_brick_bin.exists() {
        return Err(format!(
            "compiled brick binary missing at {}",
            compiled_brick_bin.display()
        )
        .into());
    }

    eprintln!(
        "perf gate compile metrics: runtime_cli={runtime_cli_build:?} compiled_223={compile_223_elapsed:?} compiled_brick={compile_brick_elapsed:?}"
    );

    let bench_dataset = |name: &str,
                         shapes: &str,
                         data: &str,
                         compiled_bin: &Path|
     -> Result<(), Box<dyn Error>> {
        let mut runtime_runs = Vec::new();
        let mut compiled_runs = Vec::new();
        let mut runtime_rss_kib = Vec::new();
        let mut compiled_rss_kib = Vec::new();

        for _ in 0..3 {
            let (runtime_elapsed, runtime_rss) =
                run_runtime_validate_with_metrics(&runtime_cli, shapes, data)?;
            runtime_runs.push(runtime_elapsed);
            if let Some(rss) = runtime_rss {
                runtime_rss_kib.push(rss);
            }

            let (compiled_elapsed, compiled_rss) =
                run_compiled_validate_with_metrics(compiled_bin, data)?;
            compiled_runs.push(compiled_elapsed);
            if let Some(rss) = compiled_rss {
                compiled_rss_kib.push(rss);
            }
        }

        runtime_runs.sort();
        compiled_runs.sort();
        let runtime_median = runtime_runs[runtime_runs.len() / 2];
        let compiled_median = compiled_runs[compiled_runs.len() / 2];
        runtime_rss_kib.sort_unstable();
        compiled_rss_kib.sort_unstable();
        let runtime_rss_median = runtime_rss_kib
            .get(runtime_rss_kib.len().saturating_sub(1) / 2)
            .copied();
        let compiled_rss_median = compiled_rss_kib
            .get(compiled_rss_kib.len().saturating_sub(1) / 2)
            .copied();
        eprintln!(
            "perf gate dataset={name} runtime_median={runtime_median:?} compiled_median={compiled_median:?} runtime_rss_kib={runtime_rss_median:?} compiled_rss_kib={compiled_rss_median:?}"
        );
        assert!(
            compiled_median < runtime_median,
            "perf gate failed for {name}: compiled_median={compiled_median:?}, runtime_median={runtime_median:?}"
        );
        Ok(())
    };

    bench_dataset(
        "223",
        "ttl/223p.ttl",
        "ttl/medium-223p.ttl",
        &compiled_223_bin,
    )?;
    bench_dataset(
        "brick",
        "ttl/Brick.ttl",
        "ttl/small-brick-model.ttl",
        &compiled_brick_bin,
    )?;
    Ok(())
}
