//! §6 M2 — structural-divergence detection + declarative reparent.
//!
//! Once the manifest declares a group's parent (by directory nesting), a
//! `pic apply --dir` compares it to the server's actual parent. A divergence is
//! REFUSED by default (Terraform detect-and-refuse); `--force-local-structure`
//! reparents the group to match the manifest, `--adopt-server-structure` keeps
//! the server's placement and reconciles content in place. `pic plan` previews
//! the divergence first.

use std::fs;
use std::path::Path;

use tempfile::TempDir;

use crate::common;
use crate::common::cleanup::GroupGuard;

/// A repo that declares `mover` NESTED under `alt` (so its manifest parent is
/// `alt`), with `alt` at the attach point `base`.
fn diverging_repo(dir: &Path, project: &str, base: &str, alt: &str, mover: &str) {
    fs::write(
        dir.join("picloud.toml"),
        format!(
            "[project]\nslug = \"{project}\"\nparent_group = \"{base}\"\n\n\
             [group]\nslug = \"{alt}\"\nname = \"Alt\"\n"
        ),
    )
    .unwrap();
    let sub = dir.join("mover");
    fs::create_dir_all(&sub).unwrap();
    fs::write(
        sub.join("picloud.toml"),
        format!("[group]\nslug = \"{mover}\"\nname = \"Mover\"\n"),
    )
    .unwrap();
}

fn server_parent(env: &common::TestEnv, group: &str) -> String {
    let ls = common::pic_as(env)
        .args(["groups", "ls"])
        .output()
        .expect("groups ls");
    let table = String::from_utf8(ls.stdout).unwrap();
    table
        .lines()
        .map(common::cells)
        .find(|c| c.first() == Some(&group))
        .and_then(|c| c.get(2).map(|s| (*s).to_string()))
        .unwrap_or_else(|| panic!("group `{group}` not in groups ls:\n{table}"))
}

/// Pre-create base → {alt, mover} (both directly under base), so a repo that
/// nests `mover` under `alt` diverges.
fn seed(env: &common::TestEnv, base: &str, alt: &str, mover: &str) {
    common::pic_as(env)
        .args(["groups", "create", base])
        .assert()
        .success();
    common::pic_as(env)
        .args(["groups", "create", alt, "--parent", base])
        .assert()
        .success();
    common::pic_as(env)
        .args(["groups", "create", mover, "--parent", base])
        .assert()
        .success();
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn divergence_refused_by_default_then_force_local_reparents() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let base = common::unique_slug("sd-base");
    let alt = common::unique_slug("sd-alt");
    let mover = common::unique_slug("sd-mover");
    let project = common::unique_slug("sd-proj");
    let _b = GroupGuard::new(&env.url, &env.token, &base);
    let _a = GroupGuard::new(&env.url, &env.token, &alt);
    let _m = GroupGuard::new(&env.url, &env.token, &mover);
    seed(&env, &base, &alt, &mover);

    let dir = TempDir::new().unwrap();
    diverging_repo(dir.path(), &project, &base, &alt, &mover);

    // Plan previews the divergence (server parent `base`, manifest parent `alt`).
    let plan = String::from_utf8(
        common::pic_as(&env)
            .args(["plan", "--dir"])
            .arg(dir.path())
            .output()
            .unwrap()
            .stdout,
    )
    .unwrap();
    assert!(
        plan.contains("diverged") && plan.contains(&base) && plan.contains(&alt),
        "plan should show `mover` diverged (server={base}, manifest={alt}):\n{plan}"
    );

    // A bare apply is REFUSED on the divergence (422).
    let out = common::pic_as(&env)
        .args(["apply", "--dir"])
        .arg(dir.path())
        .output()
        .expect("apply");
    assert!(!out.status.success(), "a divergence must refuse by default");
    let err = String::from_utf8_lossy(&out.stderr).to_lowercase();
    assert!(
        err.contains("structural")
            || err.contains("force-local")
            || err.contains("different parent"),
        "the refusal must name the resolution flags:\n{err}"
    );
    assert_eq!(
        server_parent(&env, &mover),
        base,
        "a refused apply must not move the group"
    );

    // `--force-local-structure` reparents `mover` to `alt` (the manifest shape).
    common::pic_as(&env)
        .args(["apply", "--dir"])
        .arg(dir.path())
        .arg("--force-local-structure")
        .assert()
        .success();
    assert_eq!(
        server_parent(&env, &mover),
        alt,
        "--force-local-structure must reparent to the manifest's parent"
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn adopt_server_structure_keeps_the_server_placement() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let base = common::unique_slug("sda-base");
    let alt = common::unique_slug("sda-alt");
    let mover = common::unique_slug("sda-mover");
    let project = common::unique_slug("sda-proj");
    let _b = GroupGuard::new(&env.url, &env.token, &base);
    let _a = GroupGuard::new(&env.url, &env.token, &alt);
    let _m = GroupGuard::new(&env.url, &env.token, &mover);
    seed(&env, &base, &alt, &mover);

    let dir = TempDir::new().unwrap();
    diverging_repo(dir.path(), &project, &base, &alt, &mover);

    // `--adopt-server-structure` proceeds without reparenting — `mover` stays
    // under `base` (its server placement), and content reconciles in place.
    common::pic_as(&env)
        .args(["apply", "--dir"])
        .arg(dir.path())
        .arg("--adopt-server-structure")
        .assert()
        .success();
    assert_eq!(
        server_parent(&env, &mover),
        base,
        "--adopt-server-structure must keep the server's parent"
    );
}
