//! Regression net for the #55 ParamMode={Own,Borrow} totality cutover
//! (commit `76b21c0`, spec 0062 / plan 0121) and its #58 follow-up
//! (`b129af1`, sub-binder borrow inheritance).
//!
//! Property protected: the post-#55-cutover ownership surface accepts the
//! legitimate authoring forms and rejects the unsound / over-strict ones,
//! AT THE RIGHT PIPELINE STAGE. Concretely, for each fixture under
//! examples/fieldtest/cut55_*.ail, `ail check` must:
//!   - exit 0 for the legitimate forms (read-only borrow, consume own,
//!     value-typed own, clone-to-return escape hatch, and the
//!     borrow-traverse-and-rebuild that must NOT trip the #58 check);
//!   - reject the unsound / over-strict forms with the EXACT diagnostic
//!     code, at the stage that owns the rule:
//!       * bare fn-type slot      -> `surface-parse-error` (parser stage),
//!       * borrow over value type -> `borrow-over-value`   (signature),
//!       * borrowed return        -> `borrow-return-not-permitted` (sig),
//!       * consume while borrowed -> `consume-while-borrowed` (linearity),
//!         and emit the `over-strict-mode` advisory (a warning, exit 0) on a
//!         true read-only `(own)` heap param.
//!
//! Coupling discipline: the assertions key on the bracketed diagnostic
//! CODE (e.g. `[consume-while-borrowed]`) appearing in stderr, never on
//! the surrounding message wording — that wording is slated to change
//! (the borrow-return and consume-while-borrowed friction tidy-ups noted
//! in the spec-0065 findings) and the regression net must survive it.
//! Each row carries the fixture stem so a failure names which authoring
//! form regressed.
//!
//! Without this test the twelve cut55_* fixtures are dead files that the
//! build never executes; a regression in the totality check (a relaxed
//! reject, a moved stage, a re-introduced default) would land silently.

use std::path::{Path, PathBuf};
use std::process::Command;

fn ail_bin() -> &'static str {
    env!("CARGO_BIN_EXE_ail")
}

fn fixture(stem: &str) -> PathBuf {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
    workspace
        .join("examples")
        .join("fieldtest")
        .join(format!("{stem}.ail"))
}

/// What `ail check` must do with a fixture. Codes name the bracketed
/// diagnostic tag, never the message prose.
enum Expect {
    /// exit 0, no warning expected (legitimate form).
    Accept,
    /// exit 0, but the `over-strict-mode` advisory warning must appear.
    AcceptWithWarning(&'static str),
    /// exit non-zero, and `[code]` must appear in stderr.
    Reject(&'static str),
}

fn check(stem: &str) -> (bool, String) {
    let out = Command::new(ail_bin())
        .args(["check", fixture(stem).to_str().unwrap()])
        .output()
        .expect("ail check failed to run");
    let stderr = String::from_utf8(out.stderr).expect("stderr utf8");
    (out.status.success(), stderr)
}

#[test]
fn cut55_cutover_surface_accepts_and_rejects_at_the_right_stage() {
    // (fixture stem, expectation). The stem is in every failure message
    // so a red row names the regressed authoring form.
    let table: &[(&str, Expect)] = &[
        // --- decision (a): borrow vs own on a heap value -----------------
        ("cut55_1_readonly_length", Expect::Accept),
        // over-strict own that does NOT trip the lint: the recursive call
        // consumes the heap tail into an own self-param, so the lint's
        // consume_count==0 premise fails. Still a sound (if strict)
        // annotation -> accepted, silent.
        ("cut55_1b_overstrict_own", Expect::Accept),
        // true read-only own: nothing consumed -> the advisory fires.
        (
            "cut55_1c_overstrict_isempty",
            Expect::AcceptWithWarning("over-strict-mode"),
        ),
        // --- decision (b): consume vs borrow -----------------------------
        ("cut55_2_consume_map", Expect::Accept),
        // false-positive guard for #58: borrow-traverse-and-rebuild, the
        // recursive param is (borrow) so the tail is never consumed.
        ("cut55_2b_borrow_traversal_clean", Expect::Accept),
        // #58 fix: tail sub-binder of a borrowed scrutinee moved into an
        // own sink -> reject at the linearity check.
        (
            "cut55_2c_consume_borrow_reject",
            Expect::Reject("consume-while-borrowed"),
        ),
        // whole borrowed param moved into an own sink -> reject.
        (
            "cut55_2d_consume_borrow_whole",
            Expect::Reject("consume-while-borrowed"),
        ),
        // --- decision (c): value-typed params ----------------------------
        ("cut55_3_value_param", Expect::Accept),
        // borrow over an unboxed value type -> reject at the signature.
        (
            "cut55_3b_borrow_over_value",
            Expect::Reject("borrow-over-value"),
        ),
        // --- decision (d): borrowed return -------------------------------
        // a (borrow) return is refcount-invisible -> reject at the signature.
        (
            "cut55_4_borrow_return_reject",
            Expect::Reject("borrow-return-not-permitted"),
        ),
        // the clone escape hatch lifts a borrowed field to an owned return.
        ("cut55_4b_clone_to_return", Expect::Accept),
        // --- the cutover's core promise: no defaulted mode ---------------
        // a bare fn-type slot is rejected at the PARSER stage, not linearity.
        (
            "cut55_5_bare_slot_reject",
            Expect::Reject("surface-parse-error"),
        ),
    ];

    for (stem, expect) in table {
        let (ok, stderr) = check(stem);
        match expect {
            Expect::Accept => {
                assert!(
                    ok,
                    "fixture `{stem}` must be ACCEPTED (exit 0) by the \
                     post-#55 ownership surface, but `ail check` rejected \
                     it. stderr:\n{stderr}"
                );
            }
            Expect::AcceptWithWarning(code) => {
                assert!(
                    ok,
                    "fixture `{stem}` must exit 0 (the `{code}` advisory \
                     is a warning, not an error), but `ail check` exited \
                     non-zero. stderr:\n{stderr}"
                );
                assert!(
                    stderr.contains(&format!("[{code}]")),
                    "fixture `{stem}` must emit the `[{code}]` advisory \
                     warning, but it did not appear in stderr. stderr:\n{stderr}"
                );
            }
            Expect::Reject(code) => {
                assert!(
                    !ok,
                    "fixture `{stem}` must be REJECTED (non-zero exit) by \
                     the post-#55 ownership surface, but `ail check` \
                     accepted it (exit 0). stderr:\n{stderr}"
                );
                assert!(
                    stderr.contains(&format!("[{code}]")),
                    "fixture `{stem}` must be rejected with diagnostic code \
                     `[{code}]`, but that code did not appear in stderr. \
                     The reject may have moved to a different stage or code. \
                     stderr:\n{stderr}"
                );
            }
        }
    }
}
