use clap::{ArgAction, Args, Parser, Subcommand};
use std::path::PathBuf;

/// Default HTTP API port - "KOI" on a phone keypad (K=5, O=6, I=4).
pub const DEFAULT_HTTP_PORT: u16 = 5641;

/// Windows Named Pipe name for IPC.
#[cfg(windows)]
const WINDOWS_PIPE_NAME: &str = r"\\.\pipe\koi";

/// Unix domain socket filename for IPC.
#[cfg(unix)]
const UNIX_SOCKET_FILENAME: &str = "koi.sock";

/// Unix fallback runtime directory when XDG_RUNTIME_DIR is unset.
#[cfg(unix)]
const UNIX_FALLBACK_RUNTIME_DIR: &str = "/var/run";

/// Validate the `--runtime` / `KOI_RUNTIME` value at parse time.
///
/// Rejects unknown backends with a helpful error instead of letting the daemon
/// silently fall back to auto-detection. The accepted set is the single source
/// of truth in `koi_runtime::RuntimeBackendKind`.
fn parse_runtime_backend(value: &str) -> Result<String, String> {
    match koi_runtime::RuntimeBackendKind::from_str_loose(value) {
        Some(_) => Ok(value.to_string()),
        None => Err(format!(
            "unknown runtime backend '{value}' (expected: {})",
            koi_runtime::RuntimeBackendKind::ACCEPTED.join(", ")
        )),
    }
}

/// Parse a boolean flag / env value leniently.
///
/// Accepts `1`/`true`/`yes`/`on` as true and `0`/`false`/`no`/`off`/`""` as
/// false (case-insensitive); anything else is an error. clap's built-in `bool`
/// parser only accepts literal `true`/`false`, so `KOI_NO_MDNS=1` (the natural
/// `=1` form, and what the Windows-service `from_env` path already accepted)
/// was rejected on the CLI/daemon path. Wiring this as the `value_parser` for
/// every `--no-*`-style flag — and routing `from_env` through it too — makes a
/// single truthy vocabulary work uniformly across both paths.
fn parse_bool_flag(value: &str) -> Result<bool, String> {
    match value.trim().to_ascii_lowercase().as_str() {
        "1" | "true" | "yes" | "on" => Ok(true),
        "0" | "false" | "no" | "off" | "" => Ok(false),
        other => Err(format!(
            "expected a boolean (1/true/yes/on or 0/false/no/off), got '{other}'"
        )),
    }
}

/// Read a boolean env var for the Windows-service `from_env` path.
///
/// Parses with [`parse_bool_flag`]; an **unrecognized** value is logged and
/// treated as false. The CLI/daemon path fails closed on a bad value (clap
/// errors and the daemon refuses to start); the service path fails open so a
/// typo can't block startup — but it must be observable, so we `warn!` rather
/// than swallow it silently. Unset → false.
#[cfg(windows)]
fn env_bool(name: &str) -> bool {
    match std::env::var(name) {
        Ok(s) => parse_bool_flag(&s).unwrap_or_else(|e| {
            tracing::warn!(var = name, value = %s, "ignoring {name}: {e}; treating as false");
            false
        }),
        Err(_) => false,
    }
}

#[derive(Parser, Debug)]
#[command(name = "koi", version, about = "Local service discovery for everyone")]
pub struct Cli {
    /// Run in daemon mode (HTTP + IPC adapters)
    #[arg(long)]
    pub daemon: bool,

    /// HTTP API port
    #[arg(long, env = "KOI_PORT", default_value = "5641")]
    pub port: u16,

    /// HTTP API bind address: loopback (default), bridge, <ip>, or 0.0.0.0.
    /// Non-loopback exposes the daemon to other hosts/containers; mutations
    /// still require the daemon token. See `docs/reference/security-model.md`.
    #[arg(long, env = "KOI_HTTP_BIND", default_value = "loopback")]
    pub http_bind: String,

    /// mTLS port for certmesh inter-node traffic
    #[arg(long, env = "KOI_MTLS_PORT", default_value = "5642")]
    pub mtls_port: u16,

    /// IPC socket/pipe path (default: platform-specific)
    #[arg(long, env = "KOI_PIPE")]
    pub pipe: Option<PathBuf>,

    /// Log level (error, warn, info, debug, trace)
    #[arg(long, env = "KOI_LOG", default_value = "info")]
    pub log_level: String,

    /// Increase verbosity (-v = debug, -vv = trace)
    #[arg(short, long, action = ArgAction::Count, global = true)]
    pub verbose: u8,

    /// Write logs to file (in addition to stderr)
    #[arg(long, env = "KOI_LOG_FILE", value_name = "PATH", global = true)]
    pub log_file: Option<PathBuf>,

    /// Disable the HTTP adapter
    #[arg(long, env = "KOI_NO_HTTP", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_http: bool,

    /// Disable the IPC adapter
    #[arg(long, env = "KOI_NO_IPC", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_ipc: bool,

    /// Disable the mDNS capability
    #[arg(long, env = "KOI_NO_MDNS", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_mdns: bool,

    /// Disable the certmesh capability
    #[arg(long, env = "KOI_NO_CERTMESH", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_certmesh: bool,

    /// Disable the DNS capability
    #[arg(long, env = "KOI_NO_DNS", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_dns: bool,

    /// Disable the health capability
    #[arg(long, env = "KOI_NO_HEALTH", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_health: bool,

    /// Disable the proxy capability
    #[arg(long, env = "KOI_NO_PROXY", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_proxy: bool,

    /// Disable the UDP bridging capability
    #[arg(long, env = "KOI_NO_UDP", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_udp: bool,

    /// Disable the runtime adapter capability
    #[arg(long, env = "KOI_NO_RUNTIME", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_runtime: bool,

    /// Disable the ACME (RFC 8555) server capability
    #[arg(long, env = "KOI_NO_ACME", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_acme: bool,

    /// Disable the in-process MCP HTTP transport (/v1/mcp)
    #[arg(long, env = "KOI_NO_MCP_HTTP", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub no_mcp_http: bool,

    /// Port for the ACME (RFC 8555) server-auth TLS listener
    #[arg(long, env = "KOI_ACME_PORT", default_value = "5643")]
    pub acme_port: u16,

    /// Runtime backend to use (auto, docker, podman)
    #[arg(long, env = "KOI_RUNTIME", default_value = "auto", value_parser = parse_runtime_backend)]
    pub runtime: String,

    /// DNS port for the local resolver
    #[arg(long, env = "KOI_DNS_PORT", default_value = "53")]
    pub dns_port: u16,

    /// DNS zone suffix for local records
    #[arg(long, env = "KOI_DNS_ZONE", default_value = "internal")]
    pub dns_zone: String,

    /// Allow DNS queries from non-private addresses
    #[arg(long, env = "KOI_DNS_PUBLIC", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub dns_public: bool,

    /// Max DNS queries per second, per client IP (mirrors koi_dns DEFAULT_MAX_QPS)
    #[arg(long, env = "KOI_DNS_QPS", default_value = "200")]
    pub dns_qps: u32,

    /// Announce the HTTP server on the local network via mDNS (_http._tcp)
    #[arg(long, env = "KOI_ANNOUNCE_HTTP", action = ArgAction::SetTrue, value_parser = parse_bool_flag)]
    pub announce_http: bool,

    /// Output JSON instead of human-readable text
    #[arg(long, global = true)]
    pub json: bool,

    /// Auto-exit after N seconds (0 = run forever)
    #[arg(long, global = true, value_name = "SECONDS")]
    pub timeout: Option<u64>,

    /// Daemon endpoint for client/admin mode (e.g. "http://localhost:5641")
    #[arg(long, env = "KOI_ENDPOINT", global = true)]
    pub endpoint: Option<String>,

    /// Daemon access token for an explicit --endpoint (overrides KOI_TOKEN)
    #[arg(long, env = "KOI_TOKEN", global = true)]
    pub token: Option<String>,

    /// Force standalone mode (skip daemon detection)
    #[arg(long, global = true)]
    pub standalone: bool,

    /// Skip confirmation prompts for destructive commands
    #[arg(long, global = true)]
    pub yes: bool,

    #[command(subcommand)]
    pub command: Option<Command>,
}

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Install Koi as a system service
    Install,
    /// Uninstall the Koi system service
    Uninstall,
    /// Show version information
    Version,
    /// Open the dashboard in a web browser
    Launch,
    /// Show status of all capabilities
    Status,
    /// mDNS service discovery
    Mdns(MdnsCommand),
    /// Certificate mesh (private CA, enrollment, trust)
    Certmesh(CertmeshCommand),
    /// Local DNS resolver
    Dns(DnsCommand),
    /// Network health monitoring
    Health(HealthCommand),
    /// TLS-terminating reverse proxy
    Proxy(ProxyCommand),
    /// UDP datagram bridging
    Udp(UdpCommand),
    /// OS trust store — install/remove/list CA roots, export the certmesh root
    Trust(TrustCommand),
    /// Model Context Protocol server for AI agents
    Mcp(McpCommand),
    /// Manage the daemon access token (show, write to a file for containers)
    Token(TokenCommand),
    /// Destroy all Koi data and start fresh
    #[command(name = "factory-reset")]
    FactoryReset,
}

#[derive(Args, Debug)]
pub struct TokenCommand {
    #[command(subcommand)]
    pub command: Option<TokenSubcommand>,
}

#[derive(Subcommand, Debug)]
pub enum TokenSubcommand {
    /// Print the current daemon token (refuses non-tty output unless --force)
    Show {
        /// Print even when stdout is not a TTY (e.g. piping into a file)
        #[arg(long)]
        force: bool,
    },
    /// Write the daemon token to a 0600 file for mounting into containers
    Write {
        /// Destination path (created with 0600 permissions)
        path: PathBuf,
    },
}

#[derive(Args, Debug)]
pub struct MdnsCommand {
    #[command(subcommand)]
    pub command: Option<MdnsSubcommand>,
}

#[derive(Subcommand, Debug)]
pub enum MdnsSubcommand {
    /// Discover services on the local network
    Discover {
        /// Service type (e.g. "http", "_http._tcp"). Omit to discover all types.
        service_type: Option<String>,
    },
    /// Announce (register) a service on the local network
    Announce {
        /// Service instance name
        name: String,
        /// Service type (e.g. "http", "_http._tcp")
        service_type: String,
        /// Port number
        port: u16,
        /// Pin the A record to a specific IP (default: advertise all)
        #[arg(long)]
        ip: Option<String>,
        /// TXT record entries as KEY=VALUE pairs
        #[arg(trailing_var_arg = true)]
        txt: Vec<String>,
    },
    /// Unregister a previously registered service by its ID
    Unregister {
        /// Registration ID returned by the announce command
        id: String,
    },
    /// Resolve a specific service instance by its full name
    Resolve {
        /// Full instance name (e.g. "My Server._http._tcp.local.")
        instance: String,
    },
    /// Subscribe to lifecycle events for a service type
    Subscribe {
        /// Service type (e.g. "http", "_http._tcp")
        service_type: String,
    },
    /// Admin operations on a running daemon
    Admin(MdnsAdminCommand),
}

#[derive(Args, Debug)]
pub struct MdnsAdminCommand {
    #[command(subcommand)]
    pub command: Option<AdminSubcommand>,
}

#[derive(Subcommand, Debug)]
pub enum AdminSubcommand {
    /// Show daemon status
    Status,
    /// List all registrations
    #[command(name = "ls")]
    List,
    /// Inspect a registration by ID or prefix
    Inspect {
        /// Registration ID or prefix
        id: String,
    },
    /// Force-unregister a registration
    Unregister {
        /// Registration ID or prefix
        id: String,
    },
    /// Force-drain a registration
    Drain {
        /// Registration ID or prefix
        id: String,
    },
    /// Revive a draining registration
    Revive {
        /// Registration ID or prefix
        id: String,
    },
}

#[derive(Args, Debug)]
pub struct CertmeshCommand {
    #[command(subcommand)]
    pub command: Option<CertmeshSubcommand>,
}

#[derive(Args, Debug)]
pub struct DnsCommand {
    #[command(subcommand)]
    pub command: Option<DnsSubcommand>,
}

#[derive(Args, Debug)]
pub struct HealthCommand {
    #[command(subcommand)]
    pub command: Option<HealthSubcommand>,
}

#[derive(Args, Debug)]
pub struct ProxyCommand {
    #[command(subcommand)]
    pub command: Option<ProxySubcommand>,
}

#[derive(Args, Debug)]
pub struct UdpCommand {
    #[command(subcommand)]
    pub command: Option<UdpSubcommand>,
}

#[derive(Args, Debug)]
pub struct TrustCommand {
    #[command(subcommand)]
    pub command: Option<TrustSubcommand>,
}

#[derive(Subcommand, Debug)]
pub enum TrustSubcommand {
    /// Install a CA certificate (PEM) into the OS trust store
    Install {
        /// Path to a PEM-encoded CA certificate
        pem_path: PathBuf,
    },
    /// List the CA roots Koi installed
    List,
    /// Remove a Koi-installed CA root by name
    Remove {
        /// The name shown by `koi trust list`
        name: String,
    },
    /// Export a CA certificate (PEM) to stdout
    Export {
        /// Export the certmesh root CA (for ACME bootstrap recipes)
        #[arg(long)]
        ca: bool,
    },
    /// Run the trust-doctor: posture, identity & renewal health, integrity,
    /// revocation, and CA trust-install — each with an exact remedy. Exits
    /// non-zero when anything is RED.
    Diagnose {
        /// Repair what can be auto-fixed (install the mesh CA into the OS trust store)
        #[arg(long)]
        fix: bool,
    },
}

#[derive(Args, Debug)]
pub struct McpCommand {
    #[command(subcommand)]
    pub command: Option<McpSubcommand>,
}

#[derive(Subcommand, Debug)]
pub enum McpSubcommand {
    /// Serve the MCP protocol over stdio (for AI agent hosts)
    Serve,
}

#[derive(Subcommand, Debug)]
pub enum UdpSubcommand {
    /// Bind a host UDP port
    Bind {
        /// Port to bind (0 = OS-assigned)
        #[arg(long, default_value = "0")]
        port: u16,
        /// Bind address (loopback by default; a non-loopback bind needs --allow-remote)
        #[arg(long, default_value = "127.0.0.1")]
        addr: String,
        /// Lease duration in seconds
        #[arg(long, default_value = "300")]
        lease: u64,
        /// Allow binding on / sending to non-loopback addresses (LAN ingress/egress).
        /// Off by default so a binding cannot be used as an SSRF / egress relay.
        #[arg(long)]
        allow_remote: bool,
    },
    /// Unbind (close) a UDP binding
    Unbind {
        /// Binding ID returned by bind
        id: String,
    },
    /// Send a datagram through a binding
    Send {
        /// Binding ID
        id: String,
        /// Destination address (host:port)
        #[arg(long)]
        dest: String,
        /// Payload (will be base64 encoded)
        payload: String,
    },
    /// Show active bindings
    Status,
    /// Renew a binding's lease
    Heartbeat {
        /// Binding ID
        id: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum DnsSubcommand {
    /// Start the DNS resolver
    Serve,
    /// Stop the DNS resolver
    Stop,
    /// Show DNS resolver status
    Status,
    /// Lookup a name through the resolver
    Lookup {
        /// Name to query
        name: String,
        /// Record type (A, AAAA, ANY)
        #[arg(long, default_value = "A")]
        record_type: String,
    },
    /// Add a static DNS entry
    Add {
        /// Name to resolve
        name: String,
        /// IP address to return
        ip: String,
        /// Optional TTL override
        #[arg(long)]
        ttl: Option<u32>,
    },
    /// Remove a static DNS entry
    Remove {
        /// Name to remove
        name: String,
    },
    /// List all resolvable names
    List,
}

#[derive(Subcommand, Debug)]
pub enum HealthSubcommand {
    /// Show current health status
    Status,
    /// Live terminal watch view
    Watch {
        /// Refresh interval in seconds
        #[arg(long, default_value = "2")]
        interval: u64,
    },
    /// Add a service health check
    Add {
        /// Check name
        name: String,
        /// HTTP URL to check
        #[arg(long)]
        http: Option<String>,
        /// TCP host:port to check
        #[arg(long)]
        tcp: Option<String>,
        /// Check interval in seconds
        #[arg(long, default_value = "30")]
        interval: u64,
        /// Timeout in seconds
        #[arg(long, default_value = "5")]
        timeout: u64,
    },
    /// Remove a service health check
    Remove {
        /// Check name
        name: String,
    },
    /// Show health transition log
    Log,
}

#[derive(Subcommand, Debug)]
pub enum ProxySubcommand {
    /// Add or update a proxy entry
    Add {
        /// Proxy name (also used for cert path lookup)
        name: String,
        /// TLS listen port
        #[arg(long)]
        listen: u16,
        /// Backend TCP endpoint (host:port, or a URL whose host:port is used)
        #[arg(long)]
        backend: String,
        /// Allow a non-loopback backend destination
        #[arg(long)]
        backend_remote: bool,
    },
    /// Remove a proxy entry
    Remove {
        /// Proxy name
        name: String,
    },
    /// Show proxy status
    Status,
    /// List configured proxies
    List,
}

#[derive(Subcommand, Debug)]
pub enum CertmeshSubcommand {
    /// Create a new certificate mesh (initializes CA)
    Create {
        /// Trust profile: just-me, team, organization
        #[arg(long)]
        profile: Option<String>,
        /// Operator name (required for team/organization profiles)
        #[arg(long)]
        operator: Option<String>,
        /// Initial enrollment state override (open|closed)
        #[arg(long)]
        enrollment: Option<String>,
        /// Override whether join requests require operator approval (true|false)
        #[arg(long)]
        require_approval: Option<bool>,
        /// CA passphrase (when provided, skips interactive passphrase step)
        #[arg(long)]
        passphrase: Option<String>,
    },
    /// Join an existing certificate mesh
    Join {
        /// CA endpoint (e.g. "http://ca-host:5641"). Omit to discover via mDNS.
        ///
        /// Field name is `ca_endpoint` (not `endpoint`) on purpose: a positional named
        /// `endpoint` shares clap's arg id with the global `--endpoint`, so the CA address
        /// would leak into `--endpoint` — which `join` then used to resolve its LOCAL
        /// key-custody daemon, misrouting member-csr/member-cert to the remote CA
        /// (ADR-018 Tier 3 found this).
        ca_endpoint: Option<String>,
        /// Single-use invite token (from `certmesh invite`). Skips the TOTP prompt.
        #[arg(long)]
        invite: Option<String>,
    },
    /// Mint a single-use invite token authorizing one host to join
    Invite {
        /// Hostname the invite authorizes (bound at mint time)
        hostname: String,
        /// Invite lifetime in minutes
        #[arg(long, default_value_t = 60)]
        ttl: i64,
    },
    /// Show certificate mesh status
    Status,
    /// Show the audit log
    Log,
    /// Unlock the CA (decrypt key from passphrase)
    Unlock,
    /// Set a post-renewal reload hook for this host
    SetHook {
        /// Shell command to run after certificate renewal
        #[arg(long)]
        reload: String,
    },
    /// Promote a member to standby CA (transfers encrypted CA key)
    Promote {
        /// CA endpoint (e.g. "http://ca-host:5641"). Omit to discover via mDNS.
        /// Named `ca_endpoint` (not `endpoint`) to avoid the global `--endpoint`
        /// clap arg-id collision — see `Join`.
        ca_endpoint: Option<String>,
    },
    // Enrollment toggle
    /// Open the enrollment window
    OpenEnrollment,
    /// Close the enrollment window
    CloseEnrollment,
    /// Rotate the enrollment auth credential
    RotateAuth,
    /// Create an encrypted backup bundle
    Backup {
        /// Path to write the backup bundle
        path: PathBuf,
    },
    /// Restore certmesh state from a backup bundle
    Restore {
        /// Path to the backup bundle
        path: PathBuf,
    },
    /// Revoke a member from the mesh
    Revoke {
        /// Hostname to revoke
        hostname: String,
        /// Optional reason for revocation
        #[arg(long)]
        reason: Option<String>,
    },
    /// Destroy the certificate mesh (removes all CA data, certs, and audit log)
    Destroy,
    /// ACME (RFC 8555) server — let standard ACME clients get certs from the CA
    Acme(AcmeCommand),
}

/// `koi certmesh acme <subcommand>` — the RFC 8555 ACME facade.
#[derive(clap::Args, Debug)]
pub struct AcmeCommand {
    #[command(subcommand)]
    pub command: Option<AcmeSubcommand>,
}

#[derive(Subcommand, Debug)]
pub enum AcmeSubcommand {
    /// Show the ACME directory URL + the client bootstrap recipe
    Enable,
    /// Show ACME server status (directory URL, account/issuance counts)
    Status,
}

/// Resolved configuration used at runtime.
pub struct Config {
    pub http_port: u16,
    pub mtls_port: u16,
    pub acme_port: u16,
    pub pipe_path: PathBuf,
    pub no_http: bool,
    pub no_ipc: bool,
    pub no_mdns: bool,
    pub no_certmesh: bool,
    pub no_dns: bool,
    pub no_health: bool,
    pub no_proxy: bool,
    pub no_udp: bool,
    pub no_runtime: bool,
    pub no_acme: bool,
    pub no_mcp_http: bool,
    pub runtime: String,
    pub announce_http: bool,
    pub dns_port: u16,
    pub dns_zone: String,
    pub dns_public: bool,
    pub dns_qps: u32,
    /// HTTP bind mode: loopback (default), bridge, <ip>, or 0.0.0.0.
    pub http_bind: String,
    /// Machine-scoped data root, resolved once at the daemon composition root
    /// and injected into the domain cores (no ambient re-resolution).
    pub data_dir: PathBuf,
}

impl Config {
    pub fn from_cli(cli: &Cli) -> Self {
        let pipe_path = cli.pipe.clone().unwrap_or_else(default_pipe_path);
        // Composition root: resolve the data dir once (honours KOI_DATA_DIR)
        // and thread it everywhere instead of re-resolving ad hoc.
        #[allow(clippy::disallowed_methods)]
        let data_dir = koi_common::paths::koi_data_dir();
        Self {
            data_dir,
            http_port: cli.port,
            mtls_port: cli.mtls_port,
            acme_port: cli.acme_port,
            pipe_path,
            http_bind: cli.http_bind.clone(),
            no_http: cli.no_http,
            no_ipc: cli.no_ipc,
            no_mdns: cli.no_mdns,
            no_certmesh: cli.no_certmesh,
            no_dns: cli.no_dns,
            no_health: cli.no_health,
            no_proxy: cli.no_proxy,
            no_udp: cli.no_udp,
            no_runtime: cli.no_runtime,
            no_acme: cli.no_acme,
            no_mcp_http: cli.no_mcp_http,
            runtime: cli.runtime.clone(),
            announce_http: cli.announce_http,
            dns_port: cli.dns_port,
            dns_zone: cli.dns_zone.clone(),
            dns_public: cli.dns_public,
            dns_qps: cli.dns_qps,
        }
    }

    /// Returns an error if the named capability is disabled.
    pub fn require_capability(&self, name: &str) -> anyhow::Result<()> {
        let disabled = match name {
            "mdns" => self.no_mdns,
            "certmesh" => self.no_certmesh,
            "dns" => self.no_dns,
            "health" => self.no_health,
            "proxy" => self.no_proxy,
            "udp" => self.no_udp,
            "runtime" => self.no_runtime,
            "acme" => self.no_acme,
            "mcp-http" => self.no_mcp_http,
            _ => false,
        };
        if disabled {
            anyhow::bail!(
                "The '{name}' capability is disabled. \
                 Remove --no-{name} or unset KOI_NO_{} to enable it.",
                name.to_uppercase().replace('-', "_")
            );
        }
        Ok(())
    }

    pub fn dns_config(&self) -> koi_dns::DnsConfig {
        koi_dns::DnsConfig {
            port: self.dns_port,
            zone: self.dns_zone.clone(),
            allow_public_clients: self.dns_public,
            max_qps: self.dns_qps,
            ..Default::default()
        }
    }

    /// Build config from environment variables only.
    /// Used by service mode where CLI args aren't available.
    #[cfg(windows)]
    pub fn from_env() -> Self {
        let http_port = std::env::var("KOI_PORT")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(DEFAULT_HTTP_PORT);

        let mtls_port = std::env::var("KOI_MTLS_PORT")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(koi_serve::mtls::DEFAULT_MTLS_PORT);

        let acme_port = std::env::var("KOI_ACME_PORT")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(koi_serve::acme::DEFAULT_ACME_PORT);

        let pipe_path = std::env::var("KOI_PIPE")
            .ok()
            .map(PathBuf::from)
            .unwrap_or_else(default_pipe_path);

        let no_http = env_bool("KOI_NO_HTTP");
        let no_ipc = env_bool("KOI_NO_IPC");
        let no_mdns = env_bool("KOI_NO_MDNS");
        let no_certmesh = env_bool("KOI_NO_CERTMESH");
        let no_dns = env_bool("KOI_NO_DNS");
        let no_health = env_bool("KOI_NO_HEALTH");
        let no_proxy = env_bool("KOI_NO_PROXY");
        let no_udp = env_bool("KOI_NO_UDP");
        let no_runtime = env_bool("KOI_NO_RUNTIME");
        let no_acme = env_bool("KOI_NO_ACME");
        let no_mcp_http = env_bool("KOI_NO_MCP_HTTP");

        let runtime = std::env::var("KOI_RUNTIME").unwrap_or_else(|_| "auto".to_string());

        let announce_http = env_bool("KOI_ANNOUNCE_HTTP");

        let dns_port = std::env::var("KOI_DNS_PORT")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(53);

        let dns_zone = std::env::var("KOI_DNS_ZONE").unwrap_or_else(|_| "internal".to_string());

        let dns_public = env_bool("KOI_DNS_PUBLIC");

        let dns_qps = std::env::var("KOI_DNS_QPS")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(200);

        let http_bind = std::env::var("KOI_HTTP_BIND").unwrap_or_else(|_| "loopback".to_string());

        // Composition root: resolve the data dir once (honours KOI_DATA_DIR).
        #[allow(clippy::disallowed_methods)]
        let data_dir = koi_common::paths::koi_data_dir();

        Self {
            data_dir,
            http_port,
            mtls_port,
            acme_port,
            pipe_path,
            http_bind,
            no_http,
            no_ipc,
            no_mdns,
            no_certmesh,
            no_dns,
            no_health,
            no_proxy,
            no_udp,
            no_runtime,
            no_acme,
            no_mcp_http,
            runtime,
            announce_http,
            dns_port,
            dns_zone,
            dns_public,
            dns_qps,
        }
    }
}

impl Default for Config {
    fn default() -> Self {
        #[allow(clippy::disallowed_methods)]
        let data_dir = koi_common::paths::koi_data_dir();
        Self {
            data_dir,
            http_port: DEFAULT_HTTP_PORT,
            mtls_port: koi_serve::mtls::DEFAULT_MTLS_PORT,
            acme_port: koi_serve::acme::DEFAULT_ACME_PORT,
            pipe_path: default_pipe_path(),
            http_bind: "loopback".to_string(),
            no_http: false,
            no_ipc: false,
            no_mdns: false,
            no_certmesh: false,
            no_dns: false,
            no_health: false,
            no_proxy: false,
            no_udp: false,
            no_runtime: false,
            no_acme: false,
            no_mcp_http: false,
            runtime: "auto".to_string(),
            announce_http: false,
            dns_port: 53,
            dns_zone: "internal".to_string(),
            dns_public: false,
            dns_qps: 200,
        }
    }
}

// ── Default paths ────────────────────────────────────────────────────

fn default_pipe_path() -> PathBuf {
    #[cfg(windows)]
    {
        PathBuf::from(WINDOWS_PIPE_NAME)
    }
    #[cfg(unix)]
    {
        if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
            PathBuf::from(runtime_dir).join(UNIX_SOCKET_FILENAME)
        } else {
            PathBuf::from(UNIX_FALLBACK_RUNTIME_DIR).join(UNIX_SOCKET_FILENAME)
        }
    }
    #[cfg(not(any(unix, windows)))]
    {
        PathBuf::from(UNIX_SOCKET_FILENAME)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // ── Config::require_capability tests ─────────────────────────────

    #[test]
    fn require_capability_passes_when_enabled() {
        let config = Config::default();
        assert!(config.require_capability("mdns").is_ok());
        assert!(config.require_capability("certmesh").is_ok());
        assert!(config.require_capability("dns").is_ok());
        assert!(config.require_capability("health").is_ok());
        assert!(config.require_capability("proxy").is_ok());
    }

    #[test]
    fn require_capability_fails_when_mdns_disabled() {
        let config = Config {
            no_mdns: true,
            ..Config::default()
        };
        let result = config.require_capability("mdns");
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(
            msg.contains("mdns"),
            "error message should mention 'mdns': {msg}"
        );
        assert!(
            msg.contains("disabled"),
            "error message should mention 'disabled': {msg}"
        );
    }

    #[test]
    fn require_capability_fails_when_certmesh_disabled() {
        let config = Config {
            no_certmesh: true,
            ..Config::default()
        };
        let result = config.require_capability("certmesh");
        assert!(result.is_err());
    }

    #[test]
    fn require_capability_fails_when_dns_disabled() {
        let config = Config {
            no_dns: true,
            ..Config::default()
        };
        let result = config.require_capability("dns");
        assert!(result.is_err());
    }

    #[test]
    fn require_capability_fails_when_health_disabled() {
        let config = Config {
            no_health: true,
            ..Config::default()
        };
        let result = config.require_capability("health");
        assert!(result.is_err());
    }

    #[test]
    fn require_capability_fails_when_proxy_disabled() {
        let config = Config {
            no_proxy: true,
            ..Config::default()
        };
        let result = config.require_capability("proxy");
        assert!(result.is_err());
    }

    #[test]
    fn require_capability_unknown_name_passes() {
        let config = Config::default();
        assert!(config.require_capability("unknown").is_ok());
    }

    // ── Config::default tests ────────────────────────────────────────

    #[test]
    fn config_default_values() {
        let config = Config::default();
        assert_eq!(config.http_port, DEFAULT_HTTP_PORT);
        assert!(!config.no_http);
        assert!(!config.no_ipc);
        assert!(!config.no_mdns);
        assert!(!config.no_certmesh);
        assert!(!config.no_dns);
        assert!(!config.no_health);
        assert!(!config.no_proxy);
        assert_eq!(config.dns_port, 53);
        assert_eq!(config.dns_zone, "internal");
        assert!(!config.dns_public);
        assert_eq!(config.dns_qps, 200);
    }

    #[test]
    fn dns_config_propagates_qps() {
        let config = Config {
            dns_qps: 42,
            ..Config::default()
        };
        assert_eq!(config.dns_config().max_qps, 42);
    }

    #[test]
    fn default_http_port_is_5641() {
        assert_eq!(DEFAULT_HTTP_PORT, 5641);
    }

    // ── CLI parsing tests ───────────────────────────────────────────

    #[test]
    fn parse_certmesh_promote_no_endpoint() {
        let cli = Cli::try_parse_from(["koi", "certmesh", "promote"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::Promote { ca_endpoint }),
            })) => {
                assert!(ca_endpoint.is_none());
            }
            other => panic!("Expected Certmesh Promote, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_promote_with_endpoint() {
        let cli = Cli::try_parse_from(["koi", "certmesh", "promote", "http://ca:5641"]).unwrap();
        // The positional CA endpoint must NOT leak into the global --endpoint (clap
        // arg-id collision — see the join regression below).
        assert!(
            cli.endpoint.is_none(),
            "promote positional must not populate the global --endpoint"
        );
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::Promote { ca_endpoint }),
            })) => {
                assert_eq!(ca_endpoint.as_deref(), Some("http://ca:5641"));
            }
            other => panic!("Expected Certmesh Promote, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_join_positional_is_not_global_endpoint() {
        // Regression (ADR-018 Tier 3): the join CA positional shared clap's "endpoint"
        // arg id with the global `--endpoint`, so it leaked into `--endpoint` — which
        // `join` used to resolve its LOCAL key-custody daemon, misrouting member-csr /
        // member-cert to the remote CA (→ 401). Renaming the positional to `ca_endpoint`
        // breaks the collision; assert the global endpoint stays unset.
        let cli = Cli::try_parse_from([
            "koi",
            "certmesh",
            "join",
            "http://ca-host:5641",
            "--invite",
            "secret.fp",
        ])
        .unwrap();
        assert!(
            cli.endpoint.is_none(),
            "join positional must not populate the global --endpoint"
        );
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command:
                    Some(CertmeshSubcommand::Join {
                        ca_endpoint,
                        invite,
                    }),
            })) => {
                assert_eq!(ca_endpoint.as_deref(), Some("http://ca-host:5641"));
                assert_eq!(invite.as_deref(), Some("secret.fp"));
            }
            other => panic!("Expected Certmesh Join, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_invite_default_ttl() {
        let cli = Cli::try_parse_from(["koi", "certmesh", "invite", "web-01"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::Invite { hostname, ttl }),
            })) => {
                assert_eq!(hostname, "web-01");
                assert_eq!(ttl, 60);
            }
            other => panic!("Expected Certmesh Invite, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_invite_custom_ttl() {
        let cli =
            Cli::try_parse_from(["koi", "certmesh", "invite", "web-01", "--ttl", "15"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::Invite { hostname, ttl }),
            })) => {
                assert_eq!(hostname, "web-01");
                assert_eq!(ttl, 15);
            }
            other => panic!("Expected Certmesh Invite, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_join_with_invite() {
        let cli = Cli::try_parse_from([
            "koi",
            "certmesh",
            "join",
            "http://ca:5641",
            "--invite",
            "deadbeef",
        ])
        .unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command:
                    Some(CertmeshSubcommand::Join {
                        ca_endpoint,
                        invite,
                    }),
            })) => {
                assert_eq!(ca_endpoint.as_deref(), Some("http://ca:5641"));
                assert_eq!(invite.as_deref(), Some("deadbeef"));
            }
            other => panic!("Expected Certmesh Join, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_set_hook() {
        let cli = Cli::try_parse_from([
            "koi",
            "certmesh",
            "set-hook",
            "--reload",
            "systemctl restart nginx",
        ])
        .unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::SetHook { reload }),
            })) => {
                assert_eq!(reload, "systemctl restart nginx");
            }
            other => panic!("Expected Certmesh SetHook, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_create_with_all_options() {
        let cli = Cli::try_parse_from([
            "koi",
            "certmesh",
            "create",
            "--profile",
            "team",
            "--operator",
            "ops",
            "--enrollment",
            "open",
            "--require-approval",
            "true",
            "--passphrase",
            "my-secret",
        ])
        .unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command:
                    Some(CertmeshSubcommand::Create {
                        profile,
                        operator,
                        enrollment,
                        require_approval,
                        passphrase,
                    }),
            })) => {
                assert_eq!(profile.as_deref(), Some("team"));
                assert_eq!(operator.as_deref(), Some("ops"));
                assert_eq!(enrollment.as_deref(), Some("open"));
                assert_eq!(require_approval, Some(true));
                assert_eq!(passphrase.as_deref(), Some("my-secret"));
            }
            other => panic!("Expected Certmesh Create, got: {other:?}"),
        }
    }

    #[test]
    fn parse_global_json_flag() {
        let cli = Cli::try_parse_from(["koi", "--json", "certmesh", "status"]).unwrap();
        assert!(cli.json);
    }

    #[test]
    fn parse_global_endpoint_flag() {
        let cli = Cli::try_parse_from([
            "koi",
            "--endpoint",
            "http://localhost:5641",
            "certmesh",
            "status",
        ])
        .unwrap();
        assert_eq!(cli.endpoint.as_deref(), Some("http://localhost:5641"));
    }

    // ── mDNS CLI parsing tests ──────────────────────────────────────

    #[test]
    fn parse_mdns_discover_no_type() {
        let cli = Cli::try_parse_from(["koi", "mdns", "discover"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command: Some(MdnsSubcommand::Discover { service_type }),
            })) => {
                assert!(service_type.is_none());
            }
            other => panic!("Expected Mdns Discover, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_discover_with_type() {
        let cli = Cli::try_parse_from(["koi", "mdns", "discover", "_http._tcp"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command: Some(MdnsSubcommand::Discover { service_type }),
            })) => {
                assert_eq!(service_type.as_deref(), Some("_http._tcp"));
            }
            other => panic!("Expected Mdns Discover, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_announce_all_args() {
        let cli = Cli::try_parse_from([
            "koi",
            "mdns",
            "announce",
            "My App",
            "_http._tcp",
            "8080",
            "--ip",
            "10.0.0.1",
            "version=1.0",
            "env=prod",
        ])
        .unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Announce {
                        name,
                        service_type,
                        port,
                        ip,
                        txt,
                    }),
            })) => {
                assert_eq!(name, "My App");
                assert_eq!(service_type, "_http._tcp");
                assert_eq!(port, 8080);
                assert_eq!(ip.as_deref(), Some("10.0.0.1"));
                assert_eq!(txt, vec!["version=1.0", "env=prod"]);
            }
            other => panic!("Expected Mdns Announce, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_announce_minimal() {
        let cli =
            Cli::try_parse_from(["koi", "mdns", "announce", "Svc", "_ssh._tcp", "22"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Announce {
                        name,
                        service_type,
                        port,
                        ip,
                        txt,
                    }),
            })) => {
                assert_eq!(name, "Svc");
                assert_eq!(service_type, "_ssh._tcp");
                assert_eq!(port, 22);
                assert!(ip.is_none());
                assert!(txt.is_empty());
            }
            other => panic!("Expected Mdns Announce, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_unregister() {
        let cli = Cli::try_parse_from(["koi", "mdns", "unregister", "abc12345"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command: Some(MdnsSubcommand::Unregister { id }),
            })) => {
                assert_eq!(id, "abc12345");
            }
            other => panic!("Expected Mdns Unregister, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_resolve() {
        let cli =
            Cli::try_parse_from(["koi", "mdns", "resolve", "My Server._http._tcp.local."]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command: Some(MdnsSubcommand::Resolve { instance }),
            })) => {
                assert_eq!(instance, "My Server._http._tcp.local.");
            }
            other => panic!("Expected Mdns Resolve, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_subscribe() {
        let cli = Cli::try_parse_from(["koi", "mdns", "subscribe", "_http._tcp"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command: Some(MdnsSubcommand::Subscribe { service_type }),
            })) => {
                assert_eq!(service_type, "_http._tcp");
            }
            other => panic!("Expected Mdns Subscribe, got: {other:?}"),
        }
    }

    // ── Admin subcommand parsing ────────────────────────────────────

    #[test]
    fn parse_mdns_admin_status() {
        let cli = Cli::try_parse_from(["koi", "mdns", "admin", "status"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Admin(MdnsAdminCommand {
                        command: Some(AdminSubcommand::Status),
                    })),
            })) => {}
            other => panic!("Expected Admin Status, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_admin_ls() {
        let cli = Cli::try_parse_from(["koi", "mdns", "admin", "ls"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Admin(MdnsAdminCommand {
                        command: Some(AdminSubcommand::List),
                    })),
            })) => {}
            other => panic!("Expected Admin List, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_admin_inspect() {
        let cli = Cli::try_parse_from(["koi", "mdns", "admin", "inspect", "a1b2c3"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Admin(MdnsAdminCommand {
                        command: Some(AdminSubcommand::Inspect { id }),
                    })),
            })) => {
                assert_eq!(id, "a1b2c3");
            }
            other => panic!("Expected Admin Inspect, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_admin_unregister() {
        let cli = Cli::try_parse_from(["koi", "mdns", "admin", "unregister", "xyz"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Admin(MdnsAdminCommand {
                        command: Some(AdminSubcommand::Unregister { id }),
                    })),
            })) => {
                assert_eq!(id, "xyz");
            }
            other => panic!("Expected Admin Unregister, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_admin_drain() {
        let cli = Cli::try_parse_from(["koi", "mdns", "admin", "drain", "abc"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Admin(MdnsAdminCommand {
                        command: Some(AdminSubcommand::Drain { id }),
                    })),
            })) => {
                assert_eq!(id, "abc");
            }
            other => panic!("Expected Admin Drain, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_admin_revive() {
        let cli = Cli::try_parse_from(["koi", "mdns", "admin", "revive", "def"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command:
                    Some(MdnsSubcommand::Admin(MdnsAdminCommand {
                        command: Some(AdminSubcommand::Revive { id }),
                    })),
            })) => {
                assert_eq!(id, "def");
            }
            other => panic!("Expected Admin Revive, got: {other:?}"),
        }
    }

    // ── Global flags with mDNS ──────────────────────────────────────

    #[test]
    fn parse_mdns_with_timeout() {
        let cli = Cli::try_parse_from(["koi", "--timeout", "30", "mdns", "discover"]).unwrap();
        assert_eq!(cli.timeout, Some(30));
    }

    #[test]
    fn parse_mdns_with_json_flag() {
        let cli =
            Cli::try_parse_from(["koi", "--json", "mdns", "subscribe", "_http._tcp"]).unwrap();
        assert!(cli.json);
    }

    #[test]
    fn parse_mdns_with_verbose() {
        let cli = Cli::try_parse_from(["koi", "-vv", "mdns", "discover"]).unwrap();
        assert_eq!(cli.verbose, 2);
    }

    // ── Top-level commands ──────────────────────────────────────────

    #[test]
    fn parse_daemon_flag() {
        let cli = Cli::try_parse_from(["koi", "--daemon"]).unwrap();
        assert!(cli.daemon);
    }

    #[test]
    fn parse_no_subcommand() {
        let cli = Cli::try_parse_from(["koi"]).unwrap();
        assert!(cli.command.is_none());
    }

    #[test]
    fn parse_certmesh_no_subcommand() {
        let cli = Cli::try_parse_from(["koi", "certmesh"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand { command: None })) => {}
            other => panic!("Expected Certmesh without subcommand, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_no_subcommand() {
        let cli = Cli::try_parse_from(["koi", "mdns"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand { command: None })) => {}
            other => panic!("Expected Mdns without subcommand, got: {other:?}"),
        }
    }

    #[test]
    fn parse_mdns_admin_no_subcommand() {
        let cli = Cli::try_parse_from(["koi", "mdns", "admin"]).unwrap();
        match cli.command {
            Some(Command::Mdns(MdnsCommand {
                command: Some(MdnsSubcommand::Admin(MdnsAdminCommand { command: None })),
            })) => {}
            other => panic!("Expected Mdns admin without subcommand, got: {other:?}"),
        }
    }

    #[test]
    fn parse_version_subcommand() {
        let cli = Cli::try_parse_from(["koi", "version"]).unwrap();
        assert!(matches!(cli.command, Some(Command::Version)));
    }

    #[test]
    fn parse_status_subcommand() {
        let cli = Cli::try_parse_from(["koi", "status"]).unwrap();
        assert!(matches!(cli.command, Some(Command::Status)));
    }

    #[test]
    fn parse_install_subcommand() {
        let cli = Cli::try_parse_from(["koi", "install"]).unwrap();
        assert!(matches!(cli.command, Some(Command::Install)));
    }

    #[test]
    fn parse_standalone_flag() {
        let cli = Cli::try_parse_from(["koi", "--standalone", "mdns", "discover"]).unwrap();
        assert!(cli.standalone);
    }

    // ── require_capability edge cases ───────────────────────────────

    #[test]
    fn require_capability_error_message_includes_env_var_hint() {
        let config = Config {
            no_mdns: true,
            ..Config::default()
        };
        let msg = config.require_capability("mdns").unwrap_err().to_string();
        assert!(
            msg.contains("KOI_NO_MDNS"),
            "error should mention env var: {msg}"
        );
    }

    #[test]
    fn require_capability_mdns_enabled_certmesh_disabled_mdns_works() {
        let config = Config {
            no_certmesh: true,
            ..Config::default()
        };
        assert!(config.require_capability("mdns").is_ok());
        assert!(config.require_capability("certmesh").is_err());
    }

    #[test]
    fn config_default_pipe_path_is_not_empty() {
        let config = Config::default();
        assert!(
            config.pipe_path.components().count() > 0,
            "pipe path should not be empty"
        );
    }

    // ── Enrollment toggle CLI parsing ───────────────────────────────

    #[test]
    fn parse_certmesh_open_enrollment() {
        let cli = Cli::try_parse_from(["koi", "certmesh", "open-enrollment"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::OpenEnrollment),
            })) => {}
            other => panic!("Expected OpenEnrollment, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_close_enrollment() {
        let cli = Cli::try_parse_from(["koi", "certmesh", "close-enrollment"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::CloseEnrollment),
            })) => {}
            other => panic!("Expected CloseEnrollment, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_rotate_auth() {
        let cli = Cli::try_parse_from(["koi", "certmesh", "rotate-auth"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::RotateAuth),
            })) => {}
            other => panic!("Expected RotateAuth, got: {other:?}"),
        }
    }

    #[test]
    fn parse_certmesh_destroy() {
        let cli = Cli::try_parse_from(["koi", "certmesh", "destroy"]).unwrap();
        match cli.command {
            Some(Command::Certmesh(CertmeshCommand {
                command: Some(CertmeshSubcommand::Destroy),
            })) => {}
            other => panic!("Expected Destroy, got: {other:?}"),
        }
    }

    #[test]
    fn test_msrv_drift_guard() {
        use std::path::PathBuf;
        let cargo_toml_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap()
            .parent()
            .unwrap()
            .join("Cargo.toml");
        let cargo_contents =
            std::fs::read_to_string(&cargo_toml_path).expect("failed to read Cargo.toml");
        let rust_version = cargo_contents
            .lines()
            .find_map(|line| {
                let trimmed = line.trim();
                if trimmed.starts_with("rust-version") {
                    let parts: Vec<&str> = trimmed.split('=').collect();
                    if parts.len() == 2 {
                        let ver = parts[1]
                            .trim()
                            .trim_matches('"')
                            .trim_matches('\'')
                            .to_string();
                        return Some(ver);
                    }
                }
                None
            })
            .expect("Could not find rust-version in Cargo.toml");

        let readme_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap()
            .parent()
            .unwrap()
            .join("README.md");
        let readme_contents =
            std::fs::read_to_string(&readme_path).expect("failed to read README.md");
        let readme_version = readme_contents
            .lines()
            .find_map(|line| {
                if line.contains("requires [Rust](https://rustup.rs/)") {
                    let prefix = "requires [Rust](https://rustup.rs/) ";
                    if let Some(idx) = line.find(prefix) {
                        let start = idx + prefix.len();
                        let rest = &line[start..];
                        let ver = rest.split_whitespace().next().unwrap_or("");
                        return Some(ver.to_string());
                    }
                }
                None
            })
            .expect("Could not find Rust MSRV reference in README.md");

        assert_eq!(
            rust_version, readme_version,
            "MSRV in Cargo.toml ({}) does not match README.md ({})",
            rust_version, readme_version
        );
    }

    // ── Boolean flag / env parsing (parse_bool_flag) ────────────────
    //
    // parse_bool_flag is the single source of truth shared by BOTH the clap
    // `value_parser` (the CLI/daemon path, via `from_cli`) and the Windows
    // service `from_env` path. Testing it directly is the conformance check
    // for both: env vars are process-global so driving `KOI_NO_*` through
    // `Cli::try_parse_from` would race other tests in this binary that also
    // read those vars — so the shared parser carries the coverage instead.

    #[test]
    fn parse_bool_flag_accepts_truthy_set() {
        for v in [
            "1", "true", "TRUE", "True", "yes", "YES", "on", "ON", " on ",
        ] {
            assert_eq!(parse_bool_flag(v), Ok(true), "{v:?} should be true");
        }
    }

    #[test]
    fn parse_bool_flag_accepts_falsy_set() {
        for v in ["0", "false", "FALSE", "no", "NO", "off", "OFF", "", "  "] {
            assert_eq!(parse_bool_flag(v), Ok(false), "{v:?} should be false");
        }
    }

    #[test]
    fn parse_bool_flag_rejects_garbage() {
        assert!(parse_bool_flag("banana").is_err());
        assert!(parse_bool_flag("2").is_err());
        assert!(parse_bool_flag("truthy").is_err());
    }

    #[test]
    fn no_capability_flag_form_still_works() {
        // The flag form stays a pure presence flag (SetTrue, no value): `--no-http`
        // must parse without an argument and not consume the following subcommand.
        let cli = Cli::try_parse_from(["koi", "--no-http", "mdns", "discover"]).unwrap();
        assert!(cli.no_http);
        assert!(!cli.no_ipc);
        assert!(matches!(cli.command, Some(Command::Mdns(_))));
    }

    #[test]
    fn capability_flags_default_off() {
        let cli = Cli::try_parse_from(["koi"]).unwrap();
        let cfg = Config::from_cli(&cli);
        assert!(!cfg.no_http);
        assert!(!cfg.no_mdns);
        assert!(!cfg.no_certmesh);
        assert!(!cfg.no_mcp_http);
        assert!(!cfg.announce_http);
        assert!(!cfg.dns_public);
    }
}
