//! ct.1: E2E test for `ail migrate-canonical-types <dir>`.
//!
//! Builds a synthetic workspace in a tempdir with one fixture that
//! has a bare cross-module Type::Con ref, runs the migration, then
//! asserts: (a) the rewritten fixture's `Term::Ctor.type_name` is
//! now qualified; (b) running migration again produces no diff
//! (idempotence).
//!
//! Property protected: `ail migrate-canonical-types` rewrites bare
//! cross-module Type::Con / Term::Ctor.type_name refs to their
//! qualified form using first-match-in-imports-order, AND a second
//! run on the already-canonical workspace is a byte-stable no-op.

use std::fs;
use std::path::PathBuf;
use std::process::Command;

fn tmp_dir(tag: &str) -> PathBuf {
    let d = std::env::temp_dir().join(format!(
        "ailang_migrate_test_{tag}_{}",
        std::process::id()
    ));
    let _ = fs::remove_dir_all(&d);
    fs::create_dir_all(&d).unwrap();
    d
}

fn ail_bin() -> PathBuf {
    // CARGO_BIN_EXE_<name> is set by cargo when building integration tests.
    PathBuf::from(env!("CARGO_BIN_EXE_ail"))
}

#[test]
fn migrate_canonical_types_rewrites_bare_xmod_term_ctor() {
    let dir = tmp_dir("rewrites_bare_xmod");
    // Defining module: `other` declares `type Foo`.
    let other_path = dir.join("other.ail.json");
    fs::write(&other_path, serde_json::to_vec_pretty(&serde_json::json!({
        "schema": "ailang/v0",
        "name": "other",
        "imports": [],
        "defs": [{ "kind": "type", "name": "Foo", "ctors": [{ "name": "MkFoo", "fields": [] }] }],
    })).unwrap()).unwrap();
    // Consumer module: imports `other`, uses bare `Foo` Term::Ctor.
    let main_path = dir.join("main.ail.json");
    let bare_module = serde_json::json!({
        "schema": "ailang/v0",
        "name": "main",
        "imports": [{ "module": "other" }],
        "defs": [{
            "kind": "fn", "name": "f",
            "type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, "effects": [] },
            "params": [],
            "body": { "t": "ctor", "type": "Foo", "ctor": "MkFoo", "args": [] }
        }],
    });
    fs::write(&main_path, serde_json::to_vec_pretty(&bare_module).unwrap()).unwrap();

    // Run migration.
    let status = Command::new(ail_bin())
        .args(["migrate-canonical-types", dir.to_str().unwrap()])
        .status()
        .expect("ail binary must launch");
    assert!(status.success(), "migration exited non-zero");

    // Inspect rewritten consumer.
    let after: serde_json::Value = serde_json::from_slice(
        &fs::read(&main_path).unwrap()
    ).unwrap();
    let ctor_type = after["defs"][0]["body"]["type"].as_str().unwrap();
    assert_eq!(ctor_type, "other.Foo",
        "Term::Ctor.type must be qualified after migration");

    // Idempotence: run migration again, capture bytes, compare.
    let bytes_before = fs::read(&main_path).unwrap();
    let status2 = Command::new(ail_bin())
        .args(["migrate-canonical-types", dir.to_str().unwrap()])
        .status()
        .expect("ail binary must launch");
    assert!(status2.success(), "second migration exited non-zero");
    let bytes_after = fs::read(&main_path).unwrap();
    assert_eq!(bytes_before, bytes_after, "migration must be idempotent");
}

/// Symmetric to `migrate_canonical_types_rewrites_bare_xmod_term_ctor`,
/// but the only bare cross-module reference in the consumer is a
/// `Type::Con` in a fn return type — the body carries no `Term::Ctor`
/// at all. This pins the Type::Con rewrite branch of `rewrite_def` as
/// a property: a regression that broke that branch (e.g. accidentally
/// scoping `rewrite_type` to only fn params) would leave the Term::Ctor
/// test green and ship the bug.
#[test]
fn migrate_canonical_types_rewrites_bare_xmod_type_con() {
    let dir = tmp_dir("rewrites_bare_type_con");
    // Defining module: `other` declares `type Foo`.
    let other_path = dir.join("other.ail.json");
    fs::write(&other_path, serde_json::to_vec_pretty(&serde_json::json!({
        "schema": "ailang/v0",
        "name": "other",
        "imports": [],
        "defs": [{ "kind": "type", "name": "Foo", "ctors": [{ "name": "MkFoo", "fields": [] }] }],
    })).unwrap()).unwrap();
    // Consumer module: imports `other`, body is a unit literal (no
    // Term::Ctor) — the only bare cross-module reference is the
    // Type::Con `Foo` in the fn return type.
    let main_path = dir.join("main.ail.json");
    let bare_module = serde_json::json!({
        "schema": "ailang/v0",
        "name": "main",
        "imports": [{ "module": "other" }],
        "defs": [{
            "kind": "fn", "name": "f",
            "type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Foo" }, "effects": [] },
            "params": [],
            "body": { "t": "lit", "lit": { "kind": "unit" } }
        }],
    });
    fs::write(&main_path, serde_json::to_vec_pretty(&bare_module).unwrap()).unwrap();

    // Run migration.
    let status = Command::new(ail_bin())
        .args(["migrate-canonical-types", dir.to_str().unwrap()])
        .status()
        .expect("ail binary must launch");
    assert!(status.success(), "migration exited non-zero");

    // Inspect rewritten consumer.
    let after: serde_json::Value = serde_json::from_slice(
        &fs::read(&main_path).unwrap()
    ).unwrap();
    let ret_name = after["defs"][0]["type"]["ret"]["name"].as_str().unwrap();
    assert_eq!(ret_name, "other.Foo",
        "fn-return Type::Con.name must be qualified after migration");

    // Idempotence: run migration again, capture bytes, compare.
    let bytes_before = fs::read(&main_path).unwrap();
    let status2 = Command::new(ail_bin())
        .args(["migrate-canonical-types", dir.to_str().unwrap()])
        .status()
        .expect("ail binary must launch");
    assert!(status2.success(), "second migration exited non-zero");
    let bytes_after = fs::read(&main_path).unwrap();
    assert_eq!(bytes_before, bytes_after, "migration must be idempotent");
}
