//! Cross-subcommand parser and dispatch tests for `cli::dispatch`.
//!
//! Subcommand-specific clap-parse tests live next to their args struct.
//! These tests exercise the dispatch glue or a parser surface that
//! spans more than one subcommand.

use std::time::Duration;

use clap::Parser;
use clap_complete::Shell;

use super::*;

#[test]
fn parses_minimal_run_invocation() {
    let cli = Cli::try_parse_from(["mcptest", "run"]).expect("parse");
    assert!(matches!(cli.command, Command::Run(_)));
    assert!(!cli.global.debug);
}

#[test]
fn parses_eval_max_cost_flag_with_dollar_sign() {
    let cli = Cli::try_parse_from(["mcptest", "eval", "--max-cost", "$5.00"]).expect("parse");
    match cli.command {
        Command::Eval(args) => assert_eq!(args.max_cost, Some(5.0)),
        other => panic!("expected Command::Eval, got {other:?}"),
    }
}

#[test]
fn parses_eval_max_cost_flag_without_dollar_sign() {
    let cli = Cli::try_parse_from(["mcptest", "eval", "--max-cost", "1.25"]).expect("parse");
    match cli.command {
        Command::Eval(args) => assert_eq!(args.max_cost, Some(1.25)),
        other => panic!("expected Command::Eval, got {other:?}"),
    }
}

#[test]
fn rejects_negative_max_cost() {
    let err = Cli::try_parse_from(["mcptest", "eval", "--max-cost=-1"]).unwrap_err();
    assert!(err.to_string().contains("non-negative"));
}

#[test]
fn collects_repeatable_var_flag() {
    let cli = Cli::try_parse_from(["mcptest", "--var", "FOO=bar", "--var", "BAZ=qux", "run"])
        .expect("parse");
    assert_eq!(
        cli.global.vars,
        vec![
            ("FOO".to_owned(), "bar".to_owned()),
            ("BAZ".to_owned(), "qux".to_owned()),
        ]
    );
}

#[test]
fn rejects_var_without_equals() {
    let err = Cli::try_parse_from(["mcptest", "--var", "BADVAR", "run"]).unwrap_err();
    assert!(err.to_string().contains("KEY=VALUE"));
}

#[test]
fn rejects_var_with_empty_key() {
    let err = Cli::try_parse_from(["mcptest", "--var", "=value", "run"]).unwrap_err();
    assert!(err.to_string().contains("variable name is empty"));
}

#[test]
fn parses_timeout_seconds() {
    let cli = Cli::try_parse_from(["mcptest", "--timeout", "30", "run"]).expect("parse");
    assert_eq!(cli.global.timeout, Some(Duration::from_secs(30)));
}

#[test]
fn rejects_non_numeric_timeout() {
    let err = Cli::try_parse_from(["mcptest", "--timeout", "soon", "run"]).unwrap_err();
    assert!(err.to_string().contains("whole number of seconds"));
}

#[test]
fn parses_reporter_enum() {
    let cli = Cli::try_parse_from(["mcptest", "--reporter", "junit", "run"]).expect("parse");
    assert_eq!(cli.global.reporter, Some(Reporter::Junit));
}

#[test]
fn rejects_unknown_reporter() {
    let err = Cli::try_parse_from(["mcptest", "--reporter", "fancy", "run"]).unwrap_err();
    assert!(err.to_string().contains("fancy"));
}

#[test]
fn completions_requires_shell() {
    let err = Cli::try_parse_from(["mcptest", "completions"]).unwrap_err();
    assert!(err.to_string().to_lowercase().contains("shell"));
}

#[test]
fn completions_accepts_bash() {
    let cli = Cli::try_parse_from(["mcptest", "completions", "bash"]).expect("parse");
    match cli.command {
        Command::Completions(args) => assert_eq!(args.shell, Shell::Bash),
        other => panic!("unexpected variant: {other:?}"),
    }
}

#[test]
fn parses_compliance_invariants_subcommand() {
    use crate::cli::args::{ComplianceCommand, InvariantsFormat};
    let cli = Cli::try_parse_from([
        "mcptest",
        "compliance",
        "invariants",
        "--capture",
        "cap.json",
        "--format",
        "json",
    ])
    .expect("parse");
    match cli.command {
        Command::Compliance(args) => match args.command {
            ComplianceCommand::Invariants(i) => {
                assert_eq!(i.capture.to_str(), Some("cap.json"));
                assert_eq!(i.format, InvariantsFormat::Json);
            }
            other => panic!("expected Invariants, got {other:?}"),
        },
        other => panic!("expected Command::Compliance, got {other:?}"),
    }
}

#[test]
fn compliance_invariants_requires_capture() {
    let err = Cli::try_parse_from(["mcptest", "compliance", "invariants"]).unwrap_err();
    assert!(err.to_string().to_lowercase().contains("capture"));
}

#[test]
fn stub_dispatch_for_run_returns_zero() {
    // The repo checks in dogfood mcptest.yml files, so a bare default
    // resolution from the test cwd would pick one up (WOR-1350). Point
    // --config at a path that cannot exist to keep the no-config path.
    let dir = tempfile::tempdir().expect("tempdir");
    let missing = dir.path().join("mcptest.yaml");
    let code = dispatch([
        "mcptest".to_string(),
        "--config".to_string(),
        missing.display().to_string(),
        "run".to_string(),
    ])
    .expect("dispatch");
    assert_eq!(code, 0);
}

#[test]
fn dispatch_completions_runs_generator() {
    let code = dispatch(["mcptest", "completions", "bash"]).expect("dispatch");
    assert_eq!(code, 0);
}

#[test]
fn doctor_defaults_to_cl100k_base_tokenizer() {
    let cli = Cli::try_parse_from(["mcptest", "doctor"]).expect("parse");
    match cli.command {
        Command::Doctor(args) => {
            assert!(!args.no_tool_tokens);
            assert_eq!(args.tokenizer, "cl100k_base");
        }
        other => panic!("unexpected variant: {other:?}"),
    }
}

#[test]
fn doctor_accepts_no_tool_tokens_flag() {
    let cli = Cli::try_parse_from(["mcptest", "doctor", "--no-tool-tokens"]).expect("parse");
    match cli.command {
        Command::Doctor(args) => assert!(args.no_tool_tokens),
        other => panic!("unexpected variant: {other:?}"),
    }
}

#[test]
fn coverage_defaults_to_pretty_with_no_threshold() {
    let cli = Cli::try_parse_from(["mcptest", "coverage"]).expect("parse");
    match cli.command {
        Command::Coverage(args) => {
            assert!(args.threshold.is_none());
            assert_eq!(args.format, CoverageFormat::Pretty);
        }
        other => panic!("unexpected variant: {other:?}"),
    }
}

#[test]
fn coverage_accepts_threshold_and_json_format() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "coverage",
        "--threshold",
        "80",
        "--format",
        "json",
    ])
    .expect("parse");
    match cli.command {
        Command::Coverage(args) => {
            assert_eq!(args.threshold, Some(80.0));
            assert_eq!(args.format, CoverageFormat::Json);
        }
        other => panic!("unexpected variant: {other:?}"),
    }
}

#[test]
fn dispatch_doctor_with_no_tool_tokens_returns_zero() {
    let code = dispatch(["mcptest", "doctor", "--no-tool-tokens"]).expect("dispatch");
    assert_eq!(code, 0);
}

#[test]
fn dispatch_coverage_with_threshold_returns_zero_until_runner_lands() {
    // Hermetic suite path for the same WOR-1350 reason as the run stub:
    // the bare default would resolve the repo's checked-in mcptest.yml.
    let dir = tempfile::tempdir().expect("tempdir");
    let missing = dir.path().join("mcptest.yaml");
    let code = dispatch([
        "mcptest".to_string(),
        "coverage".to_string(),
        missing.display().to_string(),
        "--threshold".to_string(),
        "75".to_string(),
    ])
    .expect("dispatch");
    assert_eq!(code, 0);
}

#[test]
fn wait_for_ready_with_explicit_seconds_value() {
    let cli = Cli::try_parse_from(["mcptest", "run", "--wait-for-ready=30s"]).expect("parse");
    assert_eq!(cli.global.wait_for_ready, Some(Duration::from_secs(30)));
}

#[test]
fn wait_for_ready_with_minute_unit() {
    let cli = Cli::try_parse_from(["mcptest", "run", "--wait-for-ready=2m"]).expect("parse");
    assert_eq!(cli.global.wait_for_ready, Some(Duration::from_secs(120)));
}

#[test]
fn wait_for_ready_with_hour_unit() {
    let cli = Cli::try_parse_from(["mcptest", "run", "--wait-for-ready=1h"]).expect("parse");
    assert_eq!(cli.global.wait_for_ready, Some(Duration::from_secs(3600)));
}

#[test]
fn wait_for_ready_bare_integer_is_seconds() {
    let cli = Cli::try_parse_from(["mcptest", "run", "--wait-for-ready=45"]).expect("parse");
    assert_eq!(cli.global.wait_for_ready, Some(Duration::from_secs(45)));
}

#[test]
fn wait_for_ready_no_value_defaults_to_sixty_seconds() {
    let cli = Cli::try_parse_from(["mcptest", "run", "--wait-for-ready"]).expect("parse");
    assert_eq!(cli.global.wait_for_ready, Some(Duration::from_secs(60)));
}

#[test]
fn wait_for_ready_rejects_garbage() {
    let err = Cli::try_parse_from(["mcptest", "run", "--wait-for-ready=soon"]).unwrap_err();
    assert!(err.to_string().contains("duration"));
}

#[test]
fn quiet_flag_parses() {
    let cli = Cli::try_parse_from(["mcptest", "--quiet", "run"]).expect("parse");
    assert!(cli.global.quiet);
}

#[test]
fn env_files_flag_is_repeatable() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "--env-files",
        "first.env",
        "--env-files",
        "second.env",
        "run",
    ])
    .expect("parse");
    assert_eq!(
        cli.global
            .env_files
            .iter()
            .map(|p| p.display().to_string())
            .collect::<Vec<_>>(),
        vec!["first.env".to_string(), "second.env".to_string()],
    );
}

#[test]
fn env_kv_flag_is_repeatable_and_parses_key_value() {
    let cli = Cli::try_parse_from(["mcptest", "--env", "FOO=bar", "--env", "BAZ=qux", "run"])
        .expect("parse");
    assert_eq!(
        cli.global.env_vars,
        vec![
            ("FOO".to_owned(), "bar".to_owned()),
            ("BAZ".to_owned(), "qux".to_owned()),
        ]
    );
}

#[test]
fn no_env_file_flag_still_parses() {
    let cli = Cli::try_parse_from(["mcptest", "--no-env-file", "run"]).expect("parse");
    assert!(cli.global.no_env_file);
}

#[test]
fn init_parses_url_target_with_bearer_auth() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "init",
        "--url",
        "https://mcp.example.com/v1",
        "--auth",
        "bearer",
        "--auth-env",
        "MCPTEST_API_TOKEN",
    ])
    .expect("parse");
    match cli.command {
        Command::Init(args) => {
            assert_eq!(args.url.as_deref(), Some("https://mcp.example.com/v1"));
            assert_eq!(args.auth, InitAuthKind::Bearer);
            assert_eq!(args.auth_env.as_deref(), Some("MCPTEST_API_TOKEN"));
        }
        other => panic!("expected Init, got {other:?}"),
    }
}

#[test]
fn init_parses_url_target_with_wait_for_ready() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "init",
        "--url",
        "https://mcp.example.com/v1",
        "--wait-for-ready",
        "30s",
    ])
    .expect("parse");
    match cli.command {
        Command::Init(args) => {
            assert_eq!(args.wait_for_ready, Some(Duration::from_secs(30)));
        }
        other => panic!("expected Init, got {other:?}"),
    }
}

#[test]
fn discover_parses_url_flag_for_well_known_fetch() {
    let cli = Cli::try_parse_from(["mcptest", "discover", "--url", "https://mcp.example.com"])
        .expect("parse");
    match cli.command {
        Command::Discover(args) => {
            assert_eq!(args.url.as_deref(), Some("https://mcp.example.com"));
            assert!(args.server.is_none());
        }
        other => panic!("expected Discover, got {other:?}"),
    }
}

#[test]
fn discover_accepts_write_path_with_url() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "discover",
        "--url",
        "https://mcp.example.com",
        "--write",
        "mcptest.local.yml",
    ])
    .expect("parse");
    match cli.command {
        Command::Discover(args) => {
            assert_eq!(
                args.write.as_ref().map(|p| p.display().to_string()),
                Some("mcptest.local.yml".to_string())
            );
        }
        other => panic!("expected Discover, got {other:?}"),
    }
}

#[test]
fn doctor_parses_url_flag() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "doctor",
        "--url",
        "https://mcp.example.com/v1",
        "--bearer-token-env",
        "MCPTEST_API_TOKEN",
    ])
    .expect("parse");
    match cli.command {
        Command::Doctor(args) => {
            assert_eq!(args.url.as_deref(), Some("https://mcp.example.com/v1"));
            assert_eq!(args.bearer_token_env.as_deref(), Some("MCPTEST_API_TOKEN"));
        }
        other => panic!("expected Doctor, got {other:?}"),
    }
}

#[test]
fn parses_proxy_url_flag() {
    let cli = Cli::try_parse_from(["mcptest", "--proxy", "http://proxy.corp:8080", "run"])
        .expect("parse");
    assert_eq!(cli.global.proxy.as_deref(), Some("http://proxy.corp:8080"));
}

#[test]
fn parses_split_proxy_flags() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "--http-proxy",
        "http://corp:8080",
        "--https-proxy",
        "http://corp:8443",
        "--proxy-bypass",
        "localhost,.internal",
        "run",
    ])
    .expect("parse");
    assert_eq!(cli.global.http_proxy.as_deref(), Some("http://corp:8080"));
    assert_eq!(cli.global.https_proxy.as_deref(), Some("http://corp:8443"));
    assert_eq!(
        cli.global.proxy_bypass.as_deref(),
        Some("localhost,.internal")
    );
}

#[test]
fn noproxy_still_parses_as_hidden_alias_of_proxy_bypass() {
    let cli =
        Cli::try_parse_from(["mcptest", "--noproxy", "localhost,.internal", "run"]).expect("parse");
    assert_eq!(
        cli.global.proxy_bypass.as_deref(),
        Some("localhost,.internal")
    );
}

#[test]
fn rejects_no_proxy_with_proxy() {
    let err = Cli::try_parse_from([
        "mcptest",
        "--no-proxy",
        "--proxy",
        "http://corp:8080",
        "run",
    ])
    .expect_err("conflict");
    assert!(err.to_string().contains("cannot be used"));
}

#[test]
fn proxy_config_carries_flags_through() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "--http-proxy",
        "http://corp:8080",
        "--proxy-bypass",
        "localhost",
        "run",
    ])
    .expect("parse");
    let cfg = cli.global.proxy_config();
    assert_eq!(cfg.http.as_deref(), Some("http://corp:8080"));
    assert_eq!(cfg.bypass, vec!["localhost".to_string()]);
}

#[test]
fn proxy_config_default_is_empty() {
    let cli = Cli::try_parse_from(["mcptest", "run"]).expect("parse");
    assert!(cli.global.proxy_config().is_empty());
}

#[test]
fn inspect_parses_url_target() {
    let cli = Cli::try_parse_from(["mcptest", "inspect", "--url", "https://mcp.example.com/v1"])
        .expect("parse");
    match cli.command {
        Command::Inspect(args) => {
            assert_eq!(args.url.as_deref(), Some("https://mcp.example.com/v1"));
            assert!(args.command.is_empty());
            assert!(args.bearer_token_env.is_none());
        }
        other => panic!("expected Inspect, got {other:?}"),
    }
}

#[test]
fn inspect_parses_url_with_bearer_token_env() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "inspect",
        "--url",
        "https://mcp.example.com/v1",
        "--bearer-token-env",
        "MY_TOKEN",
    ])
    .expect("parse");
    match cli.command {
        Command::Inspect(args) => {
            assert_eq!(args.bearer_token_env.as_deref(), Some("MY_TOKEN"));
        }
        other => panic!("expected Inspect, got {other:?}"),
    }
}

#[test]
fn inspect_parses_stdio_target() {
    let cli = Cli::try_parse_from(["mcptest", "inspect", "--", "npx", "-y", "@scope/server"])
        .expect("parse");
    match cli.command {
        Command::Inspect(args) => {
            assert!(args.url.is_none());
            assert_eq!(args.command, vec!["npx", "-y", "@scope/server"]);
        }
        other => panic!("expected Inspect, got {other:?}"),
    }
}

#[test]
fn inspect_requires_at_least_one_target() {
    // No --url and no trailing command: should fail to parse OR fail at
    // dispatch. We verify the args struct accepted the empty form (clap
    // allows it) while the handler validates the conflict at runtime.
    let cli = Cli::try_parse_from(["mcptest", "inspect"]).expect("parse");
    assert!(matches!(cli.command, Command::Inspect(_)));
}

// WOR-1329: --output is the one canonical sink flag; --out stays a hidden
// alias on the subcommands that shipped with the old spelling.

#[test]
fn evidence_accepts_output_and_the_hidden_out_alias() {
    for flag in ["--output", "--out"] {
        let cli = Cli::try_parse_from(["mcptest", "evidence", "run.json", flag, "evidence.json"])
            .expect("parse");
        match cli.command {
            Command::Evidence(args) => {
                assert_eq!(
                    args.output.as_ref().map(|p| p.display().to_string()),
                    Some("evidence.json".to_string()),
                    "flag {flag} should bind the output path"
                );
            }
            other => panic!("expected Evidence, got {other:?}"),
        }
    }
}

#[test]
fn sbom_accepts_output_and_the_hidden_out_alias() {
    for flag in ["--output", "--out"] {
        let cli = Cli::try_parse_from(["mcptest", "sbom", flag, "bom.json"]).expect("parse");
        match cli.command {
            Command::Sbom(args) => {
                assert_eq!(
                    args.output.as_ref().map(|p| p.display().to_string()),
                    Some("bom.json".to_string()),
                    "flag {flag} should bind the output path"
                );
            }
            other => panic!("expected Sbom, got {other:?}"),
        }
    }
}

#[test]
fn conformance_run_accepts_output_and_the_hidden_out_alias() {
    for flag in ["--output", "--out"] {
        let cli = Cli::try_parse_from(["mcptest", "conformance", "run", flag, "report.json"])
            .expect("parse");
        match cli.command {
            Command::Conformance(args) => match args.command {
                ConformanceCommand::Run(run) => {
                    assert_eq!(
                        run.output.as_ref().map(|p| p.display().to_string()),
                        Some("report.json".to_string()),
                        "flag {flag} should bind the output path"
                    );
                }
                other => panic!("expected Conformance Run, got {other:?}"),
            },
            other => panic!("expected Conformance, got {other:?}"),
        }
    }
}

#[test]
fn out_alias_is_hidden_from_help() {
    let mut cmd = <Cli as clap::CommandFactory>::command();
    let evidence = cmd
        .find_subcommand_mut("evidence")
        .expect("evidence exists")
        .render_long_help()
        .to_string();
    assert!(evidence.contains("--output"), "help: {evidence}");
    assert!(!evidence.contains("--out "), "alias leaked: {evidence}");
}

// WOR-1331: the upload flags belong to `mcptest report` alone.

#[test]
fn report_accepts_upload_flags() {
    let cli = Cli::try_parse_from([
        "mcptest",
        "report",
        "run.json",
        "--format",
        "upload",
        "--upload-endpoint",
        "https://collector.example.com/v1/runs",
        "--upload-token-env",
        "COLLECTOR_TOKEN",
        "--upload-organization",
        "acme",
    ])
    .expect("parse");
    match cli.command {
        Command::Report(args) => {
            assert_eq!(
                args.upload.upload_endpoint.as_deref(),
                Some("https://collector.example.com/v1/runs")
            );
            assert_eq!(
                args.upload.upload_token_env.as_deref(),
                Some("COLLECTOR_TOKEN")
            );
            assert_eq!(args.upload.upload_organization.as_deref(), Some("acme"));
        }
        other => panic!("expected Report, got {other:?}"),
    }
}

#[test]
fn run_rejects_upload_flags_now_that_they_are_report_scoped() {
    let err = Cli::try_parse_from([
        "mcptest",
        "run",
        "--upload-endpoint",
        "https://collector.example.com/v1/runs",
    ])
    .expect_err("upload flags must not parse on run");
    assert!(
        err.to_string().contains("--upload-endpoint"),
        "error should name the rejected flag: {err}"
    );
}

#[test]
fn run_help_omits_upload_flags_and_report_help_lists_them() {
    let mut cmd = <Cli as clap::CommandFactory>::command();
    let run_help = cmd
        .find_subcommand_mut("run")
        .expect("run exists")
        .render_long_help()
        .to_string();
    assert!(!run_help.contains("--upload-endpoint"), "{run_help}");
    let mut cmd = <Cli as clap::CommandFactory>::command();
    let report_help = cmd
        .find_subcommand_mut("report")
        .expect("report exists")
        .render_long_help()
        .to_string();
    for flag in [
        "--upload-endpoint",
        "--upload-token-env",
        "--upload-organization",
    ] {
        assert!(report_help.contains(flag), "missing {flag}: {report_help}");
    }
}

// WOR-1332: the top-level help renders task-oriented groups instead of a
// 40-entry flat list, and `exec` (an internal SDK co-process mode) is hidden.

/// The group headings in the order they must appear.
const HELP_GROUP_HEADINGS: [&str; 6] = [
    "Start here:",
    "Agent integration:",
    "Explore a server:",
    "Quality and security:",
    "Reports and artifacts:",
    "Plumbing:",
];

#[test]
fn top_level_help_shows_group_headings_in_order() {
    let help = <Cli as clap::CommandFactory>::command()
        .render_long_help()
        .to_string();
    let mut last = 0;
    for heading in HELP_GROUP_HEADINGS {
        let pos = help
            .find(heading)
            .unwrap_or_else(|| panic!("heading {heading:?} missing from help:\n{help}"));
        assert!(pos > last, "heading {heading:?} out of order:\n{help}");
        last = pos;
    }
}

#[test]
fn every_visible_subcommand_appears_in_the_grouped_help() {
    // Guards the literal grouped block against drift: adding a subcommand
    // without filing it under a heading fails here.
    let cmd = <Cli as clap::CommandFactory>::command();
    let help = cmd.clone().render_long_help().to_string();
    for sub in cmd.get_subcommands().filter(|s| !s.is_hide_set()) {
        let entry = format!("\n    {} ", sub.get_name());
        assert!(
            help.contains(&entry),
            "subcommand {:?} missing from the grouped help:\n{help}",
            sub.get_name()
        );
    }
}

#[test]
fn exec_is_hidden_from_help_but_still_parses() {
    let help = <Cli as clap::CommandFactory>::command()
        .render_long_help()
        .to_string();
    assert!(
        !help.contains("\n    exec "),
        "exec must stay hidden:\n{help}"
    );
    let cli = Cli::try_parse_from(["mcptest", "exec", "--connection-server"]).expect("parse");
    assert!(matches!(cli.command, Command::Exec(_)));
}
