//! `pic scripts deploy` / `pic scripts ls` edge cases beyond the
//! smoke test: unknown app, name override, version bumping, missing
//! file, and the no-`--app` walk across every accessible app.

use predicates::prelude::*;

use crate::common;
use crate::common::cleanup::AppGuard;

/// Extract a field value from a `pic` KvBlock (`key<pad>\tvalue` per line).
/// `pic scripts deploy` prints `name`/`version`/`action` rows, not a prose
/// "Created X vN" line, so version-bump tests assert on these fields.
fn kv_field<'a>(stdout: &'a str, key: &str) -> Option<&'a str> {
    stdout.lines().find_map(|l| {
        let (k, v) = l.split_once('\t')?;
        (k.trim() == key).then(|| v.trim())
    })
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn deploy_against_unknown_app_errors() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let fixture = common::fixture_path("hello.rhai");
    let bogus_slug = common::unique_slug("nope");

    common::pic_as(&env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &bogus_slug,
        ])
        .assert()
        .failure()
        // Specifically 404 — `apps_get` short-circuits before the deploy
        // request even starts. Loose `"HTTP 4"` would have matched a
        // regressed 401 from broken auth.
        .stderr(predicate::str::contains("HTTP 404"));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn deploy_with_name_override() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("scripts-named");
    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();
    let _guard = AppGuard::new(&env.url, &env.token, &slug);

    let fixture = common::fixture_path("hello.rhai");
    let out = common::pic_as(&env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &slug,
            "--name",
            "custom-name",
        ])
        .output()
        .expect("deploy v1");
    let v1 = String::from_utf8(out.stdout).unwrap();
    assert_eq!(kv_field(&v1, "name"), Some("custom-name"), "v1 name: {v1}");
    assert_eq!(kv_field(&v1, "version"), Some("1"), "v1 version: {v1}");
    assert_eq!(kv_field(&v1, "action"), Some("created"), "v1 action: {v1}");

    let out = common::pic_as(&env)
        .args([
            "scripts",
            "deploy",
            fixture.to_str().unwrap(),
            "--app",
            &slug,
            "--name",
            "custom-name",
        ])
        .output()
        .expect("deploy v2");
    let v2 = String::from_utf8(out.stdout).unwrap();
    assert_eq!(kv_field(&v2, "version"), Some("2"), "v2 version: {v2}");
    assert_eq!(kv_field(&v2, "action"), Some("updated"), "v2 action: {v2}");

    let out = common::pic_as(&env)
        .args(["scripts", "ls", "--app", &slug])
        .output()
        .expect("scripts ls");
    assert!(out.status.success());
    let stdout = String::from_utf8(out.stdout).unwrap();
    assert!(
        stdout
            .lines()
            .map(common::cells)
            .any(|c| c.get(2).copied() == Some("custom-name") && c.get(3).copied() == Some("2")),
        "expected custom-name v2 row, got: {stdout}",
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn deploy_bumps_version_each_redeploy() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("scripts-bump");
    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();
    let _guard = AppGuard::new(&env.url, &env.token, &slug);

    let fixture = common::fixture_path("hello.rhai");
    for (version, action) in [("1", "created"), ("2", "updated"), ("3", "updated")] {
        let out = common::pic_as(&env)
            .args([
                "scripts",
                "deploy",
                fixture.to_str().unwrap(),
                "--app",
                &slug,
            ])
            .output()
            .expect("deploy");
        assert!(out.status.success(), "deploy v{version} failed: {out:?}");
        let stdout = String::from_utf8(out.stdout).unwrap();
        assert_eq!(
            kv_field(&stdout, "version"),
            Some(version),
            "version: {stdout}"
        );
        assert_eq!(
            kv_field(&stdout, "action"),
            Some(action),
            "action: {stdout}"
        );
    }
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn deploy_missing_file_errors() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug = common::unique_slug("scripts-missing");
    common::pic_as(&env)
        .args(["apps", "create", &slug])
        .assert()
        .success();
    let _guard = AppGuard::new(&env.url, &env.token, &slug);

    let missing = std::env::temp_dir().join(common::unique_slug("ghost") + ".rhai");
    common::pic_as(&env)
        .args([
            "scripts",
            "deploy",
            missing.to_str().unwrap(),
            "--app",
            &slug,
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("reading"));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn ls_without_app_walks_every_accessible_app() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let slug_a = common::unique_slug("scripts-walk-a");
    let slug_b = common::unique_slug("scripts-walk-b");

    common::pic_as(&env)
        .args(["apps", "create", &slug_a])
        .assert()
        .success();
    let _guard_a = AppGuard::new(&env.url, &env.token, &slug_a);
    common::pic_as(&env)
        .args(["apps", "create", &slug_b])
        .assert()
        .success();
    let _guard_b = AppGuard::new(&env.url, &env.token, &slug_b);

    let fixture = common::fixture_path("hello.rhai");
    for slug in [&slug_a, &slug_b] {
        common::pic_as(&env)
            .args([
                "scripts",
                "deploy",
                fixture.to_str().unwrap(),
                "--app",
                slug,
            ])
            .assert()
            .success();
    }

    // `pic scripts ls` (no `--app`) issues a single `GET /admin/scripts`
    // against the server now — there's nothing per-app to race against
    // a concurrent AppGuard drop. The previous implementation walked
    // `apps_list` followed by per-app `scripts_list_by_app` calls and
    // aborted on the first 404, which forced this test to retry 5× to
    // paper over the bug. Both the walk and the retry are gone.
    let out = common::pic_as(&env)
        .args(["scripts", "ls"])
        .output()
        .expect("scripts ls");
    assert!(out.status.success(), "scripts ls failed: {out:?}");
    let stdout = String::from_utf8(out.stdout).unwrap();

    let slugs: std::collections::HashSet<&str> = stdout
        .lines()
        .map(common::cells)
        .filter_map(|c| c.get(1).copied())
        .collect();
    assert!(
        slugs.contains(slug_a.as_str()),
        "missing app A in: {stdout}"
    );
    assert!(
        slugs.contains(slug_b.as_str()),
        "missing app B in: {stdout}"
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn delete_removes_script_from_ls() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);
    let (id, _guard) = common::deploy_fixture(&env, "scripts-del", "hello.rhai");

    common::pic_as(&env)
        .args(["scripts", "delete", &id])
        .assert()
        .success()
        .stdout(predicate::str::contains(format!("Deleted script {id}")));

    let out = common::pic_as(&env)
        .args(["scripts", "ls"])
        .output()
        .expect("scripts ls");
    assert!(out.status.success());
    let stdout = String::from_utf8(out.stdout).unwrap();
    assert!(
        !stdout.contains(&id),
        "deleted script id should not appear in ls: {stdout}"
    );
}
