#[path = "../src/activity.rs"]
mod activity;
#[path = "../src/commands.rs"]
mod commands;
#[path = "../src/component_detection.rs"]
mod component_detection;
#[path = "../src/models.rs"]
mod models;
#[path = "../src/adapters/proxifyre.rs"]
mod proxifyre;
#[path = "../src/adapters/proxy_router.rs"]
mod proxy_router;
#[path = "../src/adapters/singbox.rs"]
mod singbox;
#[path = "../src/singbox_service.rs"]
mod singbox_service;
#[path = "../src/storage.rs"]
mod storage;
#[path = "../src/subscription.rs"]
mod subscription;
#[path = "../src/validation.rs"]
mod validation;

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

#[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!(status.config.has_subscription);
    assert_eq!(
        status.config.subscription_display_url,
        Some("https://sub.example.test/...".to_string())
    );

    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 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()),
            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());
    storage
        .write_local_singbox_config(&LocalSingBoxConfig {
            subscription_url: Some("https://sub.example.test/path".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 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.selected_server_tag, None);
    assert_eq!(cache, None);

    cleanup(&root);
}

struct MockFetcher(SubscriptionCache);

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

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!("vpn-proxy-singbox-commands-{name}-{timestamp}"))
}

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