//! Global CLI flags ([`GlobalArgs`]) and the [`Reporter`] style enum.
//!
//! Lives in its own module so [`crate::cli`] stays under the
//! AGENTS.md 400-line ceiling. `GlobalArgs` flattens into every
//! subcommand via `#[command(flatten)]`, so the same flag surface is
//! available on `mcptest run`, `mcptest doctor`, etc.

use std::path::PathBuf;
use std::time::Duration;

use clap::{Args, ValueEnum};

use crate::cli::parsers::{parse_duration, parse_seconds};
use crate::cli::parsers::{parse_header_env, parse_header_literal, parse_key_value};

/// Global flags shared by every subcommand.
///
/// Kept in its own struct so each subcommand variant can inherit them via
/// `#[command(flatten)]` without restating each option. Order here is the
/// order they render in `--help`.
#[derive(Debug, Args, Default)]
pub struct GlobalArgs {
    /// Disable ANSI color output. Useful in CI logs and when piping.
    #[arg(long, global = true)]
    pub no_color: bool,

    /// Enable debug logging (sets `RUST_LOG=mcptest=debug` if unset).
    #[arg(long, global = true)]
    pub debug: bool,

    /// Print extra detail about variable resolution and discovered files.
    #[arg(long, global = true)]
    pub verbose: bool,

    /// Logging verbosity. Accepts `off`, `error`, `warn`, `info`, `debug`,
    /// `trace`, or a `RUST_LOG`-style directive like
    /// `mcptest_core::cache=debug,mcptest_core::runner=trace`. Overrides
    /// `RUST_LOG`, `MCPTEST_LOG`, `--debug`, and `--verbose`.
    #[arg(long = "log-level", value_name = "LEVEL", global = true)]
    pub log_level: Option<String>,

    /// Format for the diagnostic log on stderr: `pretty` (human, default) or
    /// `json` (one JSON object per line, for CI log pipelines). The report
    /// itself is controlled by `--reporter`, not this flag (WOR-1517).
    #[arg(
        long = "log-format",
        value_enum,
        value_name = "FORMAT",
        default_value_t = LogFormat::Pretty,
        global = true
    )]
    pub log_format: LogFormat,

    /// OTLP/HTTP collector endpoint for span export (or set
    /// `OTEL_EXPORTER_OTLP_ENDPOINT`). Requires a build with the `otlp`
    /// feature; without it, setting this fails fast rather than silently
    /// dropping spans. The exporter drains the `mcptest.run` / `mcp.request`
    /// spans; see docs/otel.md (WOR-1517).
    #[arg(long = "otel-endpoint", value_name = "URL", global = true)]
    pub otel_endpoint: Option<String>,

    /// Path to the mcptest YAML config (defaults to ./mcptest.yaml,
    /// then ./mcptest.yml).
    ///
    /// The `--suite` alias reads naturally for the agent and conformance
    /// examples, which talk about test suites rather than a single config.
    #[arg(long, visible_alias = "suite", value_name = "PATH", global = true)]
    pub config: Option<PathBuf>,

    /// Environment file to load before running (KEY=VALUE per line).
    ///
    /// Loaded in addition to any `.env`, `.env.local`, or `.env.test` files
    /// auto-discovered in the working directory. Kept for backwards
    /// compatibility with v0.x scripts; new callers should prefer the
    /// repeatable `--env-files` form below.
    #[arg(long = "env-file", value_name = "PATH", global = true)]
    pub env_file: Option<PathBuf>,

    /// Additional environment files to load before running (repeatable).
    ///
    /// Each occurrence appends to the load list. Files load lowest-first
    /// (a later `--env-files` overrides an earlier one), and every file
    /// loads at higher precedence than auto-discovered `.env*` files.
    #[arg(
        long = "env-files",
        value_name = "PATH",
        global = true,
        action = clap::ArgAction::Append
    )]
    pub env_files: Vec<PathBuf>,

    /// Set a single environment variable for the run. Repeatable.
    /// Format `KEY=VALUE`. Highest precedence after `--var`.
    #[arg(
        long = "env",
        value_name = "KEY=VALUE",
        value_parser = parse_key_value,
        global = true,
        action = clap::ArgAction::Append,
    )]
    pub env_vars: Vec<(String, String)>,

    /// Skip auto-discovery of `.env`, `.env.local`, and `.env.test`.
    ///
    /// Useful in CI when the runner injects every variable through real
    /// environment variables and a stray `.env` checked into the repo
    /// should not change behavior.
    #[arg(long = "no-env-file", global = true)]
    pub no_env_file: bool,

    /// Override a variable. Repeatable. Format: KEY=VALUE.
    #[arg(long = "var", value_name = "KEY=VALUE", value_parser = parse_key_value, global = true)]
    pub vars: Vec<(String, String)>,

    /// Print resolved variable values verbatim instead of redacting them.
    ///
    /// UNSAFE: never enable this in CI logs or shared terminals. Secrets
    /// resolved from `.env` files or OS environment will appear in plain
    /// text. Default is off; values render as `***`.
    #[arg(long = "show-secrets", global = true)]
    pub show_secrets: bool,

    /// Filter expression: only run tests whose name matches. Plain values
    /// match by literal substring; a `re:` prefix switches to regex
    /// (`--filter "re:^create_"` is anchored). An invalid regex is a
    /// config error (exit 2).
    #[arg(long, value_name = "EXPR", global = true)]
    pub filter: Option<String>,

    /// Output format for `mcptest run`. Defaults to the pretty summary;
    /// `json` is round-trippable through `mcptest report`.
    #[arg(long, value_enum, value_name = "FORMAT", global = true)]
    pub reporter: Option<Reporter>,

    /// Approximate token ceiling for the `agent` reporter. Failures past the
    /// budget are dropped and summarized in an `OMITTED` tail so a model
    /// reading the result inside an agent loop stays within a known size.
    /// Ignored by every other reporter.
    #[arg(long = "agent-budget", value_name = "TOKENS", global = true)]
    pub agent_budget: Option<usize>,

    /// Write the report to this path instead of stdout. Use "-" for stdout.
    /// The format follows `--reporter` (default pretty).
    #[arg(long, value_name = "PATH", global = true)]
    pub output: Option<PathBuf>,

    /// Emit GitHub Actions inline annotations (::error/::warning) to stderr
    /// alongside the normal output. `auto` (default) emits only inside
    /// Actions; `always` forces them; `never` disables them.
    #[arg(long, value_enum, value_name = "WHEN", default_value_t = AnnotationsMode::Auto, global = true)]
    pub annotations: AnnotationsMode,

    /// Colorize human-readable output. `auto` (default) colors only when the
    /// output is a terminal and `NO_COLOR` is unset; `always` forces color;
    /// `never` disables it. File sinks (`--output`) and machine formats stay
    /// plain regardless.
    #[arg(long, value_enum, value_name = "WHEN", default_value_t = ColorWhen::Auto, global = true)]
    pub color: ColorWhen,

    /// Maximum number of tests to run in parallel. 0 means auto.
    #[arg(long, value_name = "N", global = true)]
    pub parallel: Option<usize>,

    /// Per-test timeout, in seconds. Overrides any value in the config.
    #[arg(long, value_name = "SECONDS", value_parser = parse_seconds, global = true)]
    pub timeout: Option<Duration>,

    /// Retry each failing test up to N times before counting it as failed.
    #[arg(long, value_name = "N", global = true)]
    pub retry: Option<u32>,

    /// Re-run the suite whenever a watched file changes.
    ///
    /// Watches the suite YAML, its `imports:`, and the `--server-command`
    /// binary, then reruns on save so the authoring loop needs no retyping.
    /// TTY reporters only; machine formats (json, junit, ...) are refused
    /// because each loop iteration would clobber the artifact.
    #[arg(long, global = true)]
    pub watch: bool,

    /// Poll the server until it accepts `initialize`, or fail after the
    /// configured budget.
    ///
    /// Accepts a duration like `60s`, `2m`, or `1h`. When the flag is passed
    /// without an `=...` value, the budget defaults to `60s`. The driver uses
    /// exponential backoff (250ms, 500ms, 1s, 2s, then 5s steady) and fails
    /// fast on authentication or protocol-level errors. A server that never
    /// comes up aborts with the readiness exit code (3).
    #[arg(
        long = "wait-for-ready",
        value_name = "DURATION",
        value_parser = parse_duration,
        num_args = 0..=1,
        default_missing_value = "60s",
        require_equals = true,
        global = true,
    )]
    pub wait_for_ready: Option<Duration>,

    /// Suppress progress output (banners, per-second elapsed lines).
    ///
    /// Exit codes still communicate success and failure. Useful in CI logs
    /// where the only signal that matters is the final status line.
    #[arg(long, global = true)]
    pub quiet: bool,

    /// Override `server.url` for every server in the YAML suite.
    ///
    /// Preview-deploy and CI scenarios often do not know the target URL when
    /// the YAML is authored. This flag swaps the in-suite `server:` target
    /// (whether it was a `command:` or a `url:`) to the supplied URL.
    /// Mutually exclusive with `--server-command`. Wins over the same field
    /// in `--server-config`.
    #[arg(long = "server-url", value_name = "URL", global = true)]
    pub server_url: Option<String>,

    /// Override `server.command` for every server in the YAML suite.
    ///
    /// The argument is split using POSIX shell rules (see `shell-words`), so
    /// `--server-command "./dev-server --debug"` parses to
    /// `["./dev-server", "--debug"]`. Mutually exclusive with `--server-url`.
    /// Wins over the same field in `--server-config`.
    #[arg(long = "server-command", value_name = "CMD", global = true)]
    pub server_command: Option<String>,

    /// Set `server.auth.bearer_token_env` for URL targets.
    ///
    /// Useful when the YAML hard-codes the bearer env name for production
    /// and a preview environment uses a different secret name. Wins over
    /// the same field in `--server-config`.
    #[arg(long = "server-auth-bearer-env", value_name = "NAME", global = true)]
    pub server_auth_bearer_env: Option<String>,

    /// Load a YAML file containing a full `server:` block and use it in
    /// place of the in-suite server block.
    ///
    /// Lower precedence than the single-field flags above: when a field
    /// appears in both `--server-config` and a flag like `--server-url`,
    /// the explicit flag wins.
    #[arg(long = "server-config", value_name = "PATH", global = true)]
    pub server_config: Option<PathBuf>,

    /// Add a literal HTTP header. Repeatable. Format: `NAME=VALUE`.
    ///
    /// `Authorization` and `Proxy-Authorization` are rejected; credentials
    /// must live in `auth:`. The value is sent verbatim; combine with
    /// `--header-env` when the value should come from an env var.
    #[arg(long = "header", value_name = "NAME=VALUE", value_parser = parse_header_literal, global = true)]
    pub headers_literal: Vec<(String, String)>,

    /// Add an env-backed HTTP header. Repeatable. Format: `NAME=VAR_NAME`.
    ///
    /// The runner reads `VAR_NAME` from the environment at connect time and
    /// uses its value as the header value. Use this for secret-flavored
    /// custom headers (multi-tenant SaaS keys, internal trace tokens).
    #[arg(long = "header-env", value_name = "NAME=VAR", value_parser = parse_header_env, global = true)]
    pub headers_env: Vec<(String, String)>,

    /// Disable TLS certificate verification.
    ///
    /// Dangerous; only use against a private staging endpoint with a
    /// self-signed certificate. The runner prints a banner whenever this
    /// flag is set so the operator can spot a misconfigured CI job.
    #[arg(long = "insecure-skip-verify", global = true)]
    pub insecure_skip_verify: bool,

    /// Path to a PEM-encoded CA bundle for HTTP transport.
    #[arg(long = "ca-bundle", value_name = "PATH", global = true)]
    pub ca_bundle: Option<PathBuf>,

    /// HTTP per-request timeout, in seconds. Overrides `server.http.timeout`.
    #[arg(long = "http-timeout", value_name = "SECONDS", value_parser = parse_seconds, global = true)]
    pub http_timeout: Option<Duration>,

    /// HTTP connect timeout, in seconds. Overrides `server.http.connect_timeout`.
    #[arg(long = "connect-timeout", value_name = "SECONDS", value_parser = parse_seconds, global = true)]
    pub connect_timeout: Option<Duration>,

    /// Catch-all HTTP / HTTPS proxy URL.
    ///
    /// Applied to both server-facing transports (StreamableHttp, SSE)
    /// and LLM provider clients (Anthropic, OpenAI, Google, Mistral,
    /// and custom providers). Equivalent to setting `HTTP_PROXY` and
    /// `HTTPS_PROXY` to the same value. Mutually exclusive with
    /// `--no-proxy`.
    #[arg(long = "proxy", value_name = "URL", global = true)]
    pub proxy: Option<String>,

    /// HTTP-only proxy URL. Wins over `--proxy` for plain-HTTP targets.
    #[arg(long = "http-proxy", value_name = "URL", global = true)]
    pub http_proxy: Option<String>,

    /// HTTPS-only proxy URL. Wins over `--proxy` for HTTPS targets.
    #[arg(long = "https-proxy", value_name = "URL", global = true)]
    pub https_proxy: Option<String>,

    /// Disable every proxy, including reqwest's automatic env-var
    /// pickup. Use when the shell has a system-wide `HTTPS_PROXY`
    /// exported but this run should go direct. Mutually exclusive
    /// with `--proxy`, `--http-proxy`, and `--https-proxy`. To keep
    /// the proxy but exempt specific hosts, use `--proxy-bypass`.
    #[arg(
        long = "no-proxy",
        global = true,
        conflicts_with_all = ["proxy", "http_proxy", "https_proxy"]
    )]
    pub no_proxy: bool,

    /// Comma-separated list of hosts to bypass the proxy for. Each
    /// entry is an exact hostname or a leading-dot suffix
    /// (`.internal.example`). Mirrors `NO_PROXY` semantics. To go
    /// direct for everything, use the boolean `--no-proxy` instead.
    /// `--noproxy` is accepted as a hidden alias for the old spelling.
    #[arg(
        long = "proxy-bypass",
        alias = "noproxy",
        value_name = "HOSTLIST",
        global = true
    )]
    pub proxy_bypass: Option<String>,

    /// Expand the per-juror block in pretty output.
    ///
    /// Collapsed by default: the consensus line shows the verdict, juror
    /// count, and aggregate score/cost. With this flag the pretty
    /// reporter expands each juror's verdict, reasoning, cost, and
    /// latency, and surfaces failed jurors' status and error text.
    /// JSON output always carries the full juror array, so this flag
    /// only affects pretty.
    #[arg(long = "show-juror-details", global = true)]
    pub show_juror_details: bool,
}

impl GlobalArgs {
    /// Project the proxy-related CLI flags onto a
    /// [`mcptest_core::network::ProxyConfig`]. Used at the
    /// boundary where reqwest clients are constructed.
    pub fn proxy_config(&self) -> mcptest_core::network::ProxyConfig {
        use mcptest_core::network::{parse_bypass_list, ProxyConfig};
        ProxyConfig {
            all: self.proxy.clone(),
            http: self.http_proxy.clone(),
            https: self.https_proxy.clone(),
            disabled: self.no_proxy,
            bypass: self
                .proxy_bypass
                .as_deref()
                .map(parse_bypass_list)
                .unwrap_or_default(),
        }
    }
}

/// Reporter styles exposed to `--reporter`.
///
/// Selects how `mcptest run` renders the finished report to stdout. The
/// `report` subcommand re-renders a saved run (from `--output`) into the same
/// formats plus the file-only ones (markdown, HTML, SARIF, GitLab).
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum Reporter {
    /// Human-friendly colored output, one line per test plus a summary
    /// (default).
    Pretty,
    /// Terse one-line summary (`ran N tests: ...`) on stdout, with a `FAIL`
    /// line per failure on stderr. The legacy default; handy for compact CI
    /// logs.
    Minimal,
    /// The full run report as a single JSON document.
    Json,
    /// JUnit XML, for CI test reporting.
    Junit,
    /// GitHub-flavored Markdown, for PR comments and job summaries.
    Md,
    /// Single-file HTML report.
    Html,
    /// SARIF 2.1.0, for GitHub code scanning.
    Sarif,
    /// GitLab Code Quality JSON, for merge request widgets.
    Gitlab,
    /// Newline-delimited JSON, one record per line, for log pipelines.
    Ndjson,
    /// Test Anything Protocol v14.
    Tap,
    /// Comparison-matrix grid (test x model) as a self-contained HTML file.
    Matrix,
    /// Comparison-matrix grid as GitHub-flavored Markdown.
    MatrixMd,
    /// Compact, model-facing output for a coding agent reading the result in
    /// a loop. Verdict and counts first, failures only, token-budgeted.
    Agent,
    /// OpenInference / OTLP-compatible trace spans as JSONL, for eval and
    /// observability platforms.
    Openinference,
    /// Silent: only the exit code communicates results.
    Quiet,
}

impl Reporter {
    /// Map to the shared [`RenderFormat`](crate::cli::reporters::RenderFormat),
    /// or `None` for the stdout-only presentation modes `quiet` (renders
    /// nothing) and `minimal` (the one-line summary, handled by the caller).
    pub(crate) fn render_format(self) -> Option<crate::cli::reporters::RenderFormat> {
        use crate::cli::reporters::RenderFormat;
        Some(match self {
            Reporter::Pretty => RenderFormat::Pretty,
            Reporter::Minimal => return None,
            Reporter::Json => RenderFormat::Json,
            Reporter::Junit => RenderFormat::Junit,
            Reporter::Md => RenderFormat::Md,
            Reporter::Html => RenderFormat::Html,
            Reporter::Sarif => RenderFormat::Sarif,
            Reporter::Gitlab => RenderFormat::Gitlab,
            Reporter::Ndjson => RenderFormat::Ndjson,
            Reporter::Tap => RenderFormat::Tap,
            Reporter::Matrix => RenderFormat::Matrix,
            Reporter::MatrixMd => RenderFormat::MatrixMd,
            Reporter::Agent => RenderFormat::Agent,
            Reporter::Openinference => RenderFormat::Openinference,
            Reporter::Quiet => return None,
        })
    }
}

/// When to emit GitHub Actions inline annotations (`--annotations`).
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum AnnotationsMode {
    /// Emit only when running inside GitHub Actions (`GITHUB_ACTIONS=true`).
    #[default]
    Auto,
    /// Always emit, regardless of environment.
    Always,
    /// Never emit.
    Never,
}

/// When to colorize human-readable output (`--color`). The clap-facing mirror
/// of [`mcptest_core::style::ColorChoice`].
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum ColorWhen {
    /// Color only when the output is a terminal and `NO_COLOR` is unset.
    #[default]
    Auto,
    /// Always color.
    Always,
    /// Never color.
    Never,
}

/// Format for the stderr diagnostic log (`--log-format`, WOR-1517). `pretty` is
/// human text; `json` emits one JSON object per line for a CI log pipeline.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum LogFormat {
    /// Human-readable text (default).
    #[default]
    Pretty,
    /// One JSON object per log line.
    Json,
}

impl ColorWhen {
    /// Map to the engine-side choice.
    fn to_core(self) -> mcptest_core::style::ColorChoice {
        match self {
            ColorWhen::Auto => mcptest_core::style::ColorChoice::Auto,
            ColorWhen::Always => mcptest_core::style::ColorChoice::Always,
            ColorWhen::Never => mcptest_core::style::ColorChoice::Never,
        }
    }

    /// Resolve to an on/off decision for a sink. `to_terminal` is whether the
    /// sink is a TTY; a file sink passes `false` so file output stays plain
    /// even under `--color always`.
    pub fn resolve(self, to_terminal: bool) -> bool {
        self.to_core().resolve(to_terminal)
    }
}
