use chrono::Utc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri_plugin_shell::ShellExt;

use crate::browser::ProxySettings;
use crate::events;
use crate::ip_utils;

// Export data format for JSON export
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyExportData {
  pub version: String,
  pub proxies: Vec<ExportedProxy>,
  pub exported_at: String,
  pub source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportedProxy {
  pub name: String,
  #[serde(rename = "type")]
  pub proxy_type: String,
  pub host: String,
  pub port: u16,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub username: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub password: Option<String>,
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub vless_uri: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyImportResult {
  pub imported_count: usize,
  pub skipped_count: usize,
  pub errors: Vec<String>,
  pub proxies: Vec<StoredProxy>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedProxyLine {
  pub proxy_type: String,
  pub host: String,
  pub port: u16,
  pub username: Option<String>,
  pub password: Option<String>,
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub vless_uri: Option<String>,
  pub original_line: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum ProxyParseResult {
  #[serde(rename = "parsed")]
  Parsed(ParsedProxyLine),
  #[serde(rename = "ambiguous")]
  Ambiguous {
    line: String,
    possible_formats: Vec<String>,
  },
  #[serde(rename = "invalid")]
  Invalid { line: String, reason: String },
}

// Store active proxy information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyInfo {
  pub id: String,
  pub local_url: String,
  pub upstream_host: String,
  pub upstream_port: u16,
  pub upstream_type: String,
  pub local_port: u16,
  // Optional profile ID to which this proxy instance is logically tied
  pub profile_id: Option<String>,
  pub blocklist_file: Option<String>,
}

// Proxy check result cache
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyCheckResult {
  pub ip: String,
  pub city: Option<String>,
  pub country: Option<String>,
  pub country_code: Option<String>,
  pub timestamp: u64,
  pub is_valid: bool,
}

pub const CLOUD_PROXY_ID: &str = "cloud-included-proxy";

// Stored proxy configuration with name and ID for reuse
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredProxy {
  pub id: String,
  pub name: String,
  pub proxy_settings: ProxySettings,
  #[serde(default)]
  pub sync_enabled: bool,
  #[serde(default)]
  pub last_sync: Option<u64>,
  /// Unix seconds of the last meaningful user edit. Source of truth for sync
  /// conflict resolution (last-write-wins) — bumped on config edits only, never
  /// by sync bookkeeping. `None` on legacy files is treated as 0.
  #[serde(default)]
  pub updated_at: Option<u64>,
  #[serde(default)]
  pub is_cloud_managed: bool,
  #[serde(default)]
  pub is_cloud_derived: bool,
  #[serde(default)]
  pub geo_country: Option<String>,
  // Legacy field kept for deserialization compat; mapped to geo_region on load
  #[serde(default)]
  pub geo_state: Option<String>,
  #[serde(default)]
  pub geo_region: Option<String>,
  #[serde(default)]
  pub geo_city: Option<String>,
  #[serde(default)]
  pub geo_isp: Option<String>,
  #[serde(default)]
  pub dynamic_proxy_url: Option<String>,
  #[serde(default)]
  pub dynamic_proxy_format: Option<String>,
}

/// Current unix time in whole seconds. Used to stamp `updated_at` on edits.
pub fn now_secs() -> u64 {
  SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .unwrap_or_default()
    .as_secs()
}

/// Keys in `active_proxies` at or above this value are in-flight launch
/// placeholders, not real browser PIDs. Real OS PIDs never reach this range
/// (Linux pid_max caps at 2^22, macOS at ~100k, Windows PIDs are small DWORD
/// multiples of 4), so a placeholder can never shadow a live process ID.
pub const LAUNCH_PLACEHOLDER_PID_MIN: u32 = u32::MAX - 0x00FF_FFFF;

static NEXT_LAUNCH_PLACEHOLDER_PID: std::sync::atomic::AtomicU32 =
  std::sync::atomic::AtomicU32::new(u32::MAX);

/// Allocate a unique `active_proxies` key for a launch in flight, so concurrent
/// launches can never overwrite each other's placeholder entry.
pub fn next_launch_placeholder_pid() -> u32 {
  NEXT_LAUNCH_PLACEHOLDER_PID.fetch_sub(1, std::sync::atomic::Ordering::Relaxed)
}

pub fn is_launch_placeholder_pid(pid: u32) -> bool {
  pid >= LAUNCH_PLACEHOLDER_PID_MIN
}

/// Write the exact browser process identity (PID plus its start time) onto a
/// worker's on-disk config. Both halves matter: the PID says which process to
/// watch, the start time pins it to THAT process so a recycled PID can never
/// read as "my browser is still alive".
///
/// Rejects launch placeholders and 0 — neither is a real browser — so a caller
/// can treat `false` as "this worker has no verified owner" and abort rather
/// than leave a worker nothing will reap.
pub(crate) fn persist_browser_identity(proxy_id: &str, browser_pid: u32) -> bool {
  if browser_pid == 0 || is_launch_placeholder_pid(browser_pid) {
    return false;
  }
  let Some(mut cfg) = crate::proxy_storage::get_proxy_config(proxy_id) else {
    return false;
  };
  let Some(start_time) = crate::proxy_storage::resolve_process_start_time(browser_pid) else {
    log::warn!("Could not resolve start time for browser PID {browser_pid} (proxy {proxy_id})");
    return false;
  };

  cfg.browser_pid = Some(browser_pid);
  cfg.browser_pid_start_time = Some(start_time);
  if crate::proxy_storage::update_proxy_config(&cfg) {
    log::info!("Recorded browser PID {browser_pid} on proxy config {proxy_id} for self-reaping");
    true
  } else {
    log::warn!("Failed to persist browser_pid {browser_pid} to proxy config {proxy_id}");
    false
  }
}

impl StoredProxy {
  pub fn new(name: String, proxy_settings: ProxySettings) -> Self {
    let sync_enabled = crate::sync::is_sync_configured();
    Self {
      id: uuid::Uuid::new_v4().to_string(),
      name,
      proxy_settings,
      sync_enabled,
      last_sync: None,
      updated_at: Some(now_secs()),
      is_cloud_managed: false,
      is_cloud_derived: false,
      geo_country: None,
      geo_state: None,
      geo_region: None,
      geo_city: None,
      geo_isp: None,
      dynamic_proxy_url: None,
      dynamic_proxy_format: None,
    }
  }

  /// Migrate legacy geo_state to geo_region
  pub fn migrate_geo_fields(&mut self) {
    if self.geo_region.is_none() && self.geo_state.is_some() {
      self.geo_region = self.geo_state.take();
    }
  }

  /// Get the effective region (prefers geo_region, falls back to geo_state for compat)
  pub fn effective_region(&self) -> Option<&String> {
    self.geo_region.as_ref().or(self.geo_state.as_ref())
  }

  pub fn update_settings(&mut self, proxy_settings: ProxySettings) {
    self.proxy_settings = proxy_settings;
    self.updated_at = Some(now_secs());
  }

  pub fn update_name(&mut self, name: String) {
    self.name = name;
    self.updated_at = Some(now_secs());
  }
}

// Global proxy manager to track active proxies and stored proxy configurations
pub struct ProxyManager {
  active_proxies: Mutex<HashMap<u32, ProxyInfo>>, // Maps browser process ID to proxy info
  // Store proxy info by profile name for persistence across browser restarts
  profile_proxies: Mutex<HashMap<String, ProxySettings>>, // Maps profile name to proxy settings
  // Track active proxy IDs by profile name for targeted cleanup
  profile_active_proxy_ids: Mutex<HashMap<String, String>>, // Maps profile name to proxy id
  stored_proxies: Mutex<HashMap<String, StoredProxy>>,      // Maps proxy ID to stored proxy
  // Consecutive cleanup passes during which a browser PID looked dead.
  // We only reap a worker after it has been missed in N consecutive scans —
  // a single sysinfo blip under load shouldn't kill a still-running worker.
  dead_browser_misses: Mutex<HashMap<u32, u8>>,
}

impl ProxyManager {
  pub fn new() -> Self {
    let manager = Self {
      active_proxies: Mutex::new(HashMap::new()),
      profile_proxies: Mutex::new(HashMap::new()),
      profile_active_proxy_ids: Mutex::new(HashMap::new()),
      stored_proxies: Mutex::new(HashMap::new()),
      dead_browser_misses: Mutex::new(HashMap::new()),
    };

    // Load stored proxies on initialization
    if let Err(e) = manager.load_stored_proxies() {
      log::warn!("Failed to load stored proxies: {e}");
    }

    manager
  }

  fn get_proxies_dir(&self) -> PathBuf {
    crate::app_dirs::proxies_dir()
  }

  fn get_proxy_check_cache_dir(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let path = crate::app_dirs::cache_dir().join("proxy_checks");
    fs::create_dir_all(&path)?;
    Ok(path)
  }

  // Get the path to a specific proxy check cache file
  fn get_proxy_check_cache_file(
    &self,
    proxy_id: &str,
  ) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let cache_dir = self.get_proxy_check_cache_dir()?;
    Ok(cache_dir.join(format!("{proxy_id}.json")))
  }

  // Load cached proxy check result
  fn load_proxy_check_cache(&self, proxy_id: &str) -> Option<ProxyCheckResult> {
    let cache_file = match self.get_proxy_check_cache_file(proxy_id) {
      Ok(file) => file,
      Err(_) => return None,
    };

    if !cache_file.exists() {
      return None;
    }

    let content = match fs::read_to_string(&cache_file) {
      Ok(content) => content,
      Err(_) => return None,
    };

    serde_json::from_str::<ProxyCheckResult>(&content).ok()
  }

  // Save proxy check result to cache
  fn save_proxy_check_cache(
    &self,
    proxy_id: &str,
    result: &ProxyCheckResult,
  ) -> Result<(), Box<dyn std::error::Error>> {
    let cache_file = self.get_proxy_check_cache_file(proxy_id)?;
    let content = serde_json::to_string_pretty(result)?;
    crate::app_dirs::write_owner_only(&cache_file, content.as_bytes())?;
    Ok(())
  }

  // Get current timestamp
  fn get_current_timestamp() -> u64 {
    SystemTime::now()
      .duration_since(UNIX_EPOCH)
      .unwrap_or_default()
      .as_secs()
  }

  pub async fn get_ip_geolocation(
    ip: &str,
  ) -> Result<(Option<String>, Option<String>, Option<String>), String> {
    // Use ip-api.com (free, no API key required)
    let url = format!(
      "http://ip-api.com/json/{}?fields=status,message,country,countryCode,city",
      ip
    );

    let client = reqwest::Client::builder()
      .timeout(std::time::Duration::from_secs(5))
      .build()
      .map_err(|e| format!("Failed to create HTTP client: {e}"))?;

    match client.get(&url).send().await {
      Ok(response) => {
        if response.status().is_success() {
          match response.json::<serde_json::Value>().await {
            Ok(json) => {
              if json.get("status").and_then(|s| s.as_str()) == Some("success") {
                let country = json
                  .get("country")
                  .and_then(|v| v.as_str())
                  .map(|s| s.to_string());
                let country_code = json
                  .get("countryCode")
                  .and_then(|v| v.as_str())
                  .map(|s| s.to_string());
                let city = json
                  .get("city")
                  .and_then(|v| v.as_str())
                  .map(|s| s.to_string());
                Ok((city, country, country_code))
              } else {
                Ok((None, None, None))
              }
            }
            Err(e) => Err(format!("Failed to parse geolocation response: {e}")),
          }
        } else {
          Ok((None, None, None))
        }
      }
      Err(e) => Err(format!("Failed to fetch geolocation: {e}")),
    }
  }

  pub fn get_proxy_file_path(&self, proxy_id: &str) -> PathBuf {
    self.get_proxies_dir().join(format!("{proxy_id}.json"))
  }

  // Load stored proxies from disk
  fn load_stored_proxies(&self) -> Result<(), Box<dyn std::error::Error>> {
    let proxies_dir = self.get_proxies_dir();

    if !proxies_dir.exists() {
      log::debug!("Proxies directory does not exist: {:?}", proxies_dir);
      return Ok(()); // No proxies directory yet
    }

    log::debug!("Loading stored proxies from: {:?}", proxies_dir);

    let mut stored_proxies = self.stored_proxies.lock().unwrap();
    let mut loaded_count = 0;
    let mut error_count = 0;

    // Read all JSON files from the proxies directory
    for entry in fs::read_dir(&proxies_dir)? {
      let entry = entry?;
      let path = entry.path();

      if path.extension().is_some_and(|ext| ext == "json") {
        match fs::read_to_string(&path) {
          Ok(content) => match serde_json::from_str::<StoredProxy>(&content) {
            Ok(proxy) => {
              log::debug!("Loaded stored proxy: {} ({})", proxy.name, proxy.id);
              stored_proxies.insert(proxy.id.clone(), proxy);
              loaded_count += 1;
            }
            Err(e) => {
              log::warn!(
                "Failed to parse proxy file {:?} as StoredProxy: {}",
                path,
                e
              );
              error_count += 1;
            }
          },
          Err(e) => {
            log::warn!("Failed to read proxy file {:?}: {}", path, e);
            error_count += 1;
          }
        }
      }
    }

    log::info!(
      "Loaded {} stored proxies ({} errors)",
      loaded_count,
      error_count
    );
    Ok(())
  }

  // Save a single proxy to disk
  fn save_proxy(&self, proxy: &StoredProxy) -> Result<(), Box<dyn std::error::Error>> {
    let proxies_dir = self.get_proxies_dir();

    // Ensure directory exists
    fs::create_dir_all(&proxies_dir)?;

    let proxy_file = self.get_proxy_file_path(&proxy.id);
    let content = serde_json::to_string_pretty(proxy)?;
    crate::app_dirs::write_owner_only(&proxy_file, content.as_bytes())?;

    Ok(())
  }

  // Delete a proxy file from disk
  fn delete_proxy_file(&self, proxy_id: &str) -> Result<(), Box<dyn std::error::Error>> {
    let proxy_file = self.get_proxy_file_path(proxy_id);
    if proxy_file.exists() {
      fs::remove_file(proxy_file)?;
    }
    Ok(())
  }

  fn normalize_proxy_settings(mut proxy_settings: ProxySettings) -> Result<ProxySettings, String> {
    if !proxy_settings.proxy_type.eq_ignore_ascii_case("vless") {
      proxy_settings.vless_uri = None;
      return Ok(proxy_settings);
    }

    let uri = proxy_settings
      .vless_uri
      .as_deref()
      .filter(|uri| !uri.is_empty())
      .ok_or_else(|| crate::backend_error("VLESS_CONFIG_INVALID"))?;
    let parsed =
      crate::xray::parse_vless_uri(uri).map_err(|error| crate::vless_config_error(&error))?;
    let canonical_uri = crate::xray::export_vless_uri(&parsed.config, parsed.name.as_deref())
      .map_err(|error| crate::vless_config_error(&error))?;

    proxy_settings.proxy_type = "vless".to_string();
    proxy_settings.host = parsed.config.address;
    proxy_settings.port = parsed.config.port;
    proxy_settings.username = None;
    proxy_settings.password = None;
    proxy_settings.vless_uri = Some(canonical_uri);
    Ok(proxy_settings)
  }

  // Create a new stored proxy
  pub fn create_stored_proxy(
    &self,
    _app_handle: &tauri::AppHandle,
    name: String,
    proxy_settings: ProxySettings,
  ) -> Result<StoredProxy, String> {
    if name.trim().is_empty() {
      return Err(serde_json::json!({ "code": "NAME_CANNOT_BE_EMPTY" }).to_string());
    }

    // Check if name already exists
    {
      let stored_proxies = self.stored_proxies.lock().unwrap();
      if stored_proxies.values().any(|p| p.name == name) {
        return Err(format!("Proxy with name '{name}' already exists"));
      }
    }

    let proxy_settings = Self::normalize_proxy_settings(proxy_settings)?;
    let stored_proxy = StoredProxy::new(name, proxy_settings);

    {
      let mut stored_proxies = self.stored_proxies.lock().unwrap();
      stored_proxies.insert(stored_proxy.id.clone(), stored_proxy.clone());
    }

    if let Err(e) = self.save_proxy(&stored_proxy) {
      log::warn!("Failed to save proxy: {e}");
    }

    // Emit event for reactive UI updates
    if let Err(e) = events::emit_empty("proxies-changed") {
      log::error!("Failed to emit proxies-changed event: {e}");
    }

    if stored_proxy.sync_enabled {
      if let Some(scheduler) = crate::sync::get_global_scheduler() {
        let id = stored_proxy.id.clone();
        tauri::async_runtime::spawn(async move {
          scheduler.queue_proxy_sync(id).await;
        });
      }
    }

    Ok(stored_proxy)
  }

  // Check if a cloud-managed proxy exists
  pub fn has_cloud_proxy(&self) -> bool {
    let stored_proxies = self.stored_proxies.lock().unwrap();
    stored_proxies.contains_key(CLOUD_PROXY_ID)
  }

  // Upsert the cloud-managed proxy (create or update)
  pub fn upsert_cloud_proxy(&self, proxy_settings: ProxySettings) -> Result<StoredProxy, String> {
    let mut stored_proxies = self.stored_proxies.lock().unwrap();

    if let Some(existing) = stored_proxies.get_mut(CLOUD_PROXY_ID) {
      existing.proxy_settings = proxy_settings;
      let updated = existing.clone();
      drop(stored_proxies);

      if let Err(e) = self.save_proxy(&updated) {
        log::warn!("Failed to save cloud proxy: {e}");
      }
      if let Err(e) = events::emit_empty("proxies-changed") {
        log::error!("Failed to emit proxies-changed event: {e}");
      }
      Ok(updated)
    } else {
      let cloud_proxy = StoredProxy {
        id: CLOUD_PROXY_ID.to_string(),
        name: "Included Proxy".to_string(),
        proxy_settings,
        sync_enabled: false,
        last_sync: None,
        updated_at: Some(now_secs()),
        is_cloud_managed: true,
        is_cloud_derived: false,
        geo_country: None,
        geo_state: None,
        geo_region: None,
        geo_city: None,
        geo_isp: None,
        dynamic_proxy_url: None,
        dynamic_proxy_format: None,
      };
      stored_proxies.insert(CLOUD_PROXY_ID.to_string(), cloud_proxy.clone());
      drop(stored_proxies);

      if let Err(e) = self.save_proxy(&cloud_proxy) {
        log::warn!("Failed to save cloud proxy: {e}");
      }
      if let Err(e) = events::emit_empty("proxies-changed") {
        log::error!("Failed to emit proxies-changed event: {e}");
      }
      Ok(cloud_proxy)
    }
  }

  // Remove the cloud-managed proxy
  pub fn remove_cloud_proxy(&self) {
    let removed = {
      let mut stored_proxies = self.stored_proxies.lock().unwrap();
      stored_proxies.remove(CLOUD_PROXY_ID).is_some()
    };

    if removed {
      if let Err(e) = self.delete_proxy_file(CLOUD_PROXY_ID) {
        log::warn!("Failed to delete cloud proxy file: {e}");
      }
      if let Err(e) = events::emit_empty("proxies-changed") {
        log::error!("Failed to emit proxies-changed event: {e}");
      }
    }
  }

  pub fn remove_cloud_proxies(&self) {
    let removed_ids: Vec<String> = {
      let mut stored_proxies = self.stored_proxies.lock().unwrap();
      let ids_to_remove: Vec<String> = stored_proxies
        .values()
        .filter(|p| p.is_cloud_managed || p.is_cloud_derived)
        .map(|p| p.id.clone())
        .collect();
      for id in &ids_to_remove {
        stored_proxies.remove(id);
      }
      ids_to_remove
    };

    if !removed_ids.is_empty() {
      for id in &removed_ids {
        if let Err(e) = self.delete_proxy_file(id) {
          log::warn!("Failed to delete cloud proxy file {id}: {e}");
        }
      }
      if let Err(e) = events::emit_empty("proxies-changed") {
        log::error!("Failed to emit proxies-changed event: {e}");
      }
      if let Err(e) = events::emit_empty("stored-proxies-changed") {
        log::error!("Failed to emit stored-proxies-changed event: {e}");
      }
    }
  }

  // Build a geo-targeted username from base username and location parts
  // LP v2 format: username-country-{cc}[-region-{region}][-city-{city}][-isp-{isp}]
  // Note: sid and ttl are NOT included here — they are injected at browser launch time
  // per-profile via resolve_proxy_for_profile()
  fn build_geo_username(
    base_username: &str,
    country: &str,
    region: &Option<String>,
    city: &Option<String>,
    isp: &Option<String>,
  ) -> String {
    let mut username = format!("{}-country-{}", base_username, country);
    if let Some(region) = region {
      username = format!("{}-region-{}", username, region);
    }
    if let Some(city) = city {
      username = format!("{}-city-{}", username, city);
    }
    if let Some(isp) = isp {
      username = format!("{}-isp-{}", username, isp);
    }
    username
  }

  /// Generate a deterministic 11-char alphanumeric session ID from a profile UUID.
  /// This ensures the same profile always gets the same sticky IP session,
  /// even across credential refreshes.
  pub fn generate_sid_for_profile(profile_id: &str) -> String {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    let mut hasher = DefaultHasher::new();
    profile_id.hash(&mut hasher);
    let hash = hasher.finish();

    // Convert to base36 (a-z0-9) and take 11 chars
    let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect();
    let mut sid = String::with_capacity(11);
    let mut val = hash;
    for _ in 0..11 {
      sid.push(chars[(val % 36) as usize]);
      val /= 36;
    }
    sid
  }

  /// Build the full proxy username with sid and ttl for a specific profile launch.
  /// This is called at browser launch time, not at proxy creation time.
  pub fn build_username_with_sid(base_geo_username: &str, profile_id: &str) -> String {
    let sid = Self::generate_sid_for_profile(profile_id);
    format!("{}-sid-{}-ttl-1440m", base_geo_username, sid)
  }

  /// Resolve proxy settings for a specific profile, injecting profile-specific sid
  /// for cloud-derived proxies with geo targeting.
  pub fn resolve_proxy_for_profile(
    &self,
    proxy_id: &str,
    profile_id: &str,
  ) -> Option<ProxySettings> {
    let stored_proxies = self.stored_proxies.lock().unwrap();
    let proxy = stored_proxies.get(proxy_id)?;
    let mut settings = proxy.proxy_settings.clone();

    // For cloud-derived proxies with geo targeting, inject profile-specific sid
    if proxy.is_cloud_derived && proxy.geo_country.is_some() {
      if let Some(ref username) = settings.username {
        settings.username = Some(Self::build_username_with_sid(username, profile_id));
      }
    }

    Some(settings)
  }

  // Create a cloud-derived location proxy from the base cloud proxy credentials
  pub fn create_cloud_location_proxy(
    &self,
    name: String,
    country: String,
    region: Option<String>,
    city: Option<String>,
    isp: Option<String>,
  ) -> Result<StoredProxy, String> {
    // Get base cloud proxy credentials
    let base_proxy = {
      let stored_proxies = self.stored_proxies.lock().unwrap();
      stored_proxies
        .get(CLOUD_PROXY_ID)
        .cloned()
        .ok_or_else(|| "No cloud proxy available. Please log in first.".to_string())?
    };

    let base_username = base_proxy
      .proxy_settings
      .username
      .as_ref()
      .ok_or_else(|| "Cloud proxy has no username".to_string())?;

    let geo_username = Self::build_geo_username(base_username, &country, &region, &city, &isp);

    let proxy_settings = ProxySettings {
      proxy_type: base_proxy.proxy_settings.proxy_type.clone(),
      host: base_proxy.proxy_settings.host.clone(),
      port: base_proxy.proxy_settings.port,
      username: Some(geo_username),
      password: base_proxy.proxy_settings.password.clone(),
      vless_uri: None,
    };

    // Check if name already exists
    {
      let stored_proxies = self.stored_proxies.lock().unwrap();
      if stored_proxies.values().any(|p| p.name == name) {
        return Err(format!("Proxy with name '{}' already exists", name));
      }
    }

    let stored_proxy = StoredProxy {
      id: uuid::Uuid::new_v4().to_string(),
      name,
      proxy_settings,
      sync_enabled: false,
      last_sync: None,
      updated_at: Some(now_secs()),
      is_cloud_managed: false,
      is_cloud_derived: true,
      geo_country: Some(country),
      geo_state: None,
      geo_region: region,
      geo_city: city,
      geo_isp: isp,
      dynamic_proxy_url: None,
      dynamic_proxy_format: None,
    };

    {
      let mut stored_proxies = self.stored_proxies.lock().unwrap();
      stored_proxies.insert(stored_proxy.id.clone(), stored_proxy.clone());
    }

    if let Err(e) = self.save_proxy(&stored_proxy) {
      log::warn!("Failed to save location proxy: {e}");
    }

    if let Err(e) = events::emit_empty("proxies-changed") {
      log::error!("Failed to emit proxies-changed event: {e}");
    }

    Ok(stored_proxy)
  }

  // Update all cloud-derived proxies when base cloud proxy credentials change
  pub fn update_cloud_derived_proxies(&self) {
    let base_proxy = {
      let stored_proxies = self.stored_proxies.lock().unwrap();
      match stored_proxies.get(CLOUD_PROXY_ID) {
        Some(p) => p.clone(),
        None => return, // No cloud proxy, nothing to update
      }
    };

    let base_username = match &base_proxy.proxy_settings.username {
      Some(u) => u.clone(),
      None => return,
    };

    let mut updated = false;
    let mut stored_proxies = self.stored_proxies.lock().unwrap();

    for proxy in stored_proxies.values_mut() {
      if !proxy.is_cloud_derived {
        continue;
      }

      let country = match &proxy.geo_country {
        Some(c) => c.clone(),
        None => continue,
      };

      let region = proxy.effective_region().cloned();
      let geo_username = Self::build_geo_username(
        &base_username,
        &country,
        &region,
        &proxy.geo_city,
        &proxy.geo_isp,
      );

      proxy.updated_at = Some(now_secs());
      proxy.proxy_settings.username = Some(geo_username);
      proxy.proxy_settings.password = base_proxy.proxy_settings.password.clone();
      proxy.proxy_settings.host = base_proxy.proxy_settings.host.clone();
      proxy.proxy_settings.port = base_proxy.proxy_settings.port;

      updated = true;
    }

    if updated {
      // Save all updated proxies
      let proxies_to_save: Vec<StoredProxy> = stored_proxies
        .values()
        .filter(|p| p.is_cloud_derived)
        .cloned()
        .collect();
      drop(stored_proxies);

      for proxy in &proxies_to_save {
        if let Err(e) = self.save_proxy(proxy) {
          log::warn!("Failed to save updated derived proxy {}: {e}", proxy.id);
        }
      }

      if let Err(e) = events::emit_empty("proxies-changed") {
        log::error!("Failed to emit proxies-changed event: {e}");
      }

      log::debug!("Updated {} cloud-derived proxies", proxies_to_save.len());
    }
  }

  pub fn remove_from_memory(&self, proxy_id: &str) {
    let mut stored_proxies = self.stored_proxies.lock().unwrap();
    stored_proxies.remove(proxy_id);
  }

  // Get all stored proxies
  pub fn get_stored_proxies(&self) -> Vec<StoredProxy> {
    let stored_proxies = self.stored_proxies.lock().unwrap();
    let mut list: Vec<StoredProxy> = stored_proxies.values().cloned().collect();
    // Sort case-insensitively by name for consistent ordering across UI/API consumers
    list.sort_by_key(|p| p.name.to_lowercase());
    list
  }

  /// Insert/replace a stored proxy in the in-memory map. Used by sync's
  /// download_proxy after it writes the file to disk, mirroring how
  /// download_group/download_vpn/download_extension keep their managers'
  /// in-memory state in sync. Without this, get_stored_proxies (which reads
  /// only the map) never sees a downloaded proxy until restart, so sync keeps
  /// re-downloading it indefinitely.
  pub fn upsert_stored_proxy(&self, proxy: StoredProxy) {
    let mut stored_proxies = self.stored_proxies.lock().unwrap();
    stored_proxies.insert(proxy.id.clone(), proxy);
  }

  // Get a stored proxy by ID

  // Update a stored proxy
  pub fn update_stored_proxy(
    &self,
    _app_handle: &tauri::AppHandle,
    proxy_id: &str,
    name: Option<String>,
    proxy_settings: Option<ProxySettings>,
  ) -> Result<StoredProxy, String> {
    if name.as_deref().is_some_and(|n| n.trim().is_empty()) {
      return Err(serde_json::json!({ "code": "NAME_CANNOT_BE_EMPTY" }).to_string());
    }

    let proxy_settings = proxy_settings
      .map(Self::normalize_proxy_settings)
      .transpose()?;

    // First, check for conflicts without holding a mutable reference
    {
      let stored_proxies = self.stored_proxies.lock().unwrap();

      // Check if proxy exists
      if !stored_proxies.contains_key(proxy_id) {
        return Err(format!("Proxy with ID '{proxy_id}' not found"));
      }

      // Block editing cloud-managed proxies
      if stored_proxies
        .get(proxy_id)
        .is_some_and(|p| p.is_cloud_managed)
      {
        return Err("Cannot edit a cloud-managed proxy".to_string());
      }

      // Check if new name conflicts with existing proxies
      if let Some(ref new_name) = name {
        if stored_proxies
          .values()
          .any(|p| p.id != proxy_id && p.name == *new_name)
        {
          return Err(format!("Proxy with name '{new_name}' already exists"));
        }
      }
    } // Release the lock here

    // Now get mutable access for updates
    let updated_proxy = {
      let mut stored_proxies = self.stored_proxies.lock().unwrap();
      let stored_proxy = stored_proxies.get_mut(proxy_id).unwrap(); // Safe because we checked above

      if let Some(new_name) = name {
        stored_proxy.update_name(new_name);
      }

      if let Some(new_settings) = proxy_settings {
        stored_proxy.update_settings(new_settings);
      }

      stored_proxy.clone()
    };

    if let Err(e) = self.save_proxy(&updated_proxy) {
      log::warn!("Failed to save proxy: {e}");
    }

    // Emit event for reactive UI updates
    if let Err(e) = events::emit_empty("proxies-changed") {
      log::error!("Failed to emit proxies-changed event: {e}");
    }

    if updated_proxy.sync_enabled {
      if let Some(scheduler) = crate::sync::get_global_scheduler() {
        let id = updated_proxy.id.clone();
        tauri::async_runtime::spawn(async move {
          scheduler.queue_proxy_sync(id).await;
        });
      }
    }

    Ok(updated_proxy)
  }

  /// Update the in-memory `sync_enabled` / `last_sync` fields of a stored
  /// proxy and persist the change to disk. Returns the updated proxy or
  /// `Err` if the proxy isn't found / is cloud-managed.
  ///
  /// This is the canonical write path for sync-state changes — direct
  /// `fs::write` from a sync command would leave the in-memory cache
  /// (`stored_proxies`) stale, and the next `get_stored_proxies()` would
  /// return the old `sync_enabled`, breaking the UI toggle.
  pub fn set_stored_proxy_sync_state(
    &self,
    proxy_id: &str,
    sync_enabled: bool,
    last_sync: Option<u64>,
  ) -> Result<StoredProxy, String> {
    let updated_proxy = {
      let mut stored_proxies = self.stored_proxies.lock().unwrap();
      let proxy = stored_proxies
        .get_mut(proxy_id)
        .ok_or_else(|| format!("Proxy with ID '{proxy_id}' not found"))?;

      if proxy.is_cloud_managed {
        return Err("Cannot modify sync for a cloud-managed proxy".to_string());
      }

      proxy.sync_enabled = sync_enabled;
      proxy.last_sync = last_sync;
      proxy.clone()
    };

    self
      .save_proxy(&updated_proxy)
      .map_err(|e| format!("Failed to save proxy: {e}"))?;

    Ok(updated_proxy)
  }

  // Delete a stored proxy
  pub fn delete_stored_proxy(
    &self,
    app_handle: &tauri::AppHandle,
    proxy_id: &str,
  ) -> Result<(), String> {
    // Remember if sync was enabled before deleting
    let was_sync_enabled = {
      let stored_proxies = self.stored_proxies.lock().unwrap();

      // Block deleting cloud-managed proxies
      if stored_proxies
        .get(proxy_id)
        .is_some_and(|p| p.is_cloud_managed)
      {
        return Err("Cannot delete a cloud-managed proxy".to_string());
      }

      stored_proxies
        .get(proxy_id)
        .map(|p| p.sync_enabled)
        .unwrap_or(false)
    };

    {
      let mut stored_proxies = self.stored_proxies.lock().unwrap();
      if stored_proxies.remove(proxy_id).is_none() {
        return Err(format!("Proxy with ID '{proxy_id}' not found"));
      }
    }

    if let Err(e) = self.delete_proxy_file(proxy_id) {
      log::warn!("Failed to delete proxy file: {e}");
    }

    // If sync was enabled, also delete from S3
    if was_sync_enabled {
      let proxy_id_owned = proxy_id.to_string();
      let app_handle_clone = app_handle.clone();
      tauri::async_runtime::spawn(async move {
        match crate::sync::SyncEngine::create_from_settings(&app_handle_clone).await {
          Ok(engine) => {
            if let Err(e) = engine.delete_proxy(&proxy_id_owned).await {
              log::warn!("Failed to delete proxy {} from sync: {}", proxy_id_owned, e);
            } else {
              log::info!("Proxy {} deleted from S3 sync storage", proxy_id_owned);
            }
          }
          Err(e) => {
            log::debug!("Sync not configured, skipping remote deletion: {}", e);
          }
        }
      });
    }

    // Emit event for reactive UI updates
    if let Err(e) = events::emit_empty("proxies-changed") {
      log::error!("Failed to emit proxies-changed event: {e}");
    }

    Ok(())
  }

  // Check if a proxy is cloud-managed or cloud-derived (needs fresh credentials)
  pub fn is_cloud_or_derived(&self, proxy_id: &str) -> bool {
    let stored_proxies = self.stored_proxies.lock().unwrap();
    stored_proxies
      .get(proxy_id)
      .is_some_and(|p| p.is_cloud_managed || p.is_cloud_derived)
  }

  // Get proxy settings for a stored proxy ID
  pub fn get_proxy_settings_by_id(&self, proxy_id: &str) -> Option<ProxySettings> {
    let stored_proxies = self.stored_proxies.lock().unwrap();
    stored_proxies
      .get(proxy_id)
      .map(|p| p.proxy_settings.clone())
  }

  fn classify_proxy_error(raw_error: &str, settings: &ProxySettings) -> String {
    let err = raw_error.to_lowercase();
    let proxy_addr = format!("{}:{}", settings.host, settings.port);

    if err.contains("connection refused") {
      return format!(
        "Connection refused by {proxy_addr}. The proxy server is not accepting connections."
      );
    }
    if err.contains("connection reset") {
      return format!(
        "Connection reset by {proxy_addr}. The proxy server closed the connection unexpectedly."
      );
    }
    if err.contains("timed out") || err.contains("deadline has elapsed") {
      return format!("Connection to {proxy_addr} timed out. The proxy server is not responding.");
    }
    if err.contains("no such host") || err.contains("dns") || err.contains("resolve") {
      return format!(
        "Could not resolve proxy host '{}'. Check that the hostname is correct.",
        settings.host
      );
    }
    if err.contains("authentication") || err.contains("407") || err.contains("proxy auth") {
      return format!(
        "Proxy authentication failed for {proxy_addr}. Check your username and password."
      );
    }
    if err.contains("403") || err.contains("forbidden") {
      return format!("Access denied by {proxy_addr} (403 Forbidden).");
    }
    if err.contains("402") {
      return format!(
        "Payment required by {proxy_addr} (402). Your proxy subscription may have expired."
      );
    }
    if err.contains("502") || err.contains("bad gateway") {
      return format!(
        "Bad gateway from {proxy_addr} (502). The upstream proxy server may be down."
      );
    }
    if err.contains("503") || err.contains("service unavailable") {
      return format!("Proxy {proxy_addr} is temporarily unavailable (503).");
    }
    if err.contains("socks") && err.contains("unreachable") {
      return format!("SOCKS proxy {proxy_addr} could not reach the target. The proxy server may not have internet access.");
    }
    if err.contains("invalid proxy") || err.contains("unsupported proxy") {
      return format!(
        "Invalid proxy configuration for {proxy_addr}. Check the proxy type and address."
      );
    }

    // Generic fallback — still show the proxy address for context
    format!("Proxy check failed for {proxy_addr}. Could not connect through the proxy.")
  }

  // Build proxy URL string from ProxySettings. Credentials are percent-encoded:
  // a password containing `/`, `#`, `?` or `@` otherwise breaks the URL
  // authority and silently retargets the request at the wrong host.
  pub fn build_proxy_url(proxy_settings: &ProxySettings) -> String {
    if proxy_settings.proxy_type.eq_ignore_ascii_case("vless") {
      return proxy_settings.vless_uri.clone().unwrap_or_default();
    }

    let mut url = format!("{}://", proxy_settings.proxy_type);

    if let (Some(username), Some(password)) = (&proxy_settings.username, &proxy_settings.password) {
      url.push_str(&urlencoding::encode(username));
      url.push(':');
      url.push_str(&urlencoding::encode(password));
      url.push('@');
    } else if let Some(username) = &proxy_settings.username {
      url.push_str(&urlencoding::encode(username));
      url.push('@');
    }

    url.push_str(&proxy_settings.host);
    url.push(':');
    url.push_str(&proxy_settings.port.to_string());

    url
  }

  /// Proxy URL for a diagnostic probe made by the app itself (reqwest), as
  /// opposed to `build_proxy_url`, which feeds the browser.
  ///
  /// SOCKS5 becomes `socks5h://` so the probe endpoint's hostname resolves at
  /// the exit rather than on this machine. Resolving locally would leak the
  /// real DNS and, behind a split-horizon resolver, can reach a different host
  /// than the browser would.
  pub fn build_probe_proxy_url(proxy_settings: &ProxySettings) -> String {
    let url = Self::build_proxy_url(proxy_settings);
    if proxy_settings.proxy_type.eq_ignore_ascii_case("socks5") {
      return url.replacen("socks5://", "socks5h://", 1);
    }
    url
  }

  // Check if a proxy is valid by routing through a temporary donut-proxy process.
  // This tests the exact same code path the browser uses.
  // Falls back to direct reqwest check if the proxy worker fails to start.
  pub async fn check_proxy_validity(
    &self,
    proxy_id: &str,
    proxy_settings: &ProxySettings,
  ) -> Result<ProxyCheckResult, String> {
    let mut xray_worker_id = None;
    let effective_proxy_settings = if proxy_settings.proxy_type.eq_ignore_ascii_case("vless") {
      let uri = proxy_settings
        .vless_uri
        .as_deref()
        .ok_or_else(|| crate::backend_error("VLESS_CONFIG_INVALID"))?;
      let worker = crate::xray_worker_runner::start_xray_worker(None, uri)
        .await
        .map_err(|error| error.to_string())?;
      xray_worker_id = Some(worker.id.clone());
      worker.local_proxy_settings()
    } else {
      proxy_settings.clone()
    };
    let upstream_url = Self::build_proxy_url(&effective_proxy_settings);

    // Try process-based check first (identical to browser launch path).
    // If the proxy worker fails to start (e.g. Gatekeeper, antivirus, signing
    // restrictions), fall back to a direct reqwest check.
    let proxy_start_result =
      crate::proxy_runner::start_proxy_process(Some(upstream_url.clone()), None)
        .await
        .map_err(|e| e.to_string());

    let ip_result = match proxy_start_result {
      Ok(proxy_config) => {
        let local_url = format!("http://127.0.0.1:{}", proxy_config.local_port.unwrap_or(0));
        let config_id = proxy_config.id.clone();
        // Tie the check worker's lifetime to this GUI process: the worker's
        // owner watchdog self-exits when that identity dies, so if the app is
        // killed mid-check the worker follows instead of idling until the
        // next app launch.
        if !persist_browser_identity(&config_id, std::process::id()) {
          log::warn!("Failed to tag check worker {config_id} with app PID for self-expiry");
        }
        // Wrap in a timeout so the check worker doesn't stay alive indefinitely
        // if the upstream is slow or unreachable.
        let result = tokio::time::timeout(
          std::time::Duration::from_secs(30),
          ip_utils::fetch_public_ip(Some(&local_url)),
        )
        .await
        .unwrap_or_else(|_| {
          Err(ip_utils::IpError::Network(
            "Proxy check timed out after 30s".to_string(),
          ))
        });
        // Always stop the worker — even if the check failed or timed out
        let _ = crate::proxy_runner::stop_proxy_process(&config_id).await;
        result
      }
      Err(err_msg) => {
        if xray_worker_id.is_some() {
          log::warn!("Local proxy worker failed to start in front of Xray-core: {err_msg}");
          Err(ip_utils::IpError::Network(err_msg))
        } else {
          log::warn!(
            "Proxy worker failed to start ({}), falling back to direct check",
            err_msg
          );
          ip_utils::fetch_public_ip(Some(&upstream_url)).await
        }
      }
    };
    if let Some(worker_id) = xray_worker_id {
      let _ = crate::xray_worker_runner::stop_xray_worker(&worker_id).await;
    }

    let ip = match ip_result {
      Ok(ip) => ip,
      Err(e) => {
        let failed_result = ProxyCheckResult {
          ip: String::new(),
          city: None,
          country: None,
          country_code: None,
          timestamp: Self::get_current_timestamp(),
          is_valid: false,
        };
        let _ = self.save_proxy_check_cache(proxy_id, &failed_result);

        let err_str = e.to_string();
        let user_message = Self::classify_proxy_error(&err_str, proxy_settings);
        return Err(user_message);
      }
    };

    // Get geolocation
    let (city, country, country_code): (Option<String>, Option<String>, Option<String>) =
      Self::get_ip_geolocation(&ip).await.unwrap_or_default();

    // Create successful result
    let result = ProxyCheckResult {
      ip: ip.clone(),
      city,
      country,
      country_code,
      timestamp: Self::get_current_timestamp(),
      is_valid: true,
    };

    // Save to cache
    let _ = self.save_proxy_check_cache(proxy_id, &result);

    Ok(result)
  }

  // Get cached proxy check result
  pub fn get_cached_proxy_check(&self, proxy_id: &str) -> Option<ProxyCheckResult> {
    self.load_proxy_check_cache(proxy_id)
  }

  // Export all proxies as JSON
  pub fn export_proxies_json(&self) -> Result<String, String> {
    let stored_proxies = self.stored_proxies.lock().unwrap();
    let proxies: Vec<ExportedProxy> = stored_proxies
      .values()
      .filter(|p| !p.is_cloud_managed && !p.is_cloud_derived)
      .map(|p| ExportedProxy {
        name: p.name.clone(),
        proxy_type: p.proxy_settings.proxy_type.clone(),
        host: p.proxy_settings.host.clone(),
        port: p.proxy_settings.port,
        username: p.proxy_settings.username.clone(),
        password: p.proxy_settings.password.clone(),
        vless_uri: p.proxy_settings.vless_uri.clone(),
      })
      .collect();

    let export_data = ProxyExportData {
      version: "1.0".to_string(),
      proxies,
      exported_at: Utc::now().to_rfc3339(),
      source: "DonutBrowser".to_string(),
    };

    serde_json::to_string_pretty(&export_data).map_err(|e| format!("Failed to serialize: {e}"))
  }

  // Export all proxies as TXT (one per line: protocol://user:pass@host:port)
  pub fn export_proxies_txt(&self) -> String {
    let stored_proxies = self.stored_proxies.lock().unwrap();
    stored_proxies
      .values()
      .filter(|p| !p.is_cloud_managed && !p.is_cloud_derived)
      .map(|p| Self::build_proxy_url(&p.proxy_settings))
      .collect::<Vec<_>>()
      .join("\n")
  }

  // Parse TXT content with auto-detection of formats
  pub fn parse_txt_proxies(content: &str) -> Vec<ProxyParseResult> {
    content
      .lines()
      .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#'))
      .map(|line| Self::parse_single_proxy_line(line.trim()))
      .collect()
  }

  // Parse a single proxy line with format auto-detection
  fn parse_single_proxy_line(line: &str) -> ProxyParseResult {
    // Format 1: protocol://username:password@host:port (full URL)
    if let Some(result) = Self::try_parse_url_format(line) {
      return result;
    }

    // Try colon-separated formats
    let parts: Vec<&str> = line.split(':').collect();

    match parts.len() {
      // host:port (no auth)
      2 => {
        if let Ok(port) = parts[1].parse::<u16>() {
          return ProxyParseResult::Parsed(ParsedProxyLine {
            proxy_type: "http".to_string(),
            host: parts[0].to_string(),
            port,
            username: None,
            password: None,
            vless_uri: None,
            original_line: line.to_string(),
          });
        }
        ProxyParseResult::Invalid {
          line: line.to_string(),
          reason: "Invalid port number".to_string(),
        }
      }
      // Could be: host:port:user or user:pass@host (with @ in the middle)
      3 => {
        // Try username:password@host:port first
        if let Some(result) = Self::try_parse_user_pass_at_host_port(line) {
          return result;
        }
        ProxyParseResult::Invalid {
          line: line.to_string(),
          reason: "Could not determine format with 3 parts".to_string(),
        }
      }
      // 4 parts: could be host:port:user:pass OR user:pass:host:port
      4 => Self::parse_colon_separated_quad(&parts, "http", line),
      _ => ProxyParseResult::Invalid {
        line: line.to_string(),
        reason: format!("Unexpected format with {} parts", parts.len()),
      },
    }
  }

  // Resolve a four-part colon-separated body, which is either
  // host:port:username:password or username:password:host:port. The port
  // position tells the two apart; when both positions parse as a port the
  // caller has to ask the user.
  fn parse_colon_separated_quad(parts: &[&str], proxy_type: &str, line: &str) -> ProxyParseResult {
    let port_at_1 = parts[1].parse::<u16>().ok();
    let port_at_3 = parts[3].parse::<u16>().ok();

    match (port_at_1, port_at_3) {
      // host:port:user:pass
      (Some(port), None) => ProxyParseResult::Parsed(ParsedProxyLine {
        proxy_type: proxy_type.to_string(),
        host: parts[0].to_string(),
        port,
        username: Some(parts[2].to_string()),
        password: Some(parts[3].to_string()),
        vless_uri: None,
        original_line: line.to_string(),
      }),
      // user:pass:host:port
      (None, Some(port)) => ProxyParseResult::Parsed(ParsedProxyLine {
        proxy_type: proxy_type.to_string(),
        host: parts[2].to_string(),
        port,
        username: Some(parts[0].to_string()),
        password: Some(parts[1].to_string()),
        vless_uri: None,
        original_line: line.to_string(),
      }),
      // Both could be ports - ambiguous
      (Some(_), Some(_)) => ProxyParseResult::Ambiguous {
        line: line.to_string(),
        possible_formats: vec![
          "host:port:username:password".to_string(),
          "username:password:host:port".to_string(),
        ],
      },
      // Neither is a valid port
      (None, None) => ProxyParseResult::Invalid {
        line: line.to_string(),
        reason: "No valid port number found".to_string(),
      },
    }
  }

  // Try to parse URL format: protocol://username:password@host:port
  fn try_parse_url_format(line: &str) -> Option<ProxyParseResult> {
    if line.starts_with("vless://") {
      return Some(match crate::xray::parse_vless_uri(line) {
        Ok(parsed) => ProxyParseResult::Parsed(ParsedProxyLine {
          proxy_type: "vless".to_string(),
          host: parsed.config.address,
          port: parsed.config.port,
          username: None,
          password: None,
          vless_uri: Some(line.to_string()),
          original_line: line.to_string(),
        }),
        Err(error) => ProxyParseResult::Invalid {
          line: line.to_string(),
          reason: error.to_string(),
        },
      });
    }

    // Check for protocol prefix using strip_prefix
    let (protocol, rest) = if let Some(rest) = line.strip_prefix("http://") {
      ("http", rest)
    } else if let Some(rest) = line.strip_prefix("https://") {
      ("https", rest)
    } else if let Some(rest) = line.strip_prefix("socks4://") {
      ("socks4", rest)
    } else if let Some(rest) = line.strip_prefix("socks5://") {
      ("socks5", rest)
    } else if let Some(rest) = line.strip_prefix("socks://") {
      ("socks5", rest) // Default socks to socks5
    } else if let Some(rest) = line.strip_prefix("ss://") {
      ("ss", rest)
    } else {
      let rest = line.strip_prefix("shadowsocks://")?;
      ("ss", rest)
    };

    // Check if there's auth (contains @)
    if let Some(at_pos) = rest.rfind('@') {
      let auth = &rest[..at_pos];
      let host_port = &rest[at_pos + 1..];

      // Parse auth (user:pass)
      let (username, password) = if let Some(colon_pos) = auth.find(':') {
        let user = urlencoding::decode(&auth[..colon_pos]).unwrap_or_default();
        let pass = urlencoding::decode(&auth[colon_pos + 1..]).unwrap_or_default();
        (Some(user.to_string()), Some(pass.to_string()))
      } else {
        (
          Some(urlencoding::decode(auth).unwrap_or_default().to_string()),
          None,
        )
      };

      // Parse host:port
      if let Some(colon_pos) = host_port.rfind(':') {
        let host = &host_port[..colon_pos];
        if let Ok(port) = host_port[colon_pos + 1..].parse::<u16>() {
          return Some(ProxyParseResult::Parsed(ParsedProxyLine {
            proxy_type: protocol.to_string(),
            host: host.to_string(),
            port,
            username,
            password,
            vless_uri: None,
            original_line: line.to_string(),
          }));
        }
      }
    } else {
      // Vendors also hand out the colon-separated body behind a scheme, as in
      // socks5://host:port:user:pass, so try that before plain host:port. An
      // IPv6 literal splits into four too ("[", "", "1]", "8080" for [::1]:8080),
      // so require every field to be populated before reading it that way.
      let parts: Vec<&str> = rest.split(':').collect();
      if parts.len() == 4 && parts.iter().all(|part| !part.is_empty()) {
        return Some(Self::parse_colon_separated_quad(&parts, protocol, line));
      }

      // No auth, just host:port
      if let Some(colon_pos) = rest.rfind(':') {
        let host = &rest[..colon_pos];
        if let Ok(port) = rest[colon_pos + 1..].parse::<u16>() {
          return Some(ProxyParseResult::Parsed(ParsedProxyLine {
            proxy_type: protocol.to_string(),
            host: host.to_string(),
            port,
            username: None,
            password: None,
            vless_uri: None,
            original_line: line.to_string(),
          }));
        }
      }
    }

    Some(ProxyParseResult::Invalid {
      line: line.to_string(),
      reason: "Invalid URL format".to_string(),
    })
  }

  // Try to parse: username:password@host:port format (no protocol)
  fn try_parse_user_pass_at_host_port(line: &str) -> Option<ProxyParseResult> {
    if let Some(at_pos) = line.rfind('@') {
      let auth = &line[..at_pos];
      let host_port = &line[at_pos + 1..];

      // Parse auth
      let (username, password) = {
        let colon_pos = auth.find(':')?;
        (
          Some(auth[..colon_pos].to_string()),
          Some(auth[colon_pos + 1..].to_string()),
        )
      };

      // Parse host:port
      if let Some(colon_pos) = host_port.rfind(':') {
        let host = &host_port[..colon_pos];
        if let Ok(port) = host_port[colon_pos + 1..].parse::<u16>() {
          return Some(ProxyParseResult::Parsed(ParsedProxyLine {
            proxy_type: "http".to_string(),
            host: host.to_string(),
            port,
            username,
            password,
            vless_uri: None,
            original_line: line.to_string(),
          }));
        }
      }
    }
    None
  }

  // Import proxies from JSON content
  pub fn import_proxies_json(
    &self,
    app_handle: &tauri::AppHandle,
    content: &str,
  ) -> Result<ProxyImportResult, String> {
    let export_data: ProxyExportData =
      serde_json::from_str(content).map_err(|e| format!("Invalid JSON format: {e}"))?;

    let mut imported = Vec::new();
    let mut skipped = 0;
    let mut errors = Vec::new();

    for exported in export_data.proxies {
      let proxy_settings = ProxySettings {
        proxy_type: exported.proxy_type,
        host: exported.host,
        port: exported.port,
        username: exported.username,
        password: exported.password,
        vless_uri: exported.vless_uri,
      };

      match self.create_stored_proxy(app_handle, exported.name.clone(), proxy_settings) {
        Ok(proxy) => imported.push(proxy),
        Err(e) => {
          if e.contains("already exists") {
            skipped += 1;
          } else {
            errors.push(format!("Failed to import '{}': {}", exported.name, e));
          }
        }
      }
    }

    Ok(ProxyImportResult {
      imported_count: imported.len(),
      skipped_count: skipped,
      errors,
      proxies: imported,
    })
  }

  // Import proxies from already parsed proxy lines
  pub fn import_proxies_from_parsed(
    &self,
    app_handle: &tauri::AppHandle,
    parsed_proxies: Vec<ParsedProxyLine>,
    name_prefix: Option<String>,
  ) -> Result<ProxyImportResult, String> {
    let mut imported = Vec::new();
    let mut skipped = 0;
    let mut errors = Vec::new();
    let prefix = name_prefix.unwrap_or_else(|| "Imported".to_string());

    for (i, parsed) in parsed_proxies.into_iter().enumerate() {
      let proxy_name = format!("{} Proxy {}", prefix, i + 1);
      let proxy_settings = ProxySettings {
        proxy_type: parsed.proxy_type,
        host: parsed.host,
        port: parsed.port,
        username: parsed.username,
        password: parsed.password,
        vless_uri: parsed.vless_uri,
      };

      match self.create_stored_proxy(app_handle, proxy_name.clone(), proxy_settings) {
        Ok(proxy) => imported.push(proxy),
        Err(e) => {
          if e.contains("already exists") {
            skipped += 1;
          } else {
            errors.push(format!("Failed to import '{}': {}", proxy_name, e));
          }
        }
      }
    }

    Ok(ProxyImportResult {
      imported_count: imported.len(),
      skipped_count: skipped,
      errors,
      proxies: imported,
    })
  }

  // Start a proxy for given proxy settings and associate it with a browser process ID
  // If proxy_settings is None, starts a direct proxy for traffic monitoring
  #[allow(clippy::too_many_arguments)]
  pub async fn start_proxy(
    &self,
    app_handle: tauri::AppHandle,
    proxy_settings: Option<&ProxySettings>,
    browser_pid: u32,
    profile_id: Option<&str>,
    bypass_rules: Vec<String>,
    blocklist_file: Option<String>,
    dns_allowlist_mode: bool,
    // Protocol the local worker serves the browser: "socks5" (Wayfern). Reflected in
    // the returned ProxySettings.proxy_type so the caller formats the right local proxy URL scheme.
    local_protocol: &str,
  ) -> Result<ProxySettings, String> {
    if let Some(name) = profile_id {
      // Check if we have an active proxy recorded for this profile
      let maybe_existing_id = {
        let map = self.profile_active_proxy_ids.lock().unwrap();
        map.get(name).cloned()
      };

      if let Some(existing_id) = maybe_existing_id {
        // Find the existing proxy info
        let existing_info = {
          let proxies = self.active_proxies.lock().unwrap();
          proxies.values().find(|p| p.id == existing_id).cloned()
        };

        if let Some(existing) = existing_info {
          let desired_type = proxy_settings
            .map(|p| p.proxy_type.as_str())
            .unwrap_or("DIRECT");
          let desired_host = proxy_settings.map(|p| p.host.as_str()).unwrap_or("DIRECT");
          let desired_port = proxy_settings.map(|p| p.port).unwrap_or(0);

          let is_same_upstream = existing.upstream_type == desired_type
            && existing.upstream_host == desired_host
            && existing.upstream_port == desired_port;

          if is_same_upstream {
            // Settings match - can reuse existing proxy
            // Just update the PID mapping if needed
            let proxies = self.active_proxies.lock().unwrap();
            if proxies.contains_key(&browser_pid) {
              // Already mapped, reuse it
              return Ok(ProxySettings {
                proxy_type: local_protocol.to_string(),
                host: "127.0.0.1".to_string(),
                port: existing.local_port,
                username: None,
                password: None,
                vless_uri: None,
              });
            }
            // Need to add this PID to the mapping - we'll do that after starting
          }
          // Settings differ - we'll create a new proxy, but don't stop the old one
          // It will be cleaned up by periodic cleanup if it becomes dead
        }
      }
    }
    // Check if we already have a proxy for this browser PID
    // If settings match, reuse it; otherwise create a new one (don't stop the old one)
    {
      let proxies = self.active_proxies.lock().unwrap();
      if let Some(existing) = proxies.get(&browser_pid) {
        let desired_type = proxy_settings
          .map(|p| p.proxy_type.as_str())
          .unwrap_or("DIRECT");
        let desired_host = proxy_settings.map(|p| p.host.as_str()).unwrap_or("DIRECT");
        let desired_port = proxy_settings.map(|p| p.port).unwrap_or(0);

        let is_same_upstream = existing.upstream_type == desired_type
          && existing.upstream_host == desired_host
          && existing.upstream_port == desired_port;

        if is_same_upstream {
          // Check if profile_id matches
          let profile_id_matches = match (profile_id, &existing.profile_id) {
            (Some(ref new_id), Some(ref old_id)) => new_id == old_id,
            (None, None) => true,
            _ => false,
          };

          if profile_id_matches {
            // Reuse existing local proxy (settings and profile_id match)
            return Ok(ProxySettings {
              proxy_type: local_protocol.to_string(),
              host: "127.0.0.1".to_string(),
              port: existing.local_port,
              username: None,
              password: None,
              vless_uri: None,
            });
          }
          // Profile ID changed - we'll create a new proxy but don't stop the old one
          // It will be cleaned up by periodic cleanup if it becomes dead
        }
        // Upstream changed - we'll create a new proxy but don't stop the old one
        // It will be cleaned up by periodic cleanup if it becomes dead
      }
    }

    crate::proxy_runner::ensure_sidecar_version()
      .await
      .map_err(|e| e.to_string())?;

    // Start a new proxy using the donut-proxy binary with the correct CLI interface
    let mut proxy_cmd = app_handle
      .shell()
      .sidecar("donut-proxy")
      .map_err(|e| format!("Failed to create sidecar: {e}"))?
      .arg("proxy")
      .arg("start");

    // Add upstream proxy settings if provided, otherwise create direct proxy
    if let Some(proxy_settings) = proxy_settings {
      proxy_cmd = proxy_cmd
        .arg("--host")
        .arg(&proxy_settings.host)
        .arg("--proxy-port")
        .arg(proxy_settings.port.to_string())
        .arg("--type")
        .arg(&proxy_settings.proxy_type);

      // Keep credentials out of process arguments. The short-lived sidecar
      // removes these variables before it spawns the detached worker.
      if let Some(username) = &proxy_settings.username {
        proxy_cmd = proxy_cmd.env("DONUT_PROXY_USERNAME", username);
      }
      if let Some(password) = &proxy_settings.password {
        proxy_cmd = proxy_cmd.env("DONUT_PROXY_PASSWORD", password);
      }
    }

    // Add profile ID if provided for traffic tracking
    if let Some(id) = profile_id {
      proxy_cmd = proxy_cmd.arg("--profile-id").arg(id);
    }

    // Add bypass rules if any
    if !bypass_rules.is_empty() {
      let rules_json = serde_json::to_string(&bypass_rules)
        .map_err(|e| format!("Failed to serialize bypass rules: {e}"))?;
      proxy_cmd = proxy_cmd.arg("--bypass-rules").arg(rules_json);
    }

    // Add blocklist file path if provided
    if let Some(ref path) = blocklist_file {
      proxy_cmd = proxy_cmd.arg("--blocklist-file").arg(path);
      if dns_allowlist_mode {
        proxy_cmd = proxy_cmd.arg("--dns-allowlist-mode");
      }
    }

    // Tell the worker which protocol to serve the browser (http or socks5)
    proxy_cmd = proxy_cmd.arg("--local-protocol").arg(local_protocol);

    // Execute the command and wait for it to complete
    // The donut-proxy binary should start the worker and then exit
    let output = proxy_cmd
      .output()
      .await
      .map_err(|e| format!("Failed to execute donut-proxy: {e}"))?;

    if !output.status.success() {
      let stderr = String::from_utf8_lossy(&output.stderr);
      let stdout = String::from_utf8_lossy(&output.stdout);
      return Err(format!(
        "Proxy start failed - stdout: {stdout}, stderr: {stderr}"
      ));
    }

    let json_string =
      String::from_utf8(output.stdout).map_err(|e| format!("Failed to parse proxy output: {e}"))?;

    // Parse the JSON output
    let json: Value = serde_json::from_str(json_string.trim())
      .map_err(|e| format!("Failed to parse JSON: {e}. Output was: {}", json_string))?;

    // Extract proxy information
    let id = json["id"].as_str().ok_or("Missing proxy ID")?;
    let local_port = json["localPort"]
      .as_u64()
      .ok_or_else(|| format!("Missing local port in JSON: {}", json_string))?
      as u16;
    let local_url = json["localUrl"]
      .as_str()
      .ok_or_else(|| format!("Missing local URL in JSON: {}", json_string))?
      .to_string();

    let proxy_info = ProxyInfo {
      id: id.to_string(),
      local_url,
      upstream_host: proxy_settings
        .map(|p| p.host.clone())
        .unwrap_or_else(|| "DIRECT".to_string()),
      upstream_port: proxy_settings.map(|p| p.port).unwrap_or(0),
      upstream_type: proxy_settings
        .map(|p| p.proxy_type.clone())
        .unwrap_or_else(|| "DIRECT".to_string()),
      local_port,
      profile_id: profile_id.map(|s| s.to_string()),
      blocklist_file: blocklist_file.clone(),
    };

    // Wait for the local proxy port to be ready to accept connections
    {
      use tokio::net::TcpStream;
      use tokio::time::{sleep, Duration};
      let mut ready = false;
      for _ in 0..50 {
        match TcpStream::connect((std::net::Ipv4Addr::LOCALHOST, proxy_info.local_port)).await {
          Ok(_stream) => {
            ready = true;
            break;
          }
          Err(_) => {
            sleep(Duration::from_millis(100)).await;
          }
        }
      }
      if !ready {
        // The detached worker is already running with its config on disk, but
        // it was never registered in active_proxies — no cleanup pass could
        // ever find it, so it must be killed before this error propagates.
        let _ = crate::proxy_runner::stop_proxy_process(&proxy_info.id).await;
        return Err(format!(
          "Local proxy on 127.0.0.1:{} did not become ready in time",
          proxy_info.local_port
        ));
      }
    }

    // Store the proxy info
    {
      let mut proxies = self.active_proxies.lock().unwrap();
      proxies.insert(browser_pid, proxy_info.clone());
    }

    // Store the profile proxy info for persistence
    if let Some(id) = profile_id {
      if let Some(proxy_settings) = proxy_settings {
        let mut profile_proxies = self.profile_proxies.lock().unwrap();
        profile_proxies.insert(id.to_string(), proxy_settings.clone());
      }
      // Also record the active proxy id for this profile for quick cleanup on changes
      let mut map = self.profile_active_proxy_ids.lock().unwrap();
      map.insert(id.to_string(), proxy_info.id.clone());
    }

    // Return proxy settings for the browser
    Ok(ProxySettings {
      proxy_type: local_protocol.to_string(),
      host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility
      port: proxy_info.local_port,
      username: None,
      password: None,
      vless_uri: None,
    })
  }

  // Stop the proxy associated with a browser process ID
  pub async fn stop_proxy(
    &self,
    app_handle: tauri::AppHandle,
    browser_pid: u32,
  ) -> Result<(), String> {
    let (proxy_id, profile_id): (String, Option<String>) = {
      let mut proxies = self.active_proxies.lock().unwrap();
      match proxies.remove(&browser_pid) {
        Some(proxy) => (proxy.id, proxy.profile_id.clone()),
        None => return Ok(()), // No proxy to stop
      }
    };

    // Stop the proxy using the donut-proxy binary
    let proxy_cmd = app_handle
      .shell()
      .sidecar("donut-proxy")
      .map_err(|e| format!("Failed to create sidecar: {e}"))?
      .arg("proxy")
      .arg("stop")
      .arg("--id")
      .arg(&proxy_id);

    // A failed spawn (sidecar missing, permission denied, fd exhaustion) must
    // not panic the cleanup task — the proxy is already removed from tracking,
    // so degrade gracefully like the non-success branch below.
    match proxy_cmd.output().await {
      Ok(output) if !output.status.success() => {
        log::warn!(
          "Proxy stop error: {}",
          String::from_utf8_lossy(&output.stderr)
        );
      }
      Ok(_) => {}
      Err(e) => log::warn!("Failed to run donut-proxy stop: {e}"),
    }

    // Clear profile-to-proxy mapping if it references this proxy
    if let Some(id) = profile_id {
      let mut map = self.profile_active_proxy_ids.lock().unwrap();
      if let Some(current_id) = map.get(&id) {
        if current_id == &proxy_id {
          map.remove(&id);
        }
      }
    }

    // Emit event for reactive UI updates
    if let Err(e) = events::emit_empty("proxies-changed") {
      log::error!("Failed to emit proxies-changed event: {e}");
    }

    Ok(())
  }

  // Stop the proxy associated with a profile ID
  pub async fn stop_proxy_by_profile_id(
    &self,
    app_handle: tauri::AppHandle,
    profile_id: &str,
  ) -> Result<(), String> {
    // Find the proxy ID for this profile
    let proxy_id = {
      let map = self.profile_active_proxy_ids.lock().unwrap();
      map.get(profile_id).cloned()
    };

    if let Some(proxy_id) = proxy_id {
      // Find the PID for this proxy
      let pid = {
        let proxies = self.active_proxies.lock().unwrap();
        proxies.iter().find_map(|(pid, proxy)| {
          if proxy.id == proxy_id {
            Some(*pid)
          } else {
            None
          }
        })
      };

      if let Some(pid) = pid {
        // Use the existing stop_proxy method
        self.stop_proxy(app_handle, pid).await
      } else {
        // Proxy not found in active_proxies, try to stop it directly by ID
        let proxy_cmd = app_handle
          .shell()
          .sidecar("donut-proxy")
          .map_err(|e| format!("Failed to create sidecar: {e}"))?
          .arg("proxy")
          .arg("stop")
          .arg("--id")
          .arg(&proxy_id);

        // Don't panic if the sidecar can't be spawned — still clear the mapping.
        match proxy_cmd.output().await {
          Ok(output) if !output.status.success() => {
            log::warn!(
              "Proxy stop error: {}",
              String::from_utf8_lossy(&output.stderr)
            );
          }
          Ok(_) => {}
          Err(e) => log::warn!("Failed to run donut-proxy stop: {e}"),
        }

        // Clear profile-to-proxy mapping
        let mut map = self.profile_active_proxy_ids.lock().unwrap();
        map.remove(profile_id);

        // Emit event for reactive UI updates
        if let Err(e) = events::emit_empty("proxies-changed") {
          log::error!("Failed to emit proxies-changed event: {e}");
        }

        Ok(())
      }
    } else {
      // No proxy found for this profile
      Ok(())
    }
  }

  // Update the PID mapping for an existing proxy
  /// Re-key the in-memory map when a profile's browser PID changes, and rewrite
  /// the worker's on-disk owner identity to match.
  ///
  /// Persisting is not optional bookkeeping. The detached worker reaps itself by
  /// watching the identity on disk, so a re-key that only touched memory left
  /// the worker watching the PREVIOUS process: once that PID was recycled the
  /// worker saw a live "browser" forever and outlived both its browser and the
  /// GUI. Callers on the status-sync path (`profile::manager`) hit this every
  /// time a browser re-execs or restarts itself.
  pub fn update_proxy_pid(&self, old_pid: u32, new_pid: u32) -> Result<(), String> {
    let proxy_id = {
      let mut proxies = self.active_proxies.lock().unwrap();
      match proxies.remove(&old_pid) {
        Some(proxy_info) => {
          let id = proxy_info.id.clone();
          proxies.insert(new_pid, proxy_info);
          id
        }
        None => return Err(format!("No proxy found for PID {old_pid}")),
      }
    };

    if !persist_browser_identity(&proxy_id, new_pid) {
      log::warn!(
        "Re-keyed proxy {proxy_id} to browser PID {new_pid} in memory but could not persist it; \
         the detached worker is still watching the previous process"
      );
    }
    Ok(())
  }

  /// Persist the real browser PID onto the worker's on-disk config so the
  /// detached worker can self-terminate when that browser dies, independent of
  /// the GUI being alive. Resolved via the profile→proxy_id map rather than the
  /// PID-keyed `active_proxies` map: the latter is keyed by a per-launch
  /// placeholder until `update_proxy_pid` runs, so it is not a reliable way to
  /// find the worker for a profile mid-launch. Safe on the reuse
  /// path — it simply rewrites `browser_pid` to the new live PID. A `browser_pid`
  /// of 0 (launch failed to report a PID) is rejected so the caller can abort
  /// the launch instead of leaving a worker without a verified browser owner.
  pub fn set_browser_pid_for_profile(&self, profile_id: &str, browser_pid: u32) -> bool {
    if browser_pid == 0 {
      return false;
    }
    let Some(proxy_id) = self.resolve_proxy_id_for_profile(profile_id) else {
      return false;
    };
    persist_browser_identity(&proxy_id, browser_pid)
  }

  /// Find the worker serving a profile. Prefers the in-memory map, then falls
  /// back to the newest matching config on disk: after a GUI restart the map is
  /// empty, but a browser (and its worker) launched by the PREVIOUS GUI can
  /// still be running, and that worker's owner identity must stay refreshable.
  fn resolve_proxy_id_for_profile(&self, profile_id: &str) -> Option<String> {
    if let Some(id) = self
      .profile_active_proxy_ids
      .lock()
      .unwrap()
      .get(profile_id)
      .cloned()
    {
      return Some(id);
    }

    // Smallest age = most recently created worker for this profile.
    crate::proxy_storage::list_proxy_configs()
      .into_iter()
      .filter(|config| config.profile_id.as_deref() == Some(profile_id))
      .min_by_key(|config| crate::proxy_storage::proxy_config_age_secs(&config.id))
      .map(|config| config.id)
  }

  // Clean up proxies for dead browser processes
  // Only clean up orphaned config files where the proxy process itself is dead
  pub async fn cleanup_dead_proxies(
    &self,
    _app_handle: tauri::AppHandle,
  ) -> Result<Vec<u32>, String> {
    // Don't stop proxies for dead browser processes - let them run indefinitely
    // The proxy processes are idle and don't consume CPU when not in use
    // Only clean up config files where the proxy process itself is dead (see below)
    let dead_pids: Vec<u32> = Vec::new();

    // Clean up orphaned proxy configs (only where proxy process is definitely dead)
    // IMPORTANT: Only clean up configs where the proxy process itself is dead
    // If the proxy process is running (even if idle), leave it alone
    // The user doesn't care if proxy processes run indefinitely as long as they're not consuming CPU
    let orphaned_configs = {
      use crate::proxy_storage::{is_process_running, list_proxy_configs};

      let all_configs = list_proxy_configs();
      let tracked_proxy_ids: std::collections::HashSet<String> = {
        let proxies = self.active_proxies.lock().unwrap();
        proxies.values().map(|p| p.id.clone()).collect()
      };

      all_configs
        .into_iter()
        .filter(|config| {
          // If proxy is tracked in active_proxies, it's definitely not orphaned
          if tracked_proxy_ids.contains(&config.id) {
            return false;
          }

          // Creation time comes from the proxy ID (format: proxy_{timestamp}_{random}),
          // giving newly created proxies a grace period.
          let proxy_age = crate::proxy_storage::proxy_config_age_secs(&config.id);

          // Grace period: don't clean up proxies created in the last 120 seconds
          // This prevents race conditions during startup (increased from 60 to 120 for safety)
          if proxy_age < 120 {
            log::debug!(
              "Skipping cleanup of proxy {} - too new (age: {}s)",
              config.id,
              proxy_age
            );
            return false;
          }

          // ONLY clean up if we can verify the proxy process is dead
          // If proxy process is running, leave it alone (even if idle)
          if let Some(proxy_pid) = config.pid {
            // Check if proxy process is actually dead
            if !is_process_running(proxy_pid) {
              // Proxy process is dead, clean up the config file
              log::info!(
                "Proxy {} process (PID {}) is dead, will clean up config",
                config.id,
                proxy_pid
              );
              return true;
            }
            // Proxy process is running - leave it alone
            log::debug!(
              "Skipping cleanup of proxy {} - process (PID {}) is still running",
              config.id,
              proxy_pid
            );
            return false;
          }

          // No PID in config - can't verify if process is dead
          // Be conservative: don't clean up (might be starting up or PID not set yet)
          log::debug!(
            "Skipping cleanup of proxy {} - no PID in config (might be starting up)",
            config.id
          );
          false
        })
        .collect::<Vec<_>>()
    };

    // Clean up orphaned config files (proxy process is dead)
    for config in orphaned_configs {
      log::info!(
        "Cleaning up orphaned proxy config: {} (proxy process is dead)",
        config.id
      );
      use crate::proxy_storage::delete_proxy_config;
      delete_proxy_config(&config.id);
    }

    // Kill stale profileless proxy workers — these are check workers
    // (from check_proxy_validity or similar) that were never cleaned up.
    // Profile-associated proxies are left alone to avoid the regression
    // where killing proxies for "dead" browsers on Linux also killed
    // proxies for running browsers (due to launcher-vs-browser PID mismatch).
    {
      use crate::proxy_storage::{is_process_running, list_proxy_configs};

      let all_configs = list_proxy_configs();
      for config in all_configs {
        // Only target proxies WITHOUT a profile_id (check workers)
        if config.profile_id.is_some() {
          continue;
        }

        // Must have a running process to kill
        let Some(pid) = config.pid else { continue };
        if !is_process_running(pid) {
          continue;
        }

        // Check age: only kill if older than 5 minutes
        let proxy_age = crate::proxy_storage::proxy_config_age_secs(&config.id);

        if proxy_age > 300 {
          log::info!(
            "Killing stale profileless proxy {} (PID {}, age {}s)",
            config.id,
            pid,
            proxy_age
          );
          let _ = crate::proxy_runner::stop_proxy_process(&config.id).await;
        }
      }
    }

    // Kill proxy workers whose browser process has died.
    //
    // active_proxies is keyed by the EXACT browser PID that was recorded in
    // update_proxy_pid(). That PID is matched against a single process-table
    // snapshot AND, when the worker's config records one, against the browser's
    // start time — existence alone would let a recycled PID keep a dead
    // browser's worker alive indefinitely. This avoids the fuzzy
    // launcher-vs-browser detection used by check_browser_status (which
    // historically had false negatives on Linux and was the reason
    // profile-associated workers were left alone in the other cleanup branches).
    //
    // Without this, every time a user closes their browser via the window's
    // X button (bypassing Donut's stop flow) or the browser crashes, the
    // worker keeps running forever. On Windows users reported dozens of
    // donut-proxy processes accumulating this way.
    {
      // Snapshot current active entries first so we don't hold the mutex
      // while running the (expensive on Windows) sysinfo scan.
      let snapshot: Vec<(u32, String, Option<String>)> = {
        let proxies = self.active_proxies.lock().unwrap();
        proxies
          .iter()
          .map(|(&browser_pid, info)| (browser_pid, info.id.clone(), info.profile_id.clone()))
          .collect()
      };

      if !snapshot.is_empty() {
        // One process-table scan for all candidates
        let system = sysinfo::System::new_with_specifics(
          sysinfo::RefreshKind::nothing().with_processes(sysinfo::ProcessRefreshKind::everything()),
        );

        // Two-state classification: alive PIDs reset their miss counter,
        // dead PIDs increment it. A worker is only reaped after MISS_THRESHOLD
        // consecutive misses (~60s by default given the 30s cleanup cadence),
        // so a single sysinfo blip under heavy load doesn't kill a healthy worker.
        const MISS_THRESHOLD: u8 = 2;

        let mut alive_pids: Vec<u32> = Vec::new();
        let mut dead_candidates: Vec<(u32, String, Option<String>)> = Vec::new();
        let mut snapshot_pids: std::collections::HashSet<u32> = std::collections::HashSet::new();
        for (browser_pid, proxy_id, profile_id) in snapshot {
          snapshot_pids.insert(browser_pid);
          // Launch placeholders (and the legacy 0 sentinel) are not real
          // browser PIDs — update_proxy_pid hasn't recorded the real one yet.
          if browser_pid == 0 || is_launch_placeholder_pid(browser_pid) {
            continue;
          }
          // Reuse the single scan rather than re-querying per PID, but hold the
          // entry to the recorded identity when the worker has one.
          let expected_start_time = crate::proxy_storage::get_proxy_config(&proxy_id)
            .and_then(|config| config.browser_pid_start_time);
          let still_the_same_browser = match system.process(sysinfo::Pid::from_u32(browser_pid)) {
            // An exited-but-unreaped browser still occupies its PID in this
            // snapshot; only a genuinely live process counts.
            Some(process) if crate::proxy_storage::snapshot_process_is_alive(process) => {
              expected_start_time.is_none_or(|expected| {
                // Same PID, different process: the browser died and its PID was reused.
                process.start_time() == expected
              })
            }
            _ => false,
          };
          if still_the_same_browser {
            alive_pids.push(browser_pid);
          } else {
            dead_candidates.push((browser_pid, proxy_id, profile_id));
          }
        }

        let dead_browser_entries: Vec<(u32, String, Option<String>)> = {
          let mut misses = self.dead_browser_misses.lock().unwrap();
          // Forget PIDs no longer tracked at all (worker already torn down elsewhere).
          misses.retain(|pid, _| snapshot_pids.contains(pid));
          // Reset miss count for any PID that's currently alive.
          for pid in &alive_pids {
            misses.remove(pid);
          }
          // Increment dead candidates and select those past threshold.
          let mut to_reap = Vec::new();
          for (browser_pid, proxy_id, profile_id) in dead_candidates {
            let count = misses.entry(browser_pid).or_insert(0);
            *count = count.saturating_add(1);
            if *count >= MISS_THRESHOLD {
              misses.remove(&browser_pid);
              to_reap.push((browser_pid, proxy_id, profile_id));
            }
          }
          to_reap
        };

        for (browser_pid, proxy_id, profile_id) in dead_browser_entries {
          log::info!(
            "Cleanup: browser PID {} is dead, stopping proxy worker {} (profile={:?})",
            browser_pid,
            proxy_id,
            profile_id
          );
          {
            let mut proxies = self.active_proxies.lock().unwrap();
            // Re-check the entry still maps to the same proxy_id — another
            // thread may have replaced it with a new proxy since we snapshotted.
            if let Some(current) = proxies.get(&browser_pid) {
              if current.id != proxy_id {
                continue;
              }
            } else {
              continue;
            }
            proxies.remove(&browser_pid);
          }
          if let Some(ref pid) = profile_id {
            let mut map = self.profile_active_proxy_ids.lock().unwrap();
            if map.get(pid) == Some(&proxy_id) {
              map.remove(pid);
            }
          }
          let _ = crate::proxy_runner::stop_proxy_process(&proxy_id).await;
        }
      }
    }

    // Clean up orphaned VPN worker configs where the worker process is dead
    {
      use crate::proxy_storage::is_process_running;
      use crate::vpn_worker_storage::{delete_vpn_worker_config, list_vpn_worker_configs};

      let vpn_workers = list_vpn_worker_configs();
      for worker in vpn_workers {
        if let Some(pid) = worker.pid {
          if !is_process_running(pid) {
            log::info!(
              "Cleaning up orphaned VPN worker config: {} (process PID {} is dead)",
              worker.id,
              pid
            );
            let _ = std::fs::remove_file(&worker.config_file_path);
            delete_vpn_worker_config(&worker.id);
          }
        }
      }
    }

    {
      use crate::proxy_storage::process_identity_matches;
      use crate::xray_worker_storage::{list_xray_worker_configs, unstarted_worker_is_stale};

      for worker in list_xray_worker_configs() {
        let dead = worker
          .pid
          .is_some_and(|pid| !process_identity_matches(pid, worker.pid_start_time));
        if dead || unstarted_worker_is_stale(&worker) {
          log::info!(
            "Cleaning up orphaned Xray-core worker config: {}",
            worker.id
          );
          let _ = crate::xray_worker_runner::stop_xray_worker(&worker.id).await;
        }
      }
    }

    // Emit event for reactive UI updates
    if let Err(e) = events::emit_empty("proxies-changed") {
      log::error!("Failed to emit proxies-changed event: {e}");
    }

    Ok(dead_pids)
  }

  /// Snapshot the set of tracked proxy IDs (for asserting in tests).
  #[cfg(test)]
  fn tracked_proxy_ids(&self) -> std::collections::HashSet<String> {
    let proxies = self.active_proxies.lock().unwrap();
    proxies.values().map(|p| p.id.clone()).collect()
  }

  /// Snapshot active proxy count.
  #[cfg(test)]
  fn active_proxy_count(&self) -> usize {
    self.active_proxies.lock().unwrap().len()
  }

  /// Snapshot profile-to-proxy-id mapping count.
  #[cfg(test)]
  fn profile_proxy_mapping_count(&self) -> usize {
    self.profile_active_proxy_ids.lock().unwrap().len()
  }

  /// Insert a proxy info entry directly (for testing).
  #[cfg(test)]
  fn insert_active_proxy(&self, browser_pid: u32, info: ProxyInfo) {
    self
      .active_proxies
      .lock()
      .unwrap()
      .insert(browser_pid, info);
  }

  /// Insert a profile-to-proxy mapping directly (for testing).
  #[cfg(test)]
  fn insert_profile_proxy_mapping(&self, profile_id: String, proxy_id: String) {
    self
      .profile_active_proxy_ids
      .lock()
      .unwrap()
      .insert(profile_id, proxy_id);
  }

  /// Get active proxy info by browser PID (for testing).
  #[cfg(test)]
  fn get_active_proxy(&self, browser_pid: u32) -> Option<ProxyInfo> {
    self
      .active_proxies
      .lock()
      .unwrap()
      .get(&browser_pid)
      .cloned()
  }
}

// Create a singleton instance of the proxy manager
lazy_static::lazy_static! {
    pub static ref PROXY_MANAGER: ProxyManager = ProxyManager::new();
}

#[cfg(test)]
mod tests {
  use super::*;
  use std::env;
  use std::path::PathBuf;
  use std::time::Duration;
  use tokio::process::Command;
  use tokio::time::sleep;

  // Mock HTTP server for testing

  use http_body_util::Full;
  use hyper::body::Bytes;
  use hyper::server::conn::http1;
  use hyper::service::service_fn;
  use hyper::Response;
  use hyper_util::rt::TokioIo;
  use tokio::net::TcpListener;

  // Helper function to build donut-proxy binary for testing
  async fn ensure_donut_proxy_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
    let project_root = PathBuf::from(cargo_manifest_dir)
      .parent()
      .unwrap()
      .to_path_buf();
    let proxy_binary_name = if cfg!(windows) {
      "donut-proxy.exe"
    } else {
      "donut-proxy"
    };
    let proxy_binary = project_root
      .join("src-tauri")
      .join("target")
      .join("debug")
      .join(proxy_binary_name);

    // Check if binary already exists
    if proxy_binary.exists() {
      return Ok(proxy_binary);
    }

    // Build the donut-proxy binary
    println!("Building donut-proxy binary for tests...");

    let build_status = Command::new("cargo")
      .args(["build", "--bin", "donut-proxy"])
      .current_dir(project_root.join("src-tauri"))
      .status()
      .await?;

    if !build_status.success() {
      return Err("Failed to build donut-proxy binary".into());
    }

    if !proxy_binary.exists() {
      return Err("donut-proxy binary was not created successfully".into());
    }

    Ok(proxy_binary)
  }

  #[test]
  fn test_proxy_settings_validation() {
    // Test valid proxy settings
    let valid_settings = ProxySettings {
      proxy_type: "http".to_string(),
      host: "127.0.0.1".to_string(),
      port: 8080,
      username: Some("user".to_string()),
      password: Some("pass".to_string()),
      vless_uri: None,
    };

    assert!(
      !valid_settings.host.is_empty(),
      "Valid settings should have non-empty host"
    );
    assert!(
      valid_settings.port > 0,
      "Valid settings should have positive port"
    );
    assert_eq!(valid_settings.proxy_type, "http", "Proxy type should match");
    assert!(
      valid_settings.username.is_some(),
      "Username should be present"
    );
    assert!(
      valid_settings.password.is_some(),
      "Password should be present"
    );

    // Test proxy settings with empty values
    let empty_settings = ProxySettings {
      proxy_type: "http".to_string(),
      host: "".to_string(),
      port: 0,
      username: None,
      password: None,
      vless_uri: None,
    };

    assert!(
      empty_settings.host.is_empty(),
      "Empty settings should have empty host"
    );
    assert_eq!(
      empty_settings.port, 0,
      "Empty settings should have zero port"
    );
    assert!(empty_settings.username.is_none(), "Username should be None");
    assert!(empty_settings.password.is_none(), "Password should be None");
  }

  #[tokio::test]
  async fn test_proxy_manager_concurrent_access() {
    use std::sync::Arc;

    let proxy_manager = Arc::new(ProxyManager::new());
    let mut handles = vec![];

    // Spawn multiple tasks that access the proxy manager concurrently
    for i in 0..10 {
      let pm = proxy_manager.clone();
      let handle = tokio::spawn(async move {
        let browser_pid = (1000 + i) as u32;
        let proxy_info = ProxyInfo {
          id: format!("proxy_{i}"),
          local_url: format!("http://127.0.0.1:{}", 8000 + i),
          upstream_host: "127.0.0.1".to_string(),
          upstream_port: 3128,
          upstream_type: "http".to_string(),
          local_port: (8000 + i) as u16,
          profile_id: None,
          blocklist_file: None,
        };

        // Add proxy
        {
          let mut active_proxies = pm.active_proxies.lock().unwrap();
          active_proxies.insert(browser_pid, proxy_info);
        }

        browser_pid
      });
      handles.push(handle);
    }

    // Wait for all tasks to complete
    let results: Vec<u32> = futures_util::future::join_all(handles)
      .await
      .into_iter()
      .map(|r| r.unwrap())
      .collect();

    // Verify all browser PIDs were processed
    assert_eq!(results.len(), 10);
    for (i, &browser_pid) in results.iter().enumerate() {
      assert_eq!(browser_pid, (1000 + i) as u32);
    }
  }

  // Integration test that actually builds and uses donut-proxy binary
  #[tokio::test]
  async fn test_proxy_integration_with_real_proxy() -> Result<(), Box<dyn std::error::Error>> {
    // This test requires donut-proxy binary to be available
    // Skip if we can't find the binary or if proxy startup fails
    use crate::proxy_runner::{start_proxy_process, stop_proxy_process};
    use tokio::net::TcpStream;

    // Start a mock upstream HTTP server
    let upstream_listener = TcpListener::bind("127.0.0.1:0").await?;
    let upstream_addr = upstream_listener.local_addr()?;

    // Spawn upstream server
    let server_handle = tokio::spawn(async move {
      while let Ok((stream, _)) = upstream_listener.accept().await {
        let io = TokioIo::new(stream);
        tokio::task::spawn(async move {
          let _ = http1::Builder::new()
            .serve_connection(
              io,
              service_fn(|_req| async {
                Ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from("Upstream OK"))))
              }),
            )
            .await;
        });
      }
    });

    // Wait for server to start
    sleep(Duration::from_millis(100)).await;

    let upstream_url = format!("http://{}:{}", upstream_addr.ip(), upstream_addr.port());

    // Try to start proxy - if it fails, skip the test
    let config = match start_proxy_process(Some(upstream_url), None).await {
      Ok(config) => config,
      Err(e) => {
        println!("Skipping proxy integration test - proxy startup failed: {e}");
        server_handle.abort();
        return Ok(()); // Skip test instead of failing
      }
    };

    // Verify proxy configuration
    assert!(!config.id.is_empty());
    assert!(config.local_port.is_some());

    let proxy_id = config.id.clone();
    let local_port = config.local_port.unwrap();

    // Verify the local port is listening (should be fast now)
    match tokio::time::timeout(
      Duration::from_millis(500),
      TcpStream::connect(("127.0.0.1", local_port)),
    )
    .await
    {
      Ok(Ok(_)) => {
        println!("Proxy is listening on port {local_port}");
      }
      Ok(Err(e)) => {
        println!("Warning: Proxy port {local_port} is not listening: {e:?}");
        // Don't fail the test, just log a warning
      }
      Err(_) => {
        println!("Warning: Proxy port {local_port} connection check timed out");
        // Don't fail the test, just log a warning
      }
    }

    // Test stopping the proxy
    let stopped = stop_proxy_process(&proxy_id).await?;
    assert!(stopped);

    println!("Integration test passed: proxy start/stop works correctly");

    // Clean up server
    server_handle.abort();

    Ok(())
  }

  // Validate that non-secret proxy settings remain command arguments.
  #[test]
  fn test_proxy_command_construction() {
    let proxy_settings = ProxySettings {
      proxy_type: "http".to_string(),
      host: "proxy.example.com".to_string(),
      port: 8080,
      username: Some("user".to_string()),
      password: Some("pass".to_string()),
      vless_uri: None,
    };

    // Test command arguments match expected format
    let expected_args = [
      "proxy",
      "start",
      "--host",
      "proxy.example.com",
      "--proxy-port",
      "8080",
      "--type",
      "http",
    ];

    // This test verifies the argument structure without actually running the command
    assert_eq!(
      proxy_settings.host, "proxy.example.com",
      "Host should match expected value"
    );
    assert_eq!(
      proxy_settings.port, 8080,
      "Port should match expected value"
    );
    assert_eq!(
      proxy_settings.proxy_type, "http",
      "Proxy type should match expected value"
    );
    assert_eq!(
      proxy_settings.username.as_ref().unwrap(),
      "user",
      "Username should match expected value"
    );
    assert_eq!(
      proxy_settings.password.as_ref().unwrap(),
      "pass",
      "Password should match expected value"
    );

    // Verify expected args structure
    assert_eq!(expected_args[0], "proxy", "First arg should be 'proxy'");
    assert_eq!(expected_args[1], "start", "Second arg should be 'start'");
    assert_eq!(expected_args[2], "--host", "Third arg should be '--host'");
    assert_eq!(
      expected_args[3], "proxy.example.com",
      "Fourth arg should be host value"
    );
  }

  // Test the CLI detachment specifically - ensure the CLI exits properly
  #[tokio::test]
  async fn test_cli_exits_after_proxy_start() -> Result<(), Box<dyn std::error::Error>> {
    let proxy_path = ensure_donut_proxy_binary().await?;

    // Test that the CLI exits quickly with a mock upstream
    let mut cmd = Command::new(&proxy_path);
    cmd
      .arg("proxy")
      .arg("start")
      .arg("--host")
      .arg("httpbin.org")
      .arg("--proxy-port")
      .arg("80")
      .arg("--type")
      .arg("http");

    let start_time = std::time::Instant::now();
    let output = tokio::time::timeout(Duration::from_secs(10), cmd.output()).await;

    match output {
      Ok(Ok(cmd_output)) => {
        let execution_time = start_time.elapsed();

        if cmd_output.status.success() {
          let stdout = String::from_utf8(cmd_output.stdout)?;
          let config: serde_json::Value = serde_json::from_str(&stdout)?;

          // Clean up - try to stop the proxy
          if let Some(proxy_id) = config["id"].as_str() {
            let mut stop_cmd = Command::new(&proxy_path);
            stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
            let _ = stop_cmd.output().await;
          }
        }

        println!("CLI detachment test passed - CLI exited in {execution_time:?}");
      }
      Ok(Err(e)) => {
        return Err(format!("Command execution failed: {e}").into());
      }
      Err(_) => {
        return Err("CLI command timed out - this indicates improper detachment".into());
      }
    }

    Ok(())
  }

  // Test that validates proper CLI detachment behavior
  #[tokio::test]
  async fn test_cli_detachment_behavior() -> Result<(), Box<dyn std::error::Error>> {
    let proxy_path = ensure_donut_proxy_binary().await?;

    // Test that the CLI command exits quickly even with a real upstream
    let mut cmd = Command::new(&proxy_path);
    cmd
      .arg("proxy")
      .arg("start")
      .arg("--host")
      .arg("httpbin.org")
      .arg("--proxy-port")
      .arg("80")
      .arg("--type")
      .arg("http");

    let output = tokio::time::timeout(Duration::from_secs(10), cmd.output()).await??;

    if output.status.success() {
      let stdout = String::from_utf8(output.stdout)?;
      let config: serde_json::Value = serde_json::from_str(&stdout)?;
      let proxy_id = config["id"].as_str().unwrap();

      // Clean up
      let mut stop_cmd = Command::new(&proxy_path);
      stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
      let _ = stop_cmd.output().await;

      println!("CLI detachment test passed");
    } else {
      // Even if the upstream fails, the CLI should still exit quickly
      println!("CLI command failed but exited quickly as expected");
    }

    Ok(())
  }

  // ──────────────────────────────────────────────────────────────────────
  // Complex proxy process monitoring tests
  // ──────────────────────────────────────────────────────────────────────

  fn make_proxy_info(id: &str, port: u16, profile_id: Option<&str>) -> ProxyInfo {
    ProxyInfo {
      id: id.to_string(),
      local_url: format!("http://127.0.0.1:{port}"),
      upstream_host: "10.0.0.1".to_string(),
      upstream_port: 3128,
      upstream_type: "http".to_string(),
      local_port: port,
      profile_id: profile_id.map(|s| s.to_string()),
      blocklist_file: None,
    }
  }

  #[test]
  fn test_pid_mapping_lifecycle() {
    let pm = ProxyManager::new();

    // Initially empty
    assert_eq!(pm.active_proxy_count(), 0);

    // Register proxies for 3 browser PIDs
    pm.insert_active_proxy(1001, make_proxy_info("px_a", 9001, Some("profile_1")));
    pm.insert_active_proxy(1002, make_proxy_info("px_b", 9002, Some("profile_2")));
    pm.insert_active_proxy(1003, make_proxy_info("px_c", 9003, None));

    assert_eq!(pm.active_proxy_count(), 3);

    // Verify each PID resolves correctly
    let a = pm.get_active_proxy(1001).unwrap();
    assert_eq!(a.id, "px_a");
    assert_eq!(a.local_port, 9001);
    assert_eq!(a.profile_id.as_deref(), Some("profile_1"));

    let c = pm.get_active_proxy(1003).unwrap();
    assert!(c.profile_id.is_none());

    // Unknown PID returns None
    assert!(pm.get_active_proxy(9999).is_none());
  }

  #[test]
  fn test_update_proxy_pid_remaps_correctly() {
    let pm = ProxyManager::new();
    pm.insert_active_proxy(100, make_proxy_info("px_remap", 9010, Some("prof_a")));

    // Old PID 100 → new PID 200
    pm.update_proxy_pid(100, 200).unwrap();

    // Old PID should be gone
    assert!(pm.get_active_proxy(100).is_none());

    // New PID should have the same proxy info
    let info = pm.get_active_proxy(200).unwrap();
    assert_eq!(info.id, "px_remap");
    assert_eq!(info.local_port, 9010);
    assert_eq!(info.profile_id.as_deref(), Some("prof_a"));
  }

  /// Save a worker config for a profile in an isolated cache dir and return its id.
  fn saved_worker_config(profile_id: Option<&str>) -> String {
    let id = crate::proxy_storage::generate_proxy_id();
    let config = crate::proxy_storage::ProxyConfig::new(id.clone(), "DIRECT".to_string(), Some(0))
      .with_profile_id(profile_id.map(str::to_string));
    crate::proxy_storage::save_proxy_config(&config).unwrap();
    id
  }

  #[test]
  fn browser_identity_records_the_pid_and_its_start_time() {
    let temp = tempfile::tempdir().unwrap();
    let _cache_guard = crate::app_dirs::set_test_cache_dir(temp.path().to_path_buf());
    let id = saved_worker_config(Some("prof_identity"));

    let pid = std::process::id();
    assert!(persist_browser_identity(&id, pid));

    let saved = crate::proxy_storage::get_proxy_config(&id).unwrap();
    assert_eq!(saved.browser_pid, Some(pid));
    assert_eq!(
      saved.browser_pid_start_time,
      crate::proxy_storage::process_start_time(pid),
      "the start time must pin the PID to this exact process"
    );
    assert!(crate::proxy_storage::browser_owner_is_alive(&saved));
  }

  #[test]
  fn browser_identity_refuses_owners_that_are_not_real_browsers() {
    let temp = tempfile::tempdir().unwrap();
    let _cache_guard = crate::app_dirs::set_test_cache_dir(temp.path().to_path_buf());
    let id = saved_worker_config(Some("prof_reject"));

    // 0 and launch placeholders mean "no browser reported yet". Recording one
    // would leave a worker whose owner can never die, so it must fail loudly
    // enough for the caller to abort the launch.
    assert!(!persist_browser_identity(&id, 0));
    assert!(!persist_browser_identity(
      &id,
      next_launch_placeholder_pid()
    ));
    // A PID with no process behind it can't be pinned to an identity either.
    assert!(!persist_browser_identity(&id, u32::MAX));
    // An unknown worker is not silently created.
    assert!(!persist_browser_identity(
      "proxy_1_missing",
      std::process::id()
    ));

    let saved = crate::proxy_storage::get_proxy_config(&id).unwrap();
    assert_eq!(saved.browser_pid, None);
    assert_eq!(saved.browser_pid_start_time, None);
  }

  /// The regression behind the orphaned-worker reports: the status synchronizer
  /// re-keys a profile's browser PID whenever the browser re-execs, and that
  /// change has to reach the worker's config. When it only landed in memory the
  /// detached worker kept watching the PREVIOUS process — and once that PID was
  /// recycled it saw a live "browser" forever and outlived everything.
  #[test]
  fn remapping_a_browser_pid_rewrites_the_workers_on_disk_owner() {
    let temp = tempfile::tempdir().unwrap();
    let _cache_guard = crate::app_dirs::set_test_cache_dir(temp.path().to_path_buf());
    let id = saved_worker_config(Some("prof_remap"));

    let pm = ProxyManager::new();
    pm.insert_active_proxy(100, make_proxy_info(&id, 9010, Some("prof_remap")));
    let stale_start_time = 1;
    let mut seeded = crate::proxy_storage::get_proxy_config(&id).unwrap();
    seeded.browser_pid = Some(100);
    seeded.browser_pid_start_time = Some(stale_start_time);
    assert!(crate::proxy_storage::update_proxy_config(&seeded));

    let live_pid = std::process::id();
    pm.update_proxy_pid(100, live_pid).unwrap();

    let saved = crate::proxy_storage::get_proxy_config(&id).unwrap();
    assert_eq!(saved.browser_pid, Some(live_pid));
    assert_ne!(saved.browser_pid_start_time, Some(stale_start_time));
    assert!(
      crate::proxy_storage::browser_owner_is_alive(&saved),
      "the worker must now be watching the browser that is actually running"
    );
  }

  /// After a GUI restart the profile→proxy map is empty, but a browser launched
  /// by the previous GUI can still be running with its worker attached. That
  /// worker's owner still has to be refreshable, so the lookup falls back to disk.
  #[test]
  fn the_worker_for_a_profile_is_found_on_disk_when_memory_is_empty() {
    let temp = tempfile::tempdir().unwrap();
    let _cache_guard = crate::app_dirs::set_test_cache_dir(temp.path().to_path_buf());
    let id = saved_worker_config(Some("prof_restart"));
    let _other = saved_worker_config(Some("prof_unrelated"));

    let pm = ProxyManager::new();
    assert_eq!(
      pm.resolve_proxy_id_for_profile("prof_restart").as_deref(),
      Some(id.as_str())
    );
    assert!(pm.resolve_proxy_id_for_profile("prof_absent").is_none());

    let live_pid = std::process::id();
    assert!(pm.set_browser_pid_for_profile("prof_restart", live_pid));
    let saved = crate::proxy_storage::get_proxy_config(&id).unwrap();
    assert_eq!(saved.browser_pid, Some(live_pid));
    assert!(crate::proxy_storage::browser_owner_is_alive(&saved));
  }

  #[test]
  fn the_in_memory_mapping_wins_over_the_on_disk_scan() {
    let temp = tempfile::tempdir().unwrap();
    let _cache_guard = crate::app_dirs::set_test_cache_dir(temp.path().to_path_buf());
    let stale = saved_worker_config(Some("prof_both"));
    let current = saved_worker_config(Some("prof_both"));

    let pm = ProxyManager::new();
    pm.profile_active_proxy_ids
      .lock()
      .unwrap()
      .insert("prof_both".to_string(), current.clone());

    assert_eq!(
      pm.resolve_proxy_id_for_profile("prof_both").as_deref(),
      Some(current.as_str())
    );
    assert!(pm.set_browser_pid_for_profile("prof_both", std::process::id()));
    assert_eq!(
      crate::proxy_storage::get_proxy_config(&stale)
        .unwrap()
        .browser_pid,
      None,
      "only the tracked worker may be re-pointed"
    );
  }

  #[test]
  fn test_update_proxy_pid_error_for_unknown_pid() {
    let pm = ProxyManager::new();
    let result = pm.update_proxy_pid(777, 888);
    assert!(result.is_err());
    assert!(result.unwrap_err().contains("No proxy found for PID 777"));
  }

  #[test]
  fn test_launch_placeholder_pids_unique_and_reconcile_independently() {
    let a = next_launch_placeholder_pid();
    let b = next_launch_placeholder_pid();
    assert_ne!(a, b);
    assert!(is_launch_placeholder_pid(a));
    assert!(is_launch_placeholder_pid(b));
    // Real PIDs (and the legacy 0 sentinel) are never in the placeholder range.
    assert!(!is_launch_placeholder_pid(0));
    assert!(!is_launch_placeholder_pid(1));
    assert!(!is_launch_placeholder_pid(4_194_304)); // Linux pid_max
    assert!(!is_launch_placeholder_pid(std::process::id()));

    // Two concurrent launches with distinct placeholder keys: finishing
    // launch A must not remap or evict launch B's in-flight entry.
    let pm = ProxyManager::new();
    pm.insert_active_proxy(a, make_proxy_info("px_launch_a", 9201, Some("prof_a")));
    pm.insert_active_proxy(b, make_proxy_info("px_launch_b", 9202, Some("prof_b")));

    pm.update_proxy_pid(a, 3001).unwrap();
    assert_eq!(pm.get_active_proxy(3001).unwrap().id, "px_launch_a");
    assert_eq!(pm.get_active_proxy(b).unwrap().id, "px_launch_b");

    pm.update_proxy_pid(b, 3002).unwrap();
    assert_eq!(pm.get_active_proxy(3002).unwrap().id, "px_launch_b");
    assert!(pm.get_active_proxy(a).is_none());
    assert!(pm.get_active_proxy(b).is_none());
  }

  #[test]
  fn test_profile_proxy_id_mapping_tracks_active_proxy() {
    let pm = ProxyManager::new();

    pm.insert_active_proxy(500, make_proxy_info("px_1", 9100, Some("profile_x")));
    pm.insert_profile_proxy_mapping("profile_x".to_string(), "px_1".to_string());

    // Verify mapping exists
    {
      let map = pm.profile_active_proxy_ids.lock().unwrap();
      assert_eq!(map.get("profile_x").unwrap(), "px_1");
    }

    // Simulate profile-specific cleanup: remove the profile mapping
    {
      let mut map = pm.profile_active_proxy_ids.lock().unwrap();
      map.remove("profile_x");
    }

    assert_eq!(pm.profile_proxy_mapping_count(), 0);
    // Active proxy itself should still be there
    assert_eq!(pm.active_proxy_count(), 1);
  }

  #[test]
  fn test_tracked_proxy_ids_returns_all_unique_ids() {
    let pm = ProxyManager::new();
    pm.insert_active_proxy(1, make_proxy_info("alpha", 8001, None));
    pm.insert_active_proxy(2, make_proxy_info("beta", 8002, None));
    pm.insert_active_proxy(3, make_proxy_info("gamma", 8003, None));

    let ids = pm.tracked_proxy_ids();
    assert_eq!(ids.len(), 3);
    assert!(ids.contains("alpha"));
    assert!(ids.contains("beta"));
    assert!(ids.contains("gamma"));
  }

  #[tokio::test]
  async fn test_concurrent_pid_registration_and_removal() {
    use std::sync::Arc;

    let pm = Arc::new(ProxyManager::new());
    let mut handles = vec![];

    // Phase 1: concurrent insertion of 50 proxies
    for i in 0..50 {
      let pm = pm.clone();
      handles.push(tokio::spawn(async move {
        let pid = 2000 + i as u32;
        let info = make_proxy_info(&format!("px_{i}"), 7000 + i as u16, None);
        pm.insert_active_proxy(pid, info);
      }));
    }
    for h in handles.drain(..) {
      h.await.unwrap();
    }
    assert_eq!(pm.active_proxy_count(), 50);

    // Phase 2: concurrent removal of half the proxies
    for i in (0..50).step_by(2) {
      let pm = pm.clone();
      handles.push(tokio::spawn(async move {
        let pid = 2000 + i as u32;
        let mut proxies = pm.active_proxies.lock().unwrap();
        proxies.remove(&pid);
      }));
    }
    for h in handles.drain(..) {
      h.await.unwrap();
    }
    assert_eq!(pm.active_proxy_count(), 25);

    // Phase 3: remaining proxies should all have odd indices
    let proxies = pm.active_proxies.lock().unwrap();
    for (&pid, info) in proxies.iter() {
      let idx = (pid - 2000) as usize;
      assert!(idx % 2 == 1, "Only odd-index proxies should remain");
      assert_eq!(info.id, format!("px_{idx}"));
    }
  }

  #[test]
  fn test_process_running_detection_with_child_lifecycle() {
    use crate::proxy_storage::is_process_running;

    // Spawn a long-lived child so we can check while it runs.
    // On Windows, `timeout` requires console input and exits immediately in
    // non-interactive contexts, so use `ping` with a high count instead.
    let mut child = std::process::Command::new(if cfg!(windows) { "ping" } else { "sleep" })
      .args(if cfg!(windows) {
        vec!["-n", "100", "127.0.0.1"]
      } else {
        vec!["10"]
      })
      .stdout(std::process::Stdio::null())
      .stderr(std::process::Stdio::null())
      .spawn()
      .expect("spawn long-lived child");

    let pid = child.id();

    // Process should be alive
    assert!(
      is_process_running(pid),
      "Child process must be detected as running (PID {pid})"
    );

    // Kill it
    child.kill().expect("kill child");
    child.wait().expect("wait child");

    // Process should now be dead
    assert!(
      !is_process_running(pid),
      "Killed child must be detected as dead (PID {pid})"
    );
  }

  #[tokio::test]
  async fn test_cleanup_distinguishes_live_and_dead_proxy_configs() {
    use crate::proxy_storage::{save_proxy_config, ProxyConfig};

    // Spawn a live child process to use its PID.
    // On Windows, `timeout` requires console input and exits immediately in CI,
    // so use `ping` which works reliably in non-interactive contexts.
    let mut live_child = std::process::Command::new(if cfg!(windows) { "ping" } else { "sleep" })
      .args(if cfg!(windows) {
        vec!["-n", "30", "127.0.0.1"]
      } else {
        vec!["30"]
      })
      .stdout(std::process::Stdio::null())
      .stderr(std::process::Stdio::null())
      .spawn()
      .expect("spawn live child");
    let live_pid = live_child.id();

    // Spawn and kill a short-lived process to get a dead PID
    let dead_child = std::process::Command::new(if cfg!(windows) { "cmd" } else { "true" })
      .args(if cfg!(windows) {
        vec!["/C", "exit"]
      } else {
        vec![]
      })
      .spawn()
      .expect("spawn dead child");
    let dead_pid = dead_child.id();
    let mut dead_child = dead_child;
    dead_child.wait().expect("wait for dead child");

    // Use an old timestamp so the configs aren't in the grace period
    let old_ts = SystemTime::now()
      .duration_since(UNIX_EPOCH)
      .unwrap()
      .as_secs()
      - 300; // 5 minutes ago

    // Save both proxy configs to disk
    let live_id = format!("proxy_{old_ts}_11111");
    let dead_id = format!("proxy_{old_ts}_22222");

    let live_config = ProxyConfig {
      id: live_id.clone(),
      upstream_url: "DIRECT".to_string(),
      local_port: Some(19001),
      ignore_proxy_certificate: None,
      local_url: Some("http://127.0.0.1:19001".to_string()),
      pid: Some(live_pid),
      profile_id: None,
      bypass_rules: Vec::new(),
      blocklist_file: None,
      dns_allowlist_mode: false,
      local_protocol: None,
      browser_pid: None,
      browser_pid_start_time: None,
    };
    let dead_config = ProxyConfig {
      id: dead_id.clone(),
      upstream_url: "DIRECT".to_string(),
      local_port: Some(19002),
      ignore_proxy_certificate: None,
      local_url: Some("http://127.0.0.1:19002".to_string()),
      pid: Some(dead_pid),
      profile_id: None,
      bypass_rules: Vec::new(),
      blocklist_file: None,
      dns_allowlist_mode: false,
      local_protocol: None,
      browser_pid: None,
      browser_pid_start_time: None,
    };

    save_proxy_config(&live_config).unwrap();
    save_proxy_config(&dead_config).unwrap();

    // Verify is_process_running differentiates them
    assert!(
      crate::proxy_storage::is_process_running(live_pid),
      "Live PID should be detected"
    );
    assert!(
      !crate::proxy_storage::is_process_running(dead_pid),
      "Dead PID should not be detected"
    );

    // Clean up
    live_child.kill().expect("kill live child");
    live_child.wait().expect("wait live child");
    crate::proxy_storage::delete_proxy_config(&live_id);
    crate::proxy_storage::delete_proxy_config(&dead_id);
  }

  #[test]
  fn test_proxy_config_persistence_roundtrip() {
    use crate::proxy_storage::{
      delete_proxy_config, generate_proxy_id, get_proxy_config, save_proxy_config, ProxyConfig,
    };

    let id = generate_proxy_id();
    let config = ProxyConfig {
      id: id.clone(),
      upstream_url: "socks5://user:pass@10.0.0.1:1080".to_string(),
      local_port: Some(18080),
      ignore_proxy_certificate: Some(true),
      local_url: Some("http://127.0.0.1:18080".to_string()),
      pid: Some(12345),
      profile_id: Some("prof_abc".to_string()),
      bypass_rules: vec!["*.local".to_string(), "192.168.*".to_string()],
      blocklist_file: None,
      dns_allowlist_mode: false,
      local_protocol: None,
      browser_pid: None,
      browser_pid_start_time: None,
    };

    // Save
    save_proxy_config(&config).unwrap();

    #[cfg(unix)]
    {
      use std::os::unix::fs::PermissionsExt;
      let mode =
        std::fs::metadata(crate::proxy_storage::get_storage_dir().join(format!("{id}.json")))
          .unwrap()
          .permissions()
          .mode()
          & 0o777;
      assert_eq!(mode, 0o600, "proxy credentials must be owner-only");
    }

    // Load and compare
    let loaded = get_proxy_config(&id).expect("Config should be loadable");
    assert_eq!(loaded.id, config.id);
    assert_eq!(loaded.upstream_url, config.upstream_url);
    assert_eq!(loaded.local_port, config.local_port);
    assert_eq!(
      loaded.ignore_proxy_certificate,
      config.ignore_proxy_certificate
    );
    assert_eq!(loaded.local_url, config.local_url);
    assert_eq!(loaded.pid, config.pid);
    assert_eq!(loaded.profile_id, config.profile_id);
    assert_eq!(loaded.bypass_rules, config.bypass_rules);

    // Clean up
    assert!(delete_proxy_config(&id));
    assert!(get_proxy_config(&id).is_none());
  }

  #[test]
  fn test_proxy_config_update_preserves_fields() {
    use crate::proxy_storage::{
      delete_proxy_config, get_proxy_config, save_proxy_config, update_proxy_config, ProxyConfig,
    };

    let id = format!("proxy_test_update_{}", rand::random::<u32>());
    let mut config = ProxyConfig::new(id.clone(), "DIRECT".to_string(), Some(17777));
    config.pid = Some(99999);
    config.profile_id = Some("prof_up".to_string());
    config.bypass_rules = vec!["google.com".to_string()];

    save_proxy_config(&config).unwrap();

    // Update: change the local_url (simulates worker binding)
    config.local_url = Some("http://127.0.0.1:17777".to_string());
    assert!(update_proxy_config(&config));

    let reloaded = get_proxy_config(&id).unwrap();
    assert_eq!(
      reloaded.local_url.as_deref(),
      Some("http://127.0.0.1:17777")
    );
    // Other fields should be preserved
    assert_eq!(reloaded.pid, Some(99999));
    assert_eq!(reloaded.bypass_rules, vec!["google.com".to_string()]);

    delete_proxy_config(&id);
  }

  #[test]
  fn test_proxy_config_list_filters_json_only() {
    use crate::proxy_storage::{
      delete_proxy_config, list_proxy_configs, save_proxy_config, ProxyConfig,
    };

    let id1 = format!("proxy_list_test_{}", rand::random::<u32>());
    let id2 = format!("proxy_list_test_{}", rand::random::<u32>());

    let c1 = ProxyConfig::new(id1.clone(), "DIRECT".to_string(), Some(16001));
    let c2 = ProxyConfig::new(id2.clone(), "DIRECT".to_string(), Some(16002));

    save_proxy_config(&c1).unwrap();
    save_proxy_config(&c2).unwrap();

    let all = list_proxy_configs();
    let our_ids: Vec<_> = all.iter().filter(|c| c.id == id1 || c.id == id2).collect();
    assert_eq!(our_ids.len(), 2, "Both test configs should be listed");

    delete_proxy_config(&id1);
    delete_proxy_config(&id2);
  }

  #[test]
  fn test_proxy_id_uniqueness_and_format() {
    use crate::proxy_storage::generate_proxy_id;

    let mut ids = std::collections::HashSet::new();
    for _ in 0..100 {
      let id = generate_proxy_id();
      assert!(id.starts_with("proxy_"), "ID must start with proxy_");
      // Format: proxy_{timestamp}_{random}
      let parts: Vec<&str> = id.split('_').collect();
      assert_eq!(
        parts.len(),
        3,
        "ID should have exactly 3 underscore-separated parts"
      );
      assert!(
        parts[1].parse::<u64>().is_ok(),
        "Second part must be a unix timestamp"
      );
      assert!(
        parts[2].parse::<u32>().is_ok(),
        "Third part must be a u32 random"
      );
      ids.insert(id);
    }
    assert_eq!(ids.len(), 100, "All 100 generated IDs must be unique");
  }

  #[test]
  fn test_multiple_profiles_share_proxy_independently() {
    let pm = ProxyManager::new();

    // Two profiles sharing the same upstream but with distinct proxy instances
    let info_a = ProxyInfo {
      id: "px_shared_a".to_string(),
      local_url: "http://127.0.0.1:9201".to_string(),
      upstream_host: "proxy.shared.com".to_string(),
      upstream_port: 8080,
      upstream_type: "http".to_string(),
      local_port: 9201,
      profile_id: Some("profile_alpha".to_string()),
      blocklist_file: None,
    };
    let info_b = ProxyInfo {
      id: "px_shared_b".to_string(),
      local_url: "http://127.0.0.1:9202".to_string(),
      upstream_host: "proxy.shared.com".to_string(),
      upstream_port: 8080,
      upstream_type: "http".to_string(),
      local_port: 9202,
      profile_id: Some("profile_beta".to_string()),
      blocklist_file: None,
    };

    pm.insert_active_proxy(3001, info_a);
    pm.insert_active_proxy(3002, info_b);
    pm.insert_profile_proxy_mapping("profile_alpha".to_string(), "px_shared_a".to_string());
    pm.insert_profile_proxy_mapping("profile_beta".to_string(), "px_shared_b".to_string());

    // Remove alpha's browser → should NOT affect beta
    {
      let mut proxies = pm.active_proxies.lock().unwrap();
      proxies.remove(&3001);
    }
    {
      let mut map = pm.profile_active_proxy_ids.lock().unwrap();
      map.remove("profile_alpha");
    }

    assert_eq!(pm.active_proxy_count(), 1);
    assert_eq!(pm.profile_proxy_mapping_count(), 1);
    let remaining = pm.get_active_proxy(3002).unwrap();
    assert_eq!(remaining.id, "px_shared_b");
    assert_eq!(remaining.profile_id.as_deref(), Some("profile_beta"));
  }

  #[test]
  fn test_proxy_url_construction() {
    // Basic HTTP
    let url = ProxyManager::build_proxy_url(&ProxySettings {
      proxy_type: "http".to_string(),
      host: "1.2.3.4".to_string(),
      port: 8080,
      username: None,
      password: None,
      vless_uri: None,
    });
    assert_eq!(url, "http://1.2.3.4:8080");

    // With credentials
    let url = ProxyManager::build_proxy_url(&ProxySettings {
      proxy_type: "socks5".to_string(),
      host: "proxy.example.com".to_string(),
      port: 1080,
      username: Some("user".to_string()),
      password: Some("p@ss".to_string()),
      vless_uri: None,
    });
    assert_eq!(url, "socks5://user:p%40ss@proxy.example.com:1080");

    // Username-only (no password)
    let url = ProxyManager::build_proxy_url(&ProxySettings {
      proxy_type: "http".to_string(),
      host: "host.io".to_string(),
      port: 3128,
      username: Some("justuser".to_string()),
      password: None,
      vless_uri: None,
    });
    assert_eq!(url, "http://justuser@host.io:3128");
  }

  fn valid_vless_uri() -> String {
    use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};

    let public_key = URL_SAFE_NO_PAD.encode([7_u8; 32]);
    format!(
      "vless://6d6e21a1-4829-4d2b-bc7f-1b25707b61e4@127.0.0.1:443?\
       encryption=none&flow=xtls-rprx-vision&security=reality&\
       sni=www.example.com&fp=chrome&pbk={public_key}&\
       sid=0123456789abcdef&spx=%2F&type=tcp&headerType=none#Local"
    )
  }

  #[test]
  fn vless_settings_are_validated_canonicalized_and_stripped_of_unused_credentials() {
    let uri = valid_vless_uri();
    let normalized = ProxyManager::normalize_proxy_settings(ProxySettings {
      proxy_type: "VLESS".to_string(),
      host: "ignored.invalid".to_string(),
      port: 1,
      username: Some("unused-user".to_string()),
      password: Some("unused-password".to_string()),
      vless_uri: Some(uri.clone()),
    })
    .unwrap();

    assert_eq!(normalized.proxy_type, "vless");
    assert_eq!(normalized.host, "127.0.0.1");
    assert_eq!(normalized.port, 443);
    assert!(normalized.username.is_none());
    assert!(normalized.password.is_none());
    assert_eq!(normalized.vless_uri.as_deref(), Some(uri.as_str()));

    let invalid = uri.replace("security=reality", "security=tls");
    let error = ProxyManager::normalize_proxy_settings(ProxySettings {
      proxy_type: "vless".to_string(),
      host: "127.0.0.1".to_string(),
      port: 443,
      username: None,
      password: None,
      vless_uri: Some(invalid.clone()),
    })
    .unwrap_err();
    assert!(error.contains("VLESS_CONFIG_INVALID"));
    assert!(!error.contains(&invalid));
  }

  #[test]
  fn vless_stored_proxy_persistence_and_exports_preserve_the_canonical_uri() {
    let temp = tempfile::tempdir().unwrap();
    let _data_guard = crate::app_dirs::set_test_data_dir(temp.path().to_path_buf());
    let manager = ProxyManager::new();
    let settings = ProxyManager::normalize_proxy_settings(ProxySettings {
      proxy_type: "vless".to_string(),
      host: "ignored.invalid".to_string(),
      port: 1,
      username: Some("unused".to_string()),
      password: Some("unused".to_string()),
      vless_uri: Some(valid_vless_uri()),
    })
    .unwrap();
    let stored = StoredProxy::new("Local Reality".to_string(), settings);
    manager.save_proxy(&stored).unwrap();
    manager.upsert_stored_proxy(stored.clone());

    let json: ProxyExportData =
      serde_json::from_str(&manager.export_proxies_json().unwrap()).unwrap();
    assert_eq!(json.proxies.len(), 1);
    assert_eq!(json.proxies[0].proxy_type, "vless");
    assert_eq!(json.proxies[0].vless_uri, stored.proxy_settings.vless_uri);
    assert_eq!(
      manager.export_proxies_txt(),
      stored.proxy_settings.vless_uri.clone().unwrap()
    );

    let reloaded = ProxyManager::new()
      .get_stored_proxies()
      .into_iter()
      .find(|candidate| candidate.id == stored.id)
      .expect("persisted VLESS proxy should reload");
    assert_eq!(reloaded.proxy_settings, stored.proxy_settings);

    let parsed = ProxyManager::parse_txt_proxies(&manager.export_proxies_txt());
    assert!(matches!(
      parsed.as_slice(),
      [ProxyParseResult::Parsed(proxy)]
        if proxy.proxy_type == "vless"
          && proxy.vless_uri == stored.proxy_settings.vless_uri
    ));
  }

  #[test]
  fn test_geo_username_construction() {
    // Country only
    let u = ProxyManager::build_geo_username("base_user", "US", &None, &None, &None);
    assert_eq!(u, "base_user-country-US");

    // Country + region
    let u = ProxyManager::build_geo_username(
      "base_user",
      "US",
      &Some("california".to_string()),
      &None,
      &None,
    );
    assert_eq!(u, "base_user-country-US-region-california");

    // All fields
    let u = ProxyManager::build_geo_username(
      "user",
      "DE",
      &Some("bavaria".to_string()),
      &Some("munich".to_string()),
      &Some("Telekom".to_string()),
    );
    assert_eq!(u, "user-country-DE-region-bavaria-city-munich-isp-Telekom");
  }

  #[test]
  fn test_sid_generation_determinism_and_format() {
    let sid1 = ProxyManager::generate_sid_for_profile("my-profile-uuid");
    let sid2 = ProxyManager::generate_sid_for_profile("my-profile-uuid");
    assert_eq!(sid1, sid2, "Same input must produce same SID");
    assert_eq!(sid1.len(), 11, "SID must be exactly 11 characters");

    // All chars should be alphanumeric lowercase
    assert!(
      sid1
        .chars()
        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
      "SID chars must be [a-z0-9]"
    );

    // Different profiles produce different SIDs
    let sid3 = ProxyManager::generate_sid_for_profile("another-profile");
    assert_ne!(sid1, sid3, "Different profiles must produce different SIDs");
  }

  #[test]
  fn test_build_username_with_sid() {
    let full = ProxyManager::build_username_with_sid("user-country-US", "profile-123");
    // Should contain the geo base, then -sid-{11chars}-ttl-1440m
    assert!(full.starts_with("user-country-US-sid-"));
    assert!(full.ends_with("-ttl-1440m"));
    // SID portion
    let after_sid = full.strip_prefix("user-country-US-sid-").unwrap();
    let sid = after_sid.strip_suffix("-ttl-1440m").unwrap();
    assert_eq!(sid.len(), 11);
  }

  #[test]
  fn test_stored_proxy_geo_field_migration() {
    // Simulate legacy data with geo_state but no geo_region
    let mut proxy = StoredProxy {
      id: "test_migrate".to_string(),
      name: "Test".to_string(),
      proxy_settings: ProxySettings {
        proxy_type: "http".to_string(),
        host: "h.com".to_string(),
        port: 80,
        username: None,
        password: None,
        vless_uri: None,
      },
      sync_enabled: false,
      last_sync: None,
      updated_at: None,
      is_cloud_managed: false,
      is_cloud_derived: false,
      geo_country: Some("US".to_string()),
      geo_state: Some("california".to_string()),
      geo_region: None,
      geo_city: None,
      geo_isp: None,
      dynamic_proxy_url: None,
      dynamic_proxy_format: None,
    };

    // Before migration
    assert_eq!(proxy.effective_region().unwrap(), "california");
    assert!(proxy.geo_region.is_none());

    // After migration
    proxy.migrate_geo_fields();
    assert_eq!(proxy.geo_region.as_deref(), Some("california"));
    assert!(proxy.geo_state.is_none(), "geo_state should be taken");
    assert_eq!(proxy.effective_region().unwrap(), "california");
  }

  #[test]
  fn test_cleanup_skips_recently_created_configs() {
    use crate::proxy_storage::{delete_proxy_config, save_proxy_config, ProxyConfig};

    // Use current timestamp so it falls within the 120s grace period
    let now_ts = SystemTime::now()
      .duration_since(UNIX_EPOCH)
      .unwrap()
      .as_secs();

    let recent_id = format!("proxy_{now_ts}_99999");

    // Spawn and kill a child so the PID is dead
    let dead_child = std::process::Command::new(if cfg!(windows) { "cmd" } else { "true" })
      .args(if cfg!(windows) {
        vec!["/C", "exit"]
      } else {
        vec![]
      })
      .spawn()
      .unwrap();
    let dead_pid = dead_child.id();
    let mut dead_child = dead_child;
    dead_child.wait().unwrap();

    let config = ProxyConfig {
      id: recent_id.clone(),
      upstream_url: "DIRECT".to_string(),
      local_port: Some(19999),
      ignore_proxy_certificate: None,
      local_url: None,
      pid: Some(dead_pid),
      profile_id: None,
      bypass_rules: Vec::new(),
      blocklist_file: None,
      dns_allowlist_mode: false,
      local_protocol: None,
      browser_pid: None,
      browser_pid_start_time: None,
    };
    save_proxy_config(&config).unwrap();

    // The cleanup logic inspects the timestamp in the proxy ID.
    // Since we used the current timestamp, the proxy_age will be < 120 seconds,
    // so it should be skipped despite the dead PID.

    // Verify the grace period logic directly:
    let proxy_age = recent_id
      .strip_prefix("proxy_")
      .and_then(|s| s.split('_').next())
      .and_then(|s| s.parse::<u64>().ok())
      .map(|created_at| now_ts.saturating_sub(created_at))
      .unwrap_or(0);

    assert!(
      proxy_age < 120,
      "Recently created config should be in grace period"
    );

    // Clean up test config
    delete_proxy_config(&recent_id);
  }

  #[tokio::test]
  async fn test_concurrent_config_operations() {
    use crate::proxy_storage::{
      delete_proxy_config, get_proxy_config, save_proxy_config, ProxyConfig,
    };
    use std::sync::Arc;

    let ids: Vec<String> = (0..20)
      .map(|i| format!("proxy_conc_test_{}_{}", i, rand::random::<u32>()))
      .collect();
    let ids = Arc::new(ids);

    // Concurrent writes
    let mut handles = vec![];
    for id in ids.iter() {
      let id = id.clone();
      handles.push(tokio::spawn(async move {
        let config = ProxyConfig::new(id.clone(), "DIRECT".to_string(), Some(15000));
        save_proxy_config(&config).unwrap();
      }));
    }
    for h in handles {
      h.await.unwrap();
    }

    // Verify all were written
    for id in ids.iter() {
      assert!(
        get_proxy_config(id).is_some(),
        "Config {id} should be readable after concurrent write"
      );
    }

    // Concurrent deletes
    let mut handles = vec![];
    for id in ids.iter() {
      let id = id.clone();
      handles.push(tokio::spawn(async move {
        delete_proxy_config(&id);
      }));
    }
    for h in handles {
      h.await.unwrap();
    }

    // Verify all deleted
    for id in ids.iter() {
      assert!(
        get_proxy_config(id).is_none(),
        "Config {id} should be gone after concurrent delete"
      );
    }
  }

  #[test]
  fn test_proxy_txt_parsing_various_formats() {
    // URL format
    let results = ProxyManager::parse_txt_proxies("http://user:pass@proxy.com:8080\n");
    assert_eq!(results.len(), 1);
    match &results[0] {
      ProxyParseResult::Parsed(p) => {
        assert_eq!(p.proxy_type, "http");
        assert_eq!(p.host, "proxy.com");
        assert_eq!(p.port, 8080);
        assert_eq!(p.username.as_deref(), Some("user"));
        assert_eq!(p.password.as_deref(), Some("pass"));
      }
      _ => panic!("Expected Parsed result"),
    }

    // host:port format
    let results = ProxyManager::parse_txt_proxies("10.0.0.1:3128\n");
    match &results[0] {
      ProxyParseResult::Parsed(p) => {
        assert_eq!(p.host, "10.0.0.1");
        assert_eq!(p.port, 3128);
        assert!(p.username.is_none());
      }
      _ => panic!("Expected Parsed"),
    }

    // host:port:user:pass format
    let results = ProxyManager::parse_txt_proxies("myhost:9090:admin:secret\n");
    match &results[0] {
      ProxyParseResult::Parsed(p) => {
        assert_eq!(p.host, "myhost");
        assert_eq!(p.port, 9090);
        assert_eq!(p.username.as_deref(), Some("admin"));
        assert_eq!(p.password.as_deref(), Some("secret"));
      }
      _ => panic!("Expected Parsed"),
    }

    // Comments and empty lines should be skipped
    let results = ProxyManager::parse_txt_proxies("# comment\n\n  \n1.2.3.4:80\n");
    assert_eq!(results.len(), 1);

    // SOCKS5 URL
    let results = ProxyManager::parse_txt_proxies("socks5://u:p@1.2.3.4:1080\n");
    match &results[0] {
      ProxyParseResult::Parsed(p) => {
        assert_eq!(p.proxy_type, "socks5");
        assert_eq!(p.host, "1.2.3.4");
        assert_eq!(p.port, 1080);
      }
      _ => panic!("Expected Parsed"),
    }

    // Scheme in front of the colon-separated body
    let results = ProxyManager::parse_txt_proxies("socks5://1.2.3.4:1080:admin:secret\n");
    match &results[0] {
      ProxyParseResult::Parsed(p) => {
        assert_eq!(p.proxy_type, "socks5");
        assert_eq!(p.host, "1.2.3.4");
        assert_eq!(p.port, 1080);
        assert_eq!(p.username.as_deref(), Some("admin"));
        assert_eq!(p.password.as_deref(), Some("secret"));
      }
      _ => panic!("Expected Parsed"),
    }

    // Same, with the credentials in front
    let results = ProxyManager::parse_txt_proxies("https://admin:secret:proxy.com:8443\n");
    match &results[0] {
      ProxyParseResult::Parsed(p) => {
        assert_eq!(p.proxy_type, "https");
        assert_eq!(p.host, "proxy.com");
        assert_eq!(p.port, 8443);
        assert_eq!(p.username.as_deref(), Some("admin"));
        assert_eq!(p.password.as_deref(), Some("secret"));
      }
      _ => panic!("Expected Parsed"),
    }

    // An IPv6 literal splits into four parts as well, and must not be read as
    // the colon-separated form
    let results = ProxyManager::parse_txt_proxies("http://[::1]:8080\n");
    match &results[0] {
      ProxyParseResult::Parsed(p) => {
        assert_eq!(p.host, "[::1]");
        assert_eq!(p.port, 8080);
        assert!(p.username.is_none());
      }
      _ => panic!("Expected Parsed"),
    }

    // A scheme-prefixed body that is ambiguous stays ambiguous
    let results = ProxyManager::parse_txt_proxies("socks5://1234:5678:9012:3456\n");
    match &results[0] {
      ProxyParseResult::Ambiguous {
        line,
        possible_formats,
      } => {
        assert_eq!(line, "socks5://1234:5678:9012:3456");
        assert_eq!(possible_formats.len(), 2);
      }
      _ => panic!("Expected Ambiguous"),
    }

    // Ambiguous: both positions could be ports
    let results = ProxyManager::parse_txt_proxies("1234:5678:9012:3456\n");
    match &results[0] {
      ProxyParseResult::Ambiguous {
        possible_formats, ..
      } => {
        assert_eq!(possible_formats.len(), 2);
      }
      _ => panic!("Expected Ambiguous"),
    }

    // Invalid
    let results = ProxyManager::parse_txt_proxies("notaproxy\n");
    match &results[0] {
      ProxyParseResult::Invalid { .. } => {}
      _ => panic!("Expected Invalid"),
    }
  }

  #[test]
  fn test_multiple_proxy_types_coexist() {
    let pm = ProxyManager::new();

    // Different proxy types for different profiles
    let types = [
      ("http", 3128),
      ("https", 3129),
      ("socks4", 1080),
      ("socks5", 1081),
    ];

    for (i, (ptype, port)) in types.iter().enumerate() {
      let info = ProxyInfo {
        id: format!("px_type_{ptype}"),
        local_url: format!("http://127.0.0.1:{}", 9300 + i as u16),
        upstream_host: "upstream.test".to_string(),
        upstream_port: *port,
        upstream_type: ptype.to_string(),
        local_port: 9300 + i as u16,
        profile_id: Some(format!("profile_{ptype}")),
        blocklist_file: None,
      };
      pm.insert_active_proxy(4000 + i as u32, info);
    }

    assert_eq!(pm.active_proxy_count(), 4);

    // Verify each type is stored correctly
    let info = pm.get_active_proxy(4000).unwrap();
    assert_eq!(info.upstream_type, "http");
    let info = pm.get_active_proxy(4003).unwrap();
    assert_eq!(info.upstream_type, "socks5");
    assert_eq!(info.upstream_port, 1081);
  }

  #[test]
  fn test_overwrite_pid_mapping() {
    let pm = ProxyManager::new();

    // Register proxy for PID 5000
    pm.insert_active_proxy(5000, make_proxy_info("px_old", 9400, Some("prof_ow")));

    // Overwrite the same PID with a new proxy (simulates browser reconnect with different proxy)
    pm.insert_active_proxy(5000, make_proxy_info("px_new", 9401, Some("prof_ow")));

    // Should only have 1 entry, with the new proxy
    assert_eq!(pm.active_proxy_count(), 1);
    let info = pm.get_active_proxy(5000).unwrap();
    assert_eq!(info.id, "px_new");
    assert_eq!(info.local_port, 9401);
  }

  #[test]
  fn test_proxy_config_with_bypass_rules_roundtrip() {
    use crate::proxy_storage::{
      delete_proxy_config, get_proxy_config, save_proxy_config, ProxyConfig,
    };

    let id = format!("proxy_bypass_test_{}", rand::random::<u32>());
    let rules = vec![
      "*.google.com".to_string(),
      "localhost".to_string(),
      "192.168.0.*".to_string(),
      "^.*\\.internal\\.corp$".to_string(),
    ];

    let config = ProxyConfig::new(id.clone(), "http://upstream:3128".to_string(), Some(18888))
      .with_profile_id(Some("prof_bypass".to_string()))
      .with_bypass_rules(rules.clone());

    save_proxy_config(&config).unwrap();

    let loaded = get_proxy_config(&id).unwrap();
    assert_eq!(loaded.bypass_rules.len(), 4);
    assert_eq!(loaded.bypass_rules, rules);
    assert_eq!(loaded.profile_id.as_deref(), Some("prof_bypass"));

    delete_proxy_config(&id);
  }
}
