percli 0.2.3

Offline CLI simulator for the Percolator risk engine
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::NamedTempFile;

fn percli() -> Command {
    Command::cargo_bin("percli").unwrap()
}

#[test]
fn init_basic() {
    percli()
        .args(["init"])
        .assert()
        .success()
        .stdout(predicate::str::contains("[meta]"))
        .stdout(predicate::str::contains("[params]"))
        .stdout(predicate::str::contains("[[steps]]"));
}

#[test]
fn init_all_templates() {
    for template in ["basic", "liquidation", "haircut"] {
        percli()
            .args(["init", "--template", template])
            .assert()
            .success()
            .stdout(predicate::str::contains("[meta]"));
    }
}

#[test]
fn init_unknown_template() {
    percli()
        .args(["init", "--template", "nonexistent"])
        .assert()
        .failure();
}

#[test]
fn sim_all_scenarios() {
    for entry in std::fs::read_dir("../../scenarios").unwrap() {
        let path = entry.unwrap().path();
        if path.extension().is_some_and(|e| e == "toml") {
            percli()
                .args(["sim", path.to_str().unwrap()])
                .assert()
                .success();
        }
    }
}

#[test]
fn sim_json_valid() {
    let output = percli()
        .args([
            "sim",
            "../../scenarios/basic-trade.toml",
            "--format",
            "json",
        ])
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
    assert!(parsed["final_state"].is_object());
    assert!(parsed["steps"].is_array());
}

#[test]
fn sim_step_by_step() {
    let output = percli()
        .args(["sim", "../../scenarios/basic-trade.toml", "--step-by-step"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    let count = stdout.matches("Vault Summary").count();
    assert!(
        count >= 3,
        "expected multiple Vault Summary tables, got {count}"
    );
}

#[test]
fn sim_verbose() {
    percli()
        .args(["sim", "../../scenarios/basic-trade.toml", "--verbose"])
        .assert()
        .success()
        .stdout(predicate::str::contains("vault +"));
}

#[test]
fn sim_override() {
    percli()
        .args([
            "sim",
            "../../scenarios/basic-trade.toml",
            "--override",
            "maintenance_margin_bps=300",
        ])
        .assert()
        .success();
}

#[test]
fn step_deposit_creates_state() {
    let tmp = NamedTempFile::new().unwrap();
    let path = tmp.path().to_str().unwrap();
    std::fs::remove_file(path).ok();

    percli()
        .args(["step", "--state", path, "deposit", "alice", "100000"])
        .assert()
        .success();

    let content = std::fs::read_to_string(path).unwrap();
    let state: serde_json::Value = serde_json::from_str(&content).unwrap();
    assert_eq!(state["version"], 2);
    assert!(state["operations"].is_array());
}

#[test]
fn step_roundtrip() {
    let tmp = NamedTempFile::new().unwrap();
    let path = tmp.path().to_str().unwrap();
    std::fs::remove_file(path).ok();

    percli()
        .args(["step", "--state", path, "deposit", "alice", "100000"])
        .assert()
        .success();

    percli()
        .args(["step", "--state", path, "deposit", "bob", "100000"])
        .assert()
        .success();

    percli()
        .args([
            "step", "--state", path, "crank", "--oracle", "1000", "--slot", "1",
        ])
        .assert()
        .success();

    percli()
        .args(["step", "--state", path, "set-oracle", "1100"])
        .assert()
        .success();

    percli()
        .args(["query", "--state", path, "vault"])
        .assert()
        .success()
        .stdout(predicate::str::contains("200,000").or(predicate::str::contains("200000")));
}

#[test]
fn inspect_valid() {
    percli()
        .args(["inspect", "../../scenarios/basic-trade.toml"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Scenario is valid"));
}

#[test]
fn inspect_invalid() {
    let tmp = NamedTempFile::new().unwrap();
    std::fs::write(tmp.path(), "this is not valid toml {{{{").unwrap();

    percli()
        .args(["inspect", tmp.path().to_str().unwrap()])
        .assert()
        .failure();
}

#[test]
fn completions_bash() {
    percli()
        .args(["completions", "bash"])
        .assert()
        .success()
        .stdout(predicate::str::contains("_percli"));
}

#[test]
fn completions_zsh() {
    percli().args(["completions", "zsh"]).assert().success();
}