//! Canonical SMILES robustness tests — addressing RDKit issue #8775.
//!
//! RDKit #8775 documents 115+ SMILES that produce oscillating or incorrect
//! canonical forms. After recent RDKit fixes, 22 remain problematic. This suite
//! verifies chematic's behavior on these and related cases.
//!
//! Three test categories:
//! 1. **Stability**: `parse → canonical → parse → canonical` gives the same string.
//!    All cases are expected to pass.
//! 2. **Platform independence (topology)**: two SMILES of the same molecule (no stereo)
//!    must give identical canonical forms. All expected to pass.
//! 3. **Stereo parity**: same molecule written with different atom ordering must produce
//!    the same canonical SMILES. Implemented via parse-time stereo neighbor recording and
//!    permutation-parity correction in canonical.rs (v0.2.7+).

use chematic_smiles::{canonical_smiles, parse};

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

/// Returns Ok if the canonical SMILES for `smi` is roundtrip-stable, Err otherwise.
fn check_canonical_stable(smi: &str) -> Result<(), String> {
    let mol1 = parse(smi).map_err(|e| format!("PARSE_FAIL '{}': {}", smi, e))?;
    let c1 = canonical_smiles(&mol1);
    if c1.is_empty() {
        return Err(format!("EMPTY_CANONICAL '{}'", smi));
    }
    let mol2 =
        parse(&c1).map_err(|e| format!("RE_PARSE_FAIL '{}' (canonical='{}'): {}", smi, c1, e))?;
    let c2 = canonical_smiles(&mol2);
    if c1 != c2 {
        return Err(format!("UNSTABLE '{}': '{}' → '{}'", smi, c1, c2));
    }
    Ok(())
}

/// Returns Ok if `a` and `b` (same molecule, different SMILES) produce the same canonical form.
fn check_same_canonical(a: &str, b: &str) -> Result<(), String> {
    let mol_a = parse(a).map_err(|e| format!("PARSE_FAIL '{}': {}", a, e))?;
    let mol_b = parse(b).map_err(|e| format!("PARSE_FAIL '{}': {}", b, e))?;
    let ca = canonical_smiles(&mol_a);
    let cb = canonical_smiles(&mol_b);
    if ca != cb {
        Err(format!("DIFFERENT '{}' vs '{}': '{}' ≠ '{}'", a, b, ca, cb))
    } else {
        Ok(())
    }
}

/// Assert all SMILES in `cases` are roundtrip-stable, collecting all failures before panicking.
/// `#[track_caller]` ensures the panic points at the calling test, not this helper.
#[track_caller]
fn assert_all_stable(cases: &[&str]) {
    let failures: Vec<_> = cases
        .iter()
        .filter_map(|s| check_canonical_stable(s).err())
        .collect();
    assert!(
        failures.is_empty(),
        "stability failures:\n{}",
        failures.join("\n")
    );
}

// ── Test 1: Roundtrip stability ──────────────────────────────────────────────

/// Every SMILES here must be roundtrip-stable (parse → canonical → parse → canonical
/// gives the same canonical string). A failure here is a genuine correctness regression.
#[test]
fn stability_bridged_bicyclics() {
    let cases = [
        "C1CC2CCC1CC2",         // bicyclo[2.2.2]octane
        "C1CC2CCCC2C1",         // bicyclo[3.2.1]octane variant
        "C1CCC2CC3CCCCC3CC2C1", // polycycle
    ];
    assert_all_stable(&cases);
}

#[test]
fn stability_spiro() {
    let cases = [
        "C1CCC2(CC1)CCCC2", // spiro[4.5]decane
        "C1CC2(CCC1)CCC2",  // spiro[4.4]nonane
    ];
    assert_all_stable(&cases);
}

#[test]
fn stability_ring_stereocenters() {
    let cases = [
        "[C@@H]1(N)CCCC1",          // (R)-aminocyclopentane
        "[C@H]1(N)CCCC1",           // (S)-aminocyclopentane
        "[C@H]1([C@@H](O)CO)CCCO1", // bicyclic-ish stereocenters
    ];
    assert_all_stable(&cases);
}

#[test]
fn stability_fused_ring_stereo() {
    let cases = [
        "[C@@H]1(CC[C@H]2CCCC[C@@H]12)O", // trans-decalin-OH
        "[C@H]1(CC[C@H]2CCCC[C@@H]12)O",  // cis-decalin-OH
    ];
    assert_all_stable(&cases);
}

#[test]
fn stability_ez_bonds() {
    let cases = [
        "C/C=C/C",  // trans-but-2-ene
        "C/C=C\\C", // cis-but-2-ene
        "F/C=C/Cl", // E-1-chloro-2-fluoroethylene
    ];
    assert_all_stable(&cases);
}

#[test]
fn stability_complex_sugars() {
    let cases = [
        "OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O", // D-glucose
    ];
    assert_all_stable(&cases);
}

#[test]
fn stability_fused_aromatics() {
    let cases = [
        "C1=CC2=CC=CC=C2C=C1", // azulene
        "c1ccc2ccccc2c1",      // naphthalene
        "c1ccc2[nH]ccc2c1",    // indole
    ];
    assert_all_stable(&cases);
}

#[test]
fn stability_amino_acids() {
    // Each of these as a single SMILES string (not compared to another form) must be stable.
    let cases = [
        "N[C@@H](C)C(=O)O",         // L-alanine
        "N[C@H](C)C(=O)O",          // D-alanine
        "N[C@@H](Cc1ccccc1)C(=O)O", // L-phenylalanine
    ];
    assert_all_stable(&cases);
}

// ── Test 2: Platform independence (topology only) ────────────────────────────

/// Two different SMILES for the SAME molecule (no stereocenters) must give identical
/// canonical forms. If either fails to parse, that is reported without panicking the suite.
#[test]
fn platform_independence_topology() {
    let pairs: &[(&str, &str)] = &[
        ("c1ccccc1", "c1cccc(c1)"),             // benzene
        ("c1ccncc1", "n1cccc(c1)"),             // pyridine
        ("C1CCNCC1", "N1CCCCC1"),               // piperidine
        ("C1CCC2(CC1)CCCC2", "C1CCCCC12CCCC2"), // spiro
    ];
    let failures: Vec<_> = pairs
        .iter()
        .filter_map(|(a, b)| check_same_canonical(a, b).err())
        .collect();
    assert!(
        failures.is_empty(),
        "platform-independence failures:\n{}",
        failures.join("\n")
    );
}

/// DOCUMENTED GAP: bridged bicyclics with multiple valid ring-closure orderings
/// can produce different canonical SMILES depending on which ring atoms are traversed
/// first. This is a known limitation of the current canonical traversal algorithm.
/// The test documents and tracks this without panicking.
#[test]
fn bridged_bicyclic_canonical_gap_documentation() {
    let cases: &[(&str, &str, &str)] = &[("C1CC2CCC1CC2", "C1CCC2CC1CC2", "bicyclo[2.2.2]octane")];
    for &(a, b, label) in cases {
        match check_same_canonical(a, b) {
            Err(e) => eprintln!("ℹ KNOWN GAP ({}): {}", label, e),
            Ok(()) => eprintln!("ℹ RESOLVED ({}): now gives same canonical SMILES", label),
        }
    }
    // Non-panicking: documents the gap for future tracking.
}

// ── Test 3: Stereo parity gap (documentation, does not panic) ────────────────

/// DOCUMENTED GAP: When the same molecule is written with two different atom orderings,
/// the canonical writer in `canonical.rs` does not apply a parity correction for the
/// neighbor permutation at stereocenters. Two SMILES encoding the same stereochemistry
/// but with different input atom traversal orders may produce different canonical forms.
///
/// Stereo parity is now corrected in canonical.rs.  These pairs must converge.
#[test]
fn stereo_parity_gap_documentation() {
    let pairs: &[(&str, &str, &str)] = &[
        // L-alanine written from N vs from C — odd permutation, parity must flip
        ("N[C@@H](C)C(=O)O", "C[C@H](N)C(=O)O", "L-alanine"),
        // Aminocyclopentane: ring-first vs NH2-first
        ("[C@@H]1(N)CCCC1", "[C@H]1(CCCC1)N", "aminocyclopentane"),
        // Ring stereocenter: exercises PendingRing path.
        // [C@H]1(F)CCCCC1 and F[C@H]1CCCCC1 are the same enantiomer:
        //   both encode [H/F/C6/C2] negative via an even permutation.
        ("F[C@H]1CCCCC1", "[C@H]1(F)CCCCC1", "fluorocyclohexane"),
    ];

    let failures: Vec<String> = pairs
        .iter()
        .filter_map(|&(a, b, label)| {
            check_same_canonical(a, b)
                .err()
                .map(|e| format!("{label}: {e}"))
        })
        .collect();

    assert!(
        failures.is_empty(),
        "stereo parity gap not fully resolved:\n{}",
        failures.join("\n")
    );
}

/// Charged aromatics may or may not parse depending on aromatic valence model.
/// Report results without hard-failing on parse errors (feature, not bug).
#[test]
fn charged_aromatics_parse_and_stable() {
    let cases = [
        "c1cc[n+](C)cc1", // N-methylpyridinium
        "c1cc[nH]cc1",    // should not parse (wrong valence) — verify graceful error
    ];
    for smi in &cases {
        match check_canonical_stable(smi) {
            Ok(()) => eprintln!("ℹ PASS (parsed+stable): {}", smi),
            Err(e) => eprintln!("ℹ SKIP/FAIL: {}", e),
        }
    }
    // Non-panicking: charged aromatic handling is an informational probe.
}
