use proxywarden_lib::adapters::singbox::{
    SingBoxAdapter, SingBoxCheckResult, SingBoxConfigChecker, SingBoxConfigError,
};
use proxywarden_lib::commands::{
    fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage,
    generate_singbox_config_with_services, read_singbox_status,
    save_singbox_subscription_to_storage, select_singbox_server_in_storage, Clock,
    SaveSingBoxSubscriptionInputDto, SelectSingBoxServerInputDto, SubscriptionFetcher,
};
use proxywarden_lib::models::{
    ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache,
    SubscriptionServer, TargetKind,
};
use proxywarden_lib::storage::JsonStorage;
use proxywarden_lib::subscription;
use serde_json::{json, Map};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

#[test]
fn saves_subscription_url_without_exposing_secret_query() {
    let root = test_root("save-subscription");
    let storage = JsonStorage::new(root.clone());

    let status = save_singbox_subscription_to_storage(
        &storage,
        SaveSingBoxSubscriptionInputDto {
            subscription_url: " https://sub.example.test/path?token=secret ".to_string(),
        },
        &FixedClock,
    )
    .expect("subscription URL should be saved");
    let config = storage
        .read_local_singbox_config()
        .expect("read local sing-box config");

    assert_eq!(
        config.subscription_url,
        Some("https://sub.example.test/path?token=secret".to_string())
    );
    assert_valid_generated_hwid(config.device_hwid.as_deref());
    assert!(status.config.has_subscription);
    assert_eq!(
        status.config.subscription_display_url,
        Some("https://sub.example.test/...".to_string())
    );

    cleanup(&root);
}

#[cfg(debug_assertions)]
#[test]
fn singbox_status_exposes_dev_subscription_headers_without_hwid() {
    let root = test_root("dev-subscription-identity");
    let storage = JsonStorage::new(root.clone());

    let status = read_singbox_status(&storage).expect("read sing-box status");
    let headers = status
        .subscription_identity
        .headers
        .iter()
        .map(|header| (header.name.clone(), header.value.clone()))
        .collect::<Vec<_>>();
    let expected_headers = subscription::SubscriptionFetchIdentity::default()
        .request_headers_without_device_hwid()
        .into_iter()
        .map(|(name, value)| (name.to_string(), value))
        .collect::<Vec<_>>();

    assert_eq!(headers, expected_headers);
    assert!(headers
        .iter()
        .all(|(name, _)| !name.eq_ignore_ascii_case("x-hwid")));
    let serialized = serde_json::to_value(&status).expect("serialize status");
    assert!(serialized.get("subscriptionIdentity").is_some());
    assert!(!serialized
        .to_string()
        .to_ascii_lowercase()
        .contains("x-hwid"));

    cleanup(&root);
}

#[test]
fn rejects_non_http_subscription_url() {
    let root = test_root("invalid-subscription");
    let storage = JsonStorage::new(root.clone());

    let error = save_singbox_subscription_to_storage(
        &storage,
        SaveSingBoxSubscriptionInputDto {
            subscription_url: "file:///C:/sub.txt".to_string(),
        },
        &FixedClock,
    )
    .expect_err("non-http subscription URL should fail");

    assert_eq!(error.code, "singbox_subscription_url_invalid");

    cleanup(&root);
}

#[test]
fn fetches_subscription_cache_and_selects_first_server() {
    let root = test_root("fetch-subscription");
    let storage = JsonStorage::new(root.clone());
    save_singbox_subscription_to_storage(
        &storage,
        SaveSingBoxSubscriptionInputDto {
            subscription_url: "https://sub.example.test/path?token=secret".to_string(),
        },
        &FixedClock,
    )
    .expect("save subscription URL");

    let status = fetch_singbox_subscription_with_fetcher(
        &storage,
        &MockFetcher(sample_cache()),
        &FixedClock,
    )
    .expect("fetch subscription through mock");
    let config = storage
        .read_local_singbox_config()
        .expect("read local sing-box config");
    let cache = storage
        .read_singbox_subscription_cache()
        .expect("read cache")
        .expect("cache should exist");
    let activity = storage.read_activity().expect("read activity");

    assert_eq!(status.config.selected_server_tag, Some("nl-1".to_string()));
    assert_eq!(status.cache.expect("status cache").servers.len(), 2);
    assert_eq!(config.selected_server_tag, Some("nl-1".to_string()));
    assert_eq!(cache.servers.len(), 2);
    assert_eq!(activity[0].level, ActivityLevel::Success);
    assert_eq!(activity[0].title, "Подписка Local sing-box обновлена");

    cleanup(&root);
}

#[test]
fn fetches_subscription_with_saved_device_hwid() {
    let root = test_root("fetch-subscription-hwid");
    let storage = JsonStorage::new(root.clone());
    save_singbox_subscription_to_storage(
        &storage,
        SaveSingBoxSubscriptionInputDto {
            subscription_url: "https://sub.example.test/path?token=secret".to_string(),
        },
        &FixedClock,
    )
    .expect("save subscription URL and generated HWID");
    let expected_hwid = storage
        .read_local_singbox_config()
        .expect("read local sing-box config")
        .device_hwid
        .expect("generated HWID");

    fetch_singbox_subscription_with_fetcher(
        &storage,
        &HwidAssertingFetcher {
            cache: sample_cache(),
            expected_hwid,
        },
        &FixedClock,
    )
    .expect("fetch subscription through mock");

    cleanup(&root);
}

#[test]
fn fetch_generates_device_hwid_for_existing_subscription_without_one() {
    let root = test_root("fetch-generates-hwid");
    let storage = JsonStorage::new(root.clone());
    storage
        .write_local_singbox_config(&LocalSingBoxConfig {
            subscription_url: Some("https://sub.example.test/path?token=secret".to_string()),
            device_hwid: None,
            ..LocalSingBoxConfig::default()
        })
        .expect("write local sing-box config");

    fetch_singbox_subscription_with_fetcher(
        &storage,
        &GeneratedHwidAssertingFetcher(sample_cache()),
        &FixedClock,
    )
    .expect("fetch subscription through mock");
    let config = storage
        .read_local_singbox_config()
        .expect("read local sing-box config");

    assert_valid_generated_hwid(config.device_hwid.as_deref());

    cleanup(&root);
}

#[test]
fn selects_server_from_cached_subscription() {
    let root = test_root("select-server");
    let storage = JsonStorage::new(root.clone());
    storage
        .write_singbox_subscription_cache(&sample_cache())
        .expect("write cache");

    let status = select_singbox_server_in_storage(
        &storage,
        SelectSingBoxServerInputDto {
            tag: "de-1".to_string(),
            server: None,
            server_port: None,
        },
        &FixedClock,
    )
    .expect("server should be selected");
    let config = storage
        .read_local_singbox_config()
        .expect("read local sing-box config");

    assert_eq!(status.config.selected_server_tag, Some("de-1".to_string()));
    assert_eq!(config.selected_server_tag, Some("de-1".to_string()));

    cleanup(&root);
}

#[test]
fn selects_server_by_endpoint_when_display_tag_is_sanitized() {
    let root = test_root("select-server-sanitized-tag");
    let storage = JsonStorage::new(root.clone());
    storage
        .write_singbox_subscription_cache(&sample_cache_with_flag_tag())
        .expect("write cache");

    let status = select_singbox_server_in_storage(
        &storage,
        SelectSingBoxServerInputDto {
            tag: "Умный".to_string(),
            server: Some("media.example.test".to_string()),
            server_port: Some(443),
        },
        &FixedClock,
    )
    .expect("server should be selected by endpoint fallback");
    let config = storage
        .read_local_singbox_config()
        .expect("read local sing-box config");

    assert_eq!(
        status.config.selected_server_tag,
        Some("Умный 🇳🇱->🇷🇺".to_string())
    );
    assert_eq!(config.selected_server_tag, Some("Умный 🇳🇱->🇷🇺".to_string()));

    cleanup(&root);
}

#[test]
fn generate_writes_config_and_local_singbox_target() {
    let root = test_root("generate-config");
    let storage = JsonStorage::new(root.clone());
    storage
        .write_local_singbox_config(&LocalSingBoxConfig {
            subscription_url: Some("https://sub.example.test/path".to_string()),
            device_hwid: Some("C34C14C9-94BC-4918-B053-A249BC117A91".to_string()),
            selected_server_tag: Some("nl-1".to_string()),
            ..LocalSingBoxConfig::default()
        })
        .expect("write local sing-box config");
    storage
        .write_singbox_subscription_cache(&sample_cache())
        .expect("write cache");

    let response = generate_singbox_config_with_services(
        &storage,
        &SingBoxAdapter::default(),
        &MockChecker,
        &FixedClock,
        Some(Path::new("sing-box.exe")),
    )
    .expect("generate sing-box config");
    let generated = fs::read_to_string(&response.generated_config_path).expect("read generated");
    let targets = storage.read_targets().expect("read targets");
    let target = targets
        .iter()
        .find(|target| target.id == "local-singbox")
        .expect("local sing-box target");
    let activity = storage.read_activity().expect("read activity");

    assert!(response.success);
    assert_eq!(response.adapter_id, "singbox");
    assert_eq!(response.selected_server_tag, "nl-1");
    assert!(response.check.expect("check result").success);
    assert!(generated.contains("\"type\": \"mixed\""));
    assert!(generated.contains("\"tag\": \"vpn\""));
    assert_eq!(target.kind, TargetKind::Local);
    assert_eq!(target.protocol, ProxyProtocol::Socks5);
    assert_eq!(target.host, "127.0.0.1");
    assert_eq!(target.port, 1080);
    assert_eq!(target.requires_component, Some(ComponentId::Singbox));
    assert_eq!(activity[0].title, "Конфиг Local sing-box создан");

    cleanup(&root);
}

#[test]
fn generate_requires_cached_subscription() {
    let root = test_root("generate-missing-cache");
    let storage = JsonStorage::new(root.clone());

    let error = generate_singbox_config_with_services(
        &storage,
        &SingBoxAdapter::default(),
        &MockChecker,
        &FixedClock,
        None,
    )
    .expect_err("missing cache should block generation");

    assert_eq!(error.code, "singbox_subscription_cache_missing");

    cleanup(&root);
}

#[test]
fn forget_subscription_clears_url_selection_and_cache() {
    let root = test_root("forget-subscription");
    let storage = JsonStorage::new(root.clone());
    let existing_hwid = "C34C14C9-94BC-4918-B053-A249BC117A91".to_string();
    storage
        .write_local_singbox_config(&LocalSingBoxConfig {
            subscription_url: Some("https://sub.example.test/path".to_string()),
            device_hwid: Some(existing_hwid.clone()),
            selected_server_tag: Some("nl-1".to_string()),
            ..LocalSingBoxConfig::default()
        })
        .expect("write local sing-box config");
    storage
        .write_singbox_subscription_cache(&sample_cache())
        .expect("write cache");

    let status =
        forget_singbox_subscription_in_storage(&storage, &FixedClock).expect("forget subscription");
    let config = storage
        .read_local_singbox_config()
        .expect("read local sing-box config");
    let cache = storage
        .read_singbox_subscription_cache()
        .expect("read cache");

    assert!(!status.config.has_subscription);
    assert_eq!(config.subscription_url, None);
    assert_eq!(config.device_hwid, Some(existing_hwid));
    assert_eq!(config.selected_server_tag, None);
    assert_eq!(cache, None);

    cleanup(&root);
}

struct MockFetcher(SubscriptionCache);

impl SubscriptionFetcher for MockFetcher {
    fn fetch_subscription(
        &self,
        url: &str,
        _identity: &subscription::SubscriptionFetchIdentity,
    ) -> Result<SubscriptionCache, subscription::SubscriptionError> {
        assert_eq!(url, "https://sub.example.test/path?token=secret");
        Ok(self.0.clone())
    }
}

struct HwidAssertingFetcher {
    cache: SubscriptionCache,
    expected_hwid: String,
}

impl SubscriptionFetcher for HwidAssertingFetcher {
    fn fetch_subscription(
        &self,
        url: &str,
        identity: &subscription::SubscriptionFetchIdentity,
    ) -> Result<SubscriptionCache, subscription::SubscriptionError> {
        assert_eq!(url, "https://sub.example.test/path?token=secret");
        assert_eq!(
            identity.device_hwid.as_deref(),
            Some(self.expected_hwid.as_str())
        );
        assert_eq!(identity.app_name, "ProxyWarden");
        assert!(identity.user_agent.starts_with("ProxyWarden/"));
        assert_eq!(identity.device_os, std::env::consts::OS);
        assert_eq!(identity.device_model, "ProxyWarden");
        Ok(self.cache.clone())
    }
}

struct GeneratedHwidAssertingFetcher(SubscriptionCache);

impl SubscriptionFetcher for GeneratedHwidAssertingFetcher {
    fn fetch_subscription(
        &self,
        url: &str,
        identity: &subscription::SubscriptionFetchIdentity,
    ) -> Result<SubscriptionCache, subscription::SubscriptionError> {
        assert_eq!(url, "https://sub.example.test/path?token=secret");
        assert_valid_generated_hwid(identity.device_hwid.as_deref());
        assert_eq!(identity.app_name, "ProxyWarden");
        assert!(identity.user_agent.starts_with("ProxyWarden/"));
        Ok(self.0.clone())
    }
}

fn assert_valid_generated_hwid(value: Option<&str>) {
    let value = value.expect("generated HWID");
    uuid::Uuid::parse_str(value).expect("HWID should be a UUID");
    assert_eq!(value, value.to_ascii_uppercase());
}

struct MockChecker;

impl SingBoxConfigChecker for MockChecker {
    fn check_config(
        &self,
        binary_path: &Path,
        config_json: &str,
    ) -> Result<SingBoxCheckResult, SingBoxConfigError> {
        assert_eq!(binary_path, Path::new("sing-box.exe"));
        assert!(config_json.contains("\"final\": \"vpn\""));
        Ok(SingBoxCheckResult {
            checked: true,
            success: true,
            message: "mock check passed".to_string(),
        })
    }
}

struct FixedClock;

impl Clock for FixedClock {
    fn now(&self) -> String {
        "2026-07-07T00:00:00Z".to_string()
    }
}

fn sample_cache() -> SubscriptionCache {
    SubscriptionCache {
        config: json!({
            "outbounds": [
                {
                    "type": "vless",
                    "tag": "nl-1",
                    "server": "nl.example.test",
                    "server_port": 443,
                    "uuid": "11111111-1111-1111-1111-111111111111"
                },
                {
                    "type": "trojan",
                    "tag": "de-1",
                    "server": "de.example.test",
                    "server_port": 443,
                    "password": "secret"
                }
            ]
        }),
        servers: vec![
            SubscriptionServer {
                tag: "nl-1".to_string(),
                server_type: "vless".to_string(),
                server: "nl.example.test".to_string(),
                server_port: 443,
            },
            SubscriptionServer {
                tag: "de-1".to_string(),
                server_type: "trojan".to_string(),
                server: "de.example.test".to_string(),
                server_port: 443,
            },
        ],
        user_info: Map::new(),
        fetched_at: "2026-07-07T00:00:00Z".to_string(),
    }
}

fn sample_cache_with_flag_tag() -> SubscriptionCache {
    SubscriptionCache {
        config: json!({
            "outbounds": [
                {
                    "type": "vless",
                    "tag": "Умный 🇳🇱->🇷🇺",
                    "server": "media.example.test",
                    "server_port": 443,
                    "uuid": "11111111-1111-1111-1111-111111111111"
                }
            ]
        }),
        servers: vec![SubscriptionServer {
            tag: "Умный 🇳🇱->🇷🇺".to_string(),
            server_type: "vless".to_string(),
            server: "media.example.test".to_string(),
            server_port: 443,
        }],
        user_info: Map::new(),
        fetched_at: "2026-07-07T00:00:00Z".to_string(),
    }
}

fn test_root(name: &str) -> PathBuf {
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock before unix epoch")
        .as_nanos();

    std::env::temp_dir().join(format!("proxywarden-singbox-commands-{name}-{timestamp}"))
}

fn cleanup(root: &Path) {
    let _ = fs::remove_dir_all(root);
}
