//! `CliGenerator` — generates a Unix shell script and/or Windows `.cmd` file.
//!
//! The generated script:
//! - Has a `#!/usr/bin/env sh` shebang on Unix.
//! - Contains a `BRIDGES` map with one or more live session bridges.
//! - Has one `case` branch per upstream tool, each mapped to a kebab-case subcommand.
//! - Picks the bridge for the current process tree when multiple sessions share a CLI name.
//! - Passes `Authorization: Bearer <session token>` on every `POST /exec` request.
//! - Is marked executable (Unix `chmod 755`).

use std::collections::HashSet;
use std::net::ToSocketAddrs;

use crate::cli::mapping::tool_name_to_subcommand;
use crate::client_gen::generator::{
    CliBridgeEntry, ClientGenerator, GeneratedArtifact, GeneratorConfig,
};
use crate::Error;

pub struct CliGenerator;

impl ClientGenerator for CliGenerator {
    fn render(&self, config: &GeneratorConfig) -> Result<Vec<GeneratedArtifact>, Error> {
        Ok(render_cli_artifacts(config))
    }
}

#[cfg(not(windows))]
fn render_cli_artifacts(config: &GeneratorConfig) -> Vec<GeneratedArtifact> {
    vec![GeneratedArtifact::new(&config.cli_name, render_unix_script(config)).executable()]
}

#[cfg(windows)]
fn render_cli_artifacts(config: &GeneratorConfig) -> Vec<GeneratedArtifact> {
    vec![
        GeneratedArtifact::new(&config.cli_name, render_unix_script(config)).executable(),
        GeneratedArtifact::new(
            format!("{}.cmd", config.cli_name),
            render_windows_cmd(config),
        ),
    ]
}

fn render_unix_script(config: &GeneratorConfig) -> String {
    let help = render_top_level_help(config);
    let bridges = bridge_map_literal(config);
    let mut script = format!(
        r#"#!/usr/bin/env sh
# generated by mcp-compressor — do not edit manually

show_help() {{
cat <<'HELP'
{help}
HELP
}}

find_bridge() {{
  python3 - <<'PY'
import json, os, urllib.request

BRIDGES = {bridges}

def alive(entry):
    try:
        req = urllib.request.Request(entry["bridge"] + "/health", method="GET")
        with urllib.request.urlopen(req, timeout=1) as resp:
            return resp.status == 200
    except Exception:
        return False

def ancestors():
    pid = os.getppid()
    seen = set()
    while pid and pid not in seen:
        seen.add(pid)
        yield str(pid)
        try:
            with open(f"/proc/{{pid}}/stat", "r", encoding="utf-8") as handle:
                stat = handle.read()
            pid = int(stat.rsplit(")", 1)[1].split()[1])
        except Exception:
            try:
                import subprocess
                output = subprocess.check_output(["ps", "-o", "ppid=", "-p", str(pid)], text=True)
                pid = int(output.strip() or "0")
            except Exception:
                break

for pid in ancestors():
    entry = BRIDGES.get(pid)
    if entry and alive(entry):
        print(json.dumps(entry))
        raise SystemExit(0)
for entry in BRIDGES.values():
    if alive(entry):
        print(json.dumps(entry))
        raise SystemExit(0)
raise SystemExit(1)
PY
}}

case "$1" in
  ""|--help|-h|help)
    show_help
    ;;
"#,
        bridges = bridges,
        help = help,
    );

    for tool in &config.tools {
        let subcommand = tool_name_to_subcommand(&tool.name);
        let tool_help = render_tool_help(&config.cli_name, tool);
        script.push_str(&format!(
            r#"  {subcommand})
    shift
    if [ "$1" = "--help" ] || [ "$1" = "-h" ] || [ "$1" = "help" ]; then
cat <<'HELP'
{tool_help}
HELP
      exit 0
    fi
    BRIDGE_ENTRY=$(find_bridge) || {{
      echo "mcp-compressor proxy is not running; restart the mcp-compressor CLI-mode process and try again." >&2
      exit 1
    }}
    python3 - "$BRIDGE_ENTRY" "{tool_name}" "$@" <<'PY'
import json, sys, urllib.error, urllib.request

TOOL_SCHEMAS = json.loads({tool_schemas:?})

bridge_entry, tool_name, *argv = sys.argv[1:]
entry = json.loads(bridge_entry)
bridge = entry["bridge"]
token = entry["token"]
tool_schema = TOOL_SCHEMAS[tool_name]
properties = tool_schema.get("inputSchema") or tool_schema.get("input_schema") or {{}}
properties = properties.get("properties", {{}}) if isinstance(properties, dict) else {{}}

def schema_type(schema):
    return schema.get("type") if isinstance(schema, dict) else None

def canonical_name(value):
    return value.replace("-", "_").replace("_", "").lower()

def flag_to_property(flag):
    raw = flag[2:]
    if raw.startswith("no-"):
        raw = raw[3:]
    if raw in properties:
        return raw
    snake = raw.replace("-", "_")
    if snake in properties:
        return snake
    canonical = canonical_name(raw)
    for prop in properties:
        if canonical_name(prop) == canonical:
            return prop
    return raw

def coerce_value(schema, raw_value, forced_bool=None):
    if forced_bool is not None:
        return forced_bool
    typ = schema_type(schema)
    if typ == "boolean":
        if raw_value is None:
            return True
        if raw_value == "true":
            return True
        if raw_value == "false":
            return False
        raise SystemExit(f"invalid boolean value: {{raw_value}}")
    if typ == "integer":
        try:
            return int(raw_value)
        except Exception:
            raise SystemExit(f"invalid integer value: {{raw_value}}")
    if typ == "number":
        try:
            return float(raw_value)
        except Exception:
            raise SystemExit(f"invalid number value: {{raw_value}}")
    if typ == "array":
        try:
            parsed = json.loads(raw_value)
            if isinstance(parsed, list):
                return parsed
        except Exception:
            pass
        return coerce_value(schema.get("items", {{}}), raw_value)
    try:
        return json.loads(raw_value or "")
    except Exception:
        return raw_value or ""

def insert_value(output, key, schema, value):
    if schema_type(schema) == "array":
        values = value if isinstance(value, list) else [value]
        output.setdefault(key, []).extend(values)
    else:
        output[key] = value

def parse_args(argv):
    if argv and argv[0] == "--json":
        if len(argv) < 2:
            raise SystemExit("--json requires a value")
        if len(argv) > 2:
            raise SystemExit("--json cannot be combined with other arguments")
        return json.loads(argv[1])
    output = {{}}
    index = 0
    while index < len(argv):
        flag = argv[index]
        if not flag.startswith("--") or flag == "--":
            raise SystemExit(f"unexpected positional argument: {{flag}}")
        prop = flag_to_property(flag)
        if prop not in properties:
            raise SystemExit(f"unknown flag: {{flag}}")
        schema = properties[prop]
        typ = schema_type(schema)
        forced_bool = False if flag.startswith("--no-") else None
        if forced_bool is False:
            if typ != "boolean":
                raise SystemExit(f"{{flag}} can only be used with boolean properties")
            raw_value = None
            consumed = 1
        elif typ == "boolean":
            if index + 1 < len(argv) and not argv[index + 1].startswith("--"):
                raw_value = argv[index + 1]
                consumed = 2
            else:
                raw_value = None
                consumed = 1
        else:
            if index + 1 >= len(argv) or argv[index + 1].startswith("--"):
                raise SystemExit(f"{{flag}} requires a value")
            raw_value = argv[index + 1]
            consumed = 2
        insert_value(output, prop, schema, coerce_value(schema, raw_value, forced_bool))
        index += consumed
    return output

tool_input = parse_args(argv)
payload = json.dumps({{"tool": tool_name, "input": tool_input}}).encode()
req = urllib.request.Request(
    bridge + "/exec",
    data=payload,
    headers={{"Content-Type": "application/json", "Authorization": "Bearer " + token}},
    method="POST",
)
try:
    with urllib.request.urlopen(req, timeout=30) as resp:
        sys.stdout.write(resp.read().decode())
except urllib.error.HTTPError as exc:
    message = exc.read().decode(errors="replace") or exc.reason
    print(f"mcp-compressor proxy returned HTTP {{exc.code}}: {{message}}", file=sys.stderr)
    raise SystemExit(1)
except urllib.error.URLError as exc:
    print(
        "mcp-compressor proxy is not running; restart the mcp-compressor CLI-mode process and try again.",
        file=sys.stderr,
    )
    print(f"details: {{exc.reason}}", file=sys.stderr)
    raise SystemExit(1)
PY
    ;;
"#,
            subcommand = subcommand,
            tool_help = tool_help,
            tool_name = shell_quote(&tool.name),
            tool_schemas = tool_schema_map_literal(config),
        ));
    }

    script.push_str(
        r#"  *)
    echo "Usage: $0 <subcommand> [args...]" >&2
    exit 2
    ;;
esac
"#,
    );
    script
}

#[cfg(windows)]
fn render_windows_cmd(config: &GeneratorConfig) -> String {
    let mut script = format!(
        "@echo off\r\nREM generated by mcp-compressor -- do not edit manually\r\nREM session {} -> {}\r\n",
        config.session_pid, config.bridge_url
    );
    for tool in &config.tools {
        script.push_str(&format!(
            "REM subcommand: {}\r\n",
            tool_name_to_subcommand(&tool.name)
        ));
    }
    script
}

pub fn read_live_bridge_entries(script: &str) -> Vec<CliBridgeEntry> {
    let Some(line) = script
        .lines()
        .find(|line| line.trim_start().starts_with("BRIDGES = "))
    else {
        return Vec::new();
    };
    let Some(json) = line.split_once('=').map(|(_, value)| value.trim()) else {
        return Vec::new();
    };
    let Ok(value) = serde_json::from_str::<serde_json::Value>(json) else {
        return Vec::new();
    };
    value
        .as_object()
        .into_iter()
        .flat_map(|object| object.iter())
        .filter_map(|(pid, entry)| {
            let session_pid = pid.parse().ok()?;
            Some(CliBridgeEntry {
                session_pid,
                bridge_url: entry.get("bridge")?.as_str()?.to_string(),
                token: entry.get("token")?.as_str()?.to_string(),
            })
        })
        .filter(|entry| bridge_is_live(&entry.bridge_url))
        .collect()
}

fn tool_schema_map_literal(config: &GeneratorConfig) -> String {
    let mut map = serde_json::Map::new();
    for tool in &config.tools {
        map.insert(
            tool.name.clone(),
            serde_json::to_value(tool).unwrap_or_else(|_| serde_json::json!({})),
        );
    }
    serde_json::to_string(&serde_json::Value::Object(map)).unwrap_or_else(|_| "{}".to_string())
}

fn bridge_map_literal(config: &GeneratorConfig) -> String {
    let mut entries = config.extra_cli_bridges.clone();
    entries.retain(|entry| entry.session_pid != config.session_pid);
    entries.push(CliBridgeEntry {
        session_pid: config.session_pid,
        bridge_url: config.bridge_url.clone(),
        token: config.token.clone(),
    });
    let mut map = serde_json::Map::new();
    for entry in entries {
        map.insert(
            entry.session_pid.to_string(),
            serde_json::json!({ "bridge": entry.bridge_url, "token": entry.token }),
        );
    }
    serde_json::Value::Object(map).to_string()
}

fn bridge_is_live(bridge_url: &str) -> bool {
    let Some(address) = bridge_url.strip_prefix("http://") else {
        return false;
    };
    let Some(host_port) = address.split('/').next() else {
        return false;
    };
    host_port
        .to_socket_addrs()
        .ok()
        .and_then(|mut addresses| addresses.next())
        .is_some_and(|address| {
            std::net::TcpStream::connect_timeout(&address, std::time::Duration::from_millis(200))
                .is_ok()
        })
}

fn render_top_level_help(config: &GeneratorConfig) -> String {
    let mut help = format!(
        "{name} - the {name} toolset\n\nWhen relevant, outputs from this CLI will prefer using the TOON format for more efficient representation of data.\n\nUSAGE:\n  {name} <subcommand> [options]\n\nSUBCOMMANDS:\n",
        name = config.cli_name,
    );
    for tool in &config.tools {
        help.push_str(&format!(
            "  {:<35} {}\n",
            tool_name_to_subcommand(&tool.name),
            first_line(tool.description.as_deref().unwrap_or("")),
        ));
    }
    help.push_str(&format!(
        "\nRun '{} <subcommand> --help' for subcommand usage.",
        config.cli_name
    ));
    help
}

fn render_tool_help(cli_name: &str, tool: &crate::compression::engine::Tool) -> String {
    let subcommand = tool_name_to_subcommand(&tool.name);
    let mut help = format!(
        "{cli_name} {subcommand}\n\n{}\n\nUSAGE:\n  {cli_name} {subcommand}",
        tool.description.as_deref().unwrap_or("Invoke this tool."),
    );
    for param in tool.param_names() {
        help.push_str(&format!(" --{} <value>", tool_name_to_subcommand(&param)));
    }
    help.push_str("\n");
    let options = tool_options(tool);
    if !options.is_empty() {
        help.push_str("\nOPTIONS:\n");
        for option in options {
            help.push_str(&format_tool_option_help(&option));
        }
    }
    help
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolOption {
    name: String,
    ty: String,
    required: bool,
    description: Option<String>,
    default: Option<String>,
    enum_values: Vec<String>,
}

fn tool_options(tool: &crate::compression::engine::Tool) -> Vec<ToolOption> {
    let required = tool
        .input_schema
        .get("required")
        .and_then(|value| value.as_array())
        .map(|values| {
            values
                .iter()
                .filter_map(|value| value.as_str().map(str::to_string))
                .collect::<HashSet<_>>()
        })
        .unwrap_or_default();

    tool.input_schema
        .get("properties")
        .and_then(|value| value.as_object())
        .map(|properties| {
            properties
                .iter()
                .map(|(name, schema)| ToolOption {
                    name: name.clone(),
                    ty: schema_type_label(schema),
                    required: required.contains(name),
                    description: schema
                        .get("description")
                        .and_then(|value| value.as_str())
                        .map(str::to_string),
                    default: schema.get("default").map(default_value_label),
                    enum_values: schema
                        .get("enum")
                        .and_then(|value| value.as_array())
                        .map(|values| values.iter().map(default_value_label).collect())
                        .unwrap_or_default(),
                })
                .collect()
        })
        .unwrap_or_default()
}

fn format_tool_option_help(option: &ToolOption) -> String {
    let flag = format!("--{}", tool_name_to_subcommand(&option.name));
    let requirement = if option.required {
        "required"
    } else {
        "optional"
    };
    let mut line = format!("  {flag:<28} <{}>  {requirement}", option.ty);
    let mut details = Vec::new();
    if let Some(description) = &option.description {
        details.push(first_line(description).to_string());
    }
    if !option.enum_values.is_empty() {
        details.push(format!("values: {}", option.enum_values.join(", ")));
    }
    if let Some(default) = &option.default {
        details.push(format!("default: {default}"));
    }
    if !details.is_empty() {
        line.push_str(" — ");
        line.push_str(&details.join("; "));
    }
    line.push('\n');
    line
}

fn schema_type_label(schema: &serde_json::Value) -> String {
    if let Some(values) = schema.get("enum").and_then(|value| value.as_array()) {
        let labels = values
            .iter()
            .filter_map(|value| value.as_str().map(str::to_string))
            .collect::<Vec<_>>();
        if !labels.is_empty() {
            return labels.join("|");
        }
    }
    match schema.get("type").and_then(|value| value.as_str()) {
        Some("integer") => "integer".to_string(),
        Some("number") => "number".to_string(),
        Some("boolean") => "boolean".to_string(),
        Some("array") => schema
            .get("items")
            .map(|items| format!("{}[]", schema_type_label(items)))
            .unwrap_or_else(|| "array".to_string()),
        Some("object") => "json".to_string(),
        Some("string") | None => "string".to_string(),
        Some(other) => other.to_string(),
    }
}

fn default_value_label(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::String(value) => value.clone(),
        other => other.to_string(),
    }
}

fn first_line(value: &str) -> &str {
    value.lines().next().unwrap_or("")
}

fn shell_quote(value: &str) -> String {
    value.replace('\'', "'\\''")
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::client_gen::generator::test_helpers::{make_config, make_config_multiword_tool};
    use crate::client_gen::ClientGenerator;
    use std::fs;

    // ------------------------------------------------------------------
    // File creation
    // ------------------------------------------------------------------

    /// generate() returns at least one path.
    #[test]
    fn generate_returns_non_empty_paths() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        assert!(
            !paths.is_empty(),
            "expected at least one generated artifact"
        );
    }

    /// All returned paths actually exist on disk.
    #[test]
    fn generated_paths_exist() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        for path in &paths {
            assert!(path.exists(), "path does not exist: {path:?}");
        }
    }

    /// All returned paths are inside the configured `output_dir`.
    #[test]
    fn generated_paths_inside_output_dir() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        for path in &paths {
            assert!(
                path.starts_with(dir.path()),
                "path {path:?} is outside output_dir {:?}",
                dir.path(),
            );
        }
    }

    // ------------------------------------------------------------------
    // Unix script content
    // ------------------------------------------------------------------

    /// On Unix the primary script has a shebang line.
    #[test]
    #[cfg(unix)]
    fn unix_script_has_shebang() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let content = fs::read_to_string(unix_script).unwrap();
        assert!(
            content.starts_with("#!"),
            "script must start with shebang, got: {content:?}"
        );
    }

    /// On Unix the primary script is named after `cli_name`.
    #[test]
    #[cfg(unix)]
    fn unix_script_named_after_cli_name() {
        use std::ffi::OsStr;
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        // The Unix script has no extension
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        assert_eq!(unix_script.file_name(), Some(OsStr::new("my-server")));
    }

    /// The script embeds the session token verbatim.
    #[test]
    #[cfg(unix)]
    fn unix_script_contains_token() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let content = fs::read_to_string(unix_script).unwrap();
        assert!(content.contains(&config.token), "token not found in script");
    }

    /// The script embeds the bridge URL.
    #[test]
    #[cfg(unix)]
    fn unix_script_contains_bridge_url() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let content = fs::read_to_string(unix_script).unwrap();
        assert!(
            content.contains(&config.bridge_url),
            "bridge URL not found in script"
        );
    }

    #[test]
    #[cfg(unix)]
    fn unix_script_contains_multi_session_bridge_map() {
        let dir = tempfile::tempdir().unwrap();
        let mut config = make_config(dir.path());
        config.extra_cli_bridges = vec![CliBridgeEntry {
            session_pid: 111,
            bridge_url: "http://127.0.0.1:1".to_string(),
            token: "old-token".to_string(),
        }];
        let artifacts = CliGenerator.render(&config).unwrap();
        let content = &artifacts[0].contents;
        assert!(content.contains("BRIDGES = "));
        assert!(content.contains("\"111\""));
        assert!(content.contains("old-token"));
        assert!(content.contains(&format!("\"{}\"", config.session_pid)));
        assert!(content.contains(&config.token));
        assert!(content.contains("def ancestors():"));
    }

    /// Each upstream tool name appears as a subcommand in the script.
    #[test]
    #[cfg(unix)]
    fn unix_script_contains_all_tool_subcommands() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let content = fs::read_to_string(unix_script).unwrap();
        // Tool names appear either as-is or in kebab-case
        assert!(content.contains("fetch"), "subcommand 'fetch' not found");
        assert!(content.contains("search"), "subcommand 'search' not found");
    }

    /// A multi-word tool name appears as its kebab-case subcommand.
    #[test]
    #[cfg(unix)]
    fn unix_script_kebab_case_subcommand() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config_multiword_tool(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let content = fs::read_to_string(unix_script).unwrap();
        // "get_confluence_page" → "get-confluence-page"
        assert!(
            content.contains("get-confluence-page"),
            "expected kebab-case subcommand in script",
        );
    }

    /// Top-level help includes legacy-style title, usage, and subcommands.
    #[test]
    #[cfg(unix)]
    fn unix_script_contains_top_level_help() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let content = fs::read_to_string(unix_script).unwrap();
        assert!(content.contains("my-server - the my-server toolset"));
        assert!(content.contains("USAGE:"));
        assert!(content.contains("SUBCOMMANDS:"));
        assert!(content.contains("Run 'my-server <subcommand> --help'"));
    }

    /// Subcommand help includes usage and option names.
    #[test]
    #[cfg(unix)]
    fn unix_script_contains_subcommand_help() {
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let content = fs::read_to_string(unix_script).unwrap();
        assert!(content.contains("my-server fetch"));
        assert!(content.contains("OPTIONS:"));
        assert!(content.contains("--url <value>"));
        assert!(content.contains("--url"));
        assert!(content.contains("<string>  required — URL to fetch."));
        assert!(content.contains("--timeout"));
        assert!(content.contains("<integer>  optional — Timeout in seconds.; default: 30"));
        assert!(content.contains("--method"));
        assert!(content.contains("values: GET, POST"));
    }

    /// On Unix the generated script is executable.
    #[test]
    #[cfg(unix)]
    fn unix_script_is_executable() {
        use std::os::unix::fs::PermissionsExt;
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let unix_script = paths.iter().find(|p| p.extension().is_none()).unwrap();
        let perms = fs::metadata(unix_script).unwrap().permissions();
        assert_ne!(perms.mode() & 0o111, 0, "script must be executable");
    }

    // ------------------------------------------------------------------
    // Windows .cmd content
    // ------------------------------------------------------------------

    /// On Windows a `.cmd` file is generated.
    #[test]
    #[cfg(windows)]
    fn windows_cmd_generated() {
        use std::ffi::OsStr;
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        assert!(
            paths
                .iter()
                .any(|p| p.extension() == Some(OsStr::new("cmd"))),
            "expected a .cmd file on Windows",
        );
    }

    /// The Windows `.cmd` file contains the session token.
    #[test]
    #[cfg(windows)]
    fn windows_cmd_contains_token() {
        use std::ffi::OsStr;
        let dir = tempfile::tempdir().unwrap();
        let config = make_config(dir.path());
        let paths = CliGenerator.generate(&config).unwrap();
        let cmd = paths
            .iter()
            .find(|p| p.extension() == Some(OsStr::new("cmd")))
            .unwrap();
        let content = fs::read_to_string(cmd).unwrap();
        assert!(content.contains(&config.token));
    }
}
