#[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::{
    apply_profiles_with_services, build_status, resolve_component_statuses, resolve_preview,
    save_profile_to_storage, save_target_to_storage, Clock, CommandError, DetectedProxyApplyHelper,
    HelperApplyRequest, HelperApplyResult, ProfileInputDto, ProfileItemInputDto, ProxyApplyHelper,
    TargetInputDto,
};
use component_detection::{
    DetectedProxyfier, ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry,
};
use models::{
    ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType, Protocol,
    ProxyProtocol, Target, TargetKind,
};
use proxifyre::ProxiFyreAdapter;
use std::collections::HashSet;
use std::fs;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::Command as ProcessCommand;
use std::time::{SystemTime, UNIX_EPOCH};
use storage::JsonStorage;

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

    let target = save_target_to_storage(
        &storage,
        TargetInputDto {
            id: Some("Home Gateway".to_string()),
            name: " Home Gateway ".to_string(),
            kind: Some("external".to_string()),
            protocol: Some("socks5".to_string()),
            host: " 192.168.50.111 ".to_string(),
            port: 8080,
            requires_component: None,
        },
    )
    .expect("target command should normalize");
    let profile = save_profile_to_storage(
        &storage,
        ProfileInputDto {
            id: Some("Discord".to_string()),
            name: " Discord ".to_string(),
            enabled: Some(true),
            target_id: Some("home-gateway".to_string()),
            protocols: Some(vec!["tcp".to_string(), "UDP".to_string()]),
            items: Some(vec![ProfileItemInputDto {
                item_type: "process".to_string(),
                value: "Discord.exe".to_string(),
                recursive: None,
            }]),
        },
    )
    .expect("profile command should normalize");

    assert_eq!(target.id, "home-gateway");
    assert_eq!(profile.id, "discord");
    assert_eq!(profile.target_id, "home-gateway");
    assert_eq!(profile.items[0].value, "Discord");

    let status = build_status(&storage).expect("status command should read stored state");
    assert_eq!(
        status.route_line,
        "Выбранные приложения -> ProxiFyre -> внешний прокси 192.168.50.111:8080"
    );
    assert_eq!(status.active_profile_count, 1);
    assert_eq!(status.routed_app_count, 1);

    cleanup(&root);
}

#[test]
fn resolve_preview_returns_structured_apps_without_filesystem_scan() {
    let preview = resolve_preview(ProfileInputDto {
        id: Some("Game".to_string()),
        name: "Game".to_string(),
        enabled: Some(true),
        target_id: Some("home-gateway".to_string()),
        protocols: Some(vec!["TCP".to_string()]),
        items: Some(vec![
            ProfileItemInputDto {
                item_type: "process".to_string(),
                value: "Discord.exe".to_string(),
                recursive: None,
            },
            ProfileItemInputDto {
                item_type: "folder".to_string(),
                value: r"C:\Games\Launcher".to_string(),
                recursive: Some(true),
            },
        ]),
    })
    .expect("preview should normalize profile input");

    assert_eq!(preview.profile_id, "game");
    assert_eq!(preview.apps.len(), 2);
    assert_eq!(preview.apps[0].app_name, "Discord");
    assert_eq!(preview.apps[1].source_type, ProfileItemType::Folder);
    assert!(preview
        .warnings
        .iter()
        .any(|warning| warning.contains("Сканирование папок отложено")));
}

#[test]
fn ping_proxy_target_reports_open_tcp_endpoint() {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
    let port = listener.local_addr().expect("read local addr").port();

    let result = commands::ping_proxy_target_endpoint(commands::PingProxyTargetInputDto {
        host: "127.0.0.1".to_string(),
        port,
    })
    .expect("ping should return response");

    assert_eq!(result.tag, "external-proxy");
    assert_eq!(result.server, "127.0.0.1");
    assert_eq!(result.server_port, port);
    assert!(result.ok);
    assert!(result.latency.is_some());
}

#[test]
#[cfg(windows)]
fn proxifyre_install_script_parses_as_powershell() {
    let root = test_root("proxifyre-install-script");
    fs::create_dir_all(&root).expect("test root should be created");

    let script = commands::wrap_elevated_package_script(
        &commands::install_proxifyre_script(&root.join("proxifyre-app-config.json")),
        &root.join("install.log"),
    );
    let script_path = root.join("install.ps1");
    let mut script_bytes = vec![0xEF, 0xBB, 0xBF];
    script_bytes.extend_from_slice(script.as_bytes());
    fs::write(&script_path, script_bytes).expect("script should be written");

    let escaped_path = script_path.display().to_string().replace('\'', "''");
    let parser = format!(
        "$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}"
    );
    let output = ProcessCommand::new("powershell")
        .args(["-NoProfile", "-NonInteractive", "-Command", &parser])
        .output()
        .expect("powershell parser should run");

    assert!(
        output.status.success(),
        "install script should parse\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
    );

    cleanup(&root);
}

#[test]
fn singbox_runner_preserves_installer_args_with_spaces() {
    let script = commands::singbox_installer_runner_script(
        Path::new(r"C:\ProgramData\VpnProxy\state\install-singbox.ps1"),
        Path::new(r"C:\ProgramData\VpnProxy\state\install.log"),
        &[
            "-InstallRoot".to_string(),
            r"C:\Program Files\VpnProxy\sing-box".to_string(),
            "-ServiceName".to_string(),
            "VpnProxySingBox".to_string(),
            "-Uninstall".to_string(),
        ],
    );

    assert!(script.contains("$installerArgs = @('-InstallRoot', 'C:\\Program Files\\VpnProxy\\sing-box'"));
    assert!(script.contains("& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installerPath @installerArgs"));
    assert!(!script.contains("Start-Process -FilePath 'powershell.exe' -ArgumentList $argumentList"));
}

#[test]
fn apply_generates_derived_config_and_records_activity_with_mock_helper() {
    let root = test_root("apply");
    let storage = JsonStorage::new(root.clone());
    storage
        .write_profiles(&[discord_profile("home-gateway")])
        .expect("write profiles");
    storage
        .write_targets(&[external_socks5_target()])
        .expect("write targets");
    storage
        .write_components(&[proxyfier_running(), singbox_missing()])
        .expect("write components");

    let response = apply_profiles_with_services(
        &storage,
        &ProxiFyreAdapter::default(),
        &MockApplyHelper,
        &FixedClock,
    )
    .expect("apply command should generate config and call helper");

    let generated_path = PathBuf::from(&response.generated_config_path);
    let generated_contents = fs::read_to_string(&generated_path).expect("read generated config");
    let activity = storage.read_activity().expect("read activity");

    assert!(response.success);
    assert!(response.changed);
    assert_eq!(response.adapter_id, "proxifyre");
    assert_eq!(response.enabled_profiles, 1);
    assert_eq!(response.routed_apps, 1);
    assert_eq!(response.helper.action, "proxyfier.apply.mock");
    assert!(generated_contents.contains("\"appNames\""));
    assert!(generated_contents.contains("Discord"));
    assert!(generated_path.ends_with("proxifyre-app-config.json"));
    assert_eq!(activity.len(), 1);
    assert_eq!(activity[0].at, "2026-07-03T00:00:00Z");
    assert_eq!(activity[0].title, "Конфиг ProxiFyre создан");

    cleanup(&root);
}

#[test]
fn apply_blocks_local_singbox_target_when_component_is_missing() {
    let root = test_root("missing-singbox");
    let storage = JsonStorage::new(root.clone());
    storage
        .write_profiles(&[discord_profile("local-singbox")])
        .expect("write profiles");
    storage
        .write_targets(&[local_singbox_target()])
        .expect("write targets");
    storage
        .write_components(&[singbox_missing()])
        .expect("write components");

    let error = apply_profiles_with_services(
        &storage,
        &ProxiFyreAdapter::default(),
        &MockApplyHelper,
        &FixedClock,
    )
    .expect_err("missing sing-box should block local target apply");
    let activity = storage.read_activity().expect("read blocked activity");

    assert_eq!(error.code, "required_component_not_running");
    assert_eq!(activity.len(), 1);
    assert_eq!(activity[0].level, models::ActivityLevel::Error);
    assert_eq!(activity[0].title, "Применение ProxiFyre заблокировано");

    cleanup(&root);
}

#[test]
fn component_status_merges_detected_existing_proxifyre() {
    let components = resolve_component_statuses(
        Vec::new(),
        Some(DetectedProxyfier {
            engine: ProxyfierEngine::ProxiFyre,
            name: "ProxiFyre".to_string(),
            install_dir: PathBuf::from(r"C:\Tools\ProxiFyre"),
            executable_path: PathBuf::from(r"C:\Tools\ProxiFyre\ProxiFyre.exe"),
            config_path: Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")),
            running: true,
            service_name: Some("ProxiFyreService".to_string()),
        }),
        None,
    );
    let proxyfier = components
        .iter()
        .find(|component| component.id == ComponentId::Proxyfier)
        .expect("proxyfier component");

    assert_eq!(proxyfier.state, ComponentState::Running);
    assert!(proxyfier.installed);
    assert!(proxyfier.running);
    assert_eq!(proxyfier.path, Some(r"C:\Tools\ProxiFyre".to_string()));
    assert!(proxyfier.problems.is_empty());
}

#[test]
fn detected_proxy_apply_helper_writes_proxifyre_app_config() {
    let root = test_root("detected-proxifyre");
    let install_dir = root.join("ProxiFyre");
    fs::create_dir_all(&install_dir).expect("install dir");
    fs::write(install_dir.join("ProxiFyre.exe"), "mock exe").expect("mock exe");
    fs::write(install_dir.join("app-config.json"), "{}").expect("existing config");
    let generated_config = root.join("generated").join("proxifyre-app-config.json");
    let host = DetectionHost::new()
        .with_registry("ProxiFyre", &install_dir)
        .with_path(&install_dir)
        .with_path(&install_dir.join("ProxiFyre.exe"));
    let helper = DetectedProxyApplyHelper::new(host);

    let result = helper
        .apply_proxy_config(HelperApplyRequest {
            adapter_id: "proxifyre",
            config_path: &generated_config,
            config_contents: r#"{"proxies":[]}"#,
        })
        .expect("detected helper should apply");

    let applied =
        fs::read_to_string(install_dir.join("app-config.json")).expect("read applied app-config");

    assert!(result.success);
    assert!(result.changed);
    assert_eq!(result.action, "proxifyre.apply-detected-config");
    assert_eq!(applied, r#"{"proxies":[]}"#);
    assert!(install_dir.join("app-config.json.bak").exists());

    cleanup(&root);
}

#[test]
fn detected_proxy_apply_helper_ignores_plain_proxifier_install() {
    let root = test_root("detected-proxifier");
    let install_dir = root.join("Proxifier");
    fs::create_dir_all(&install_dir).expect("install dir");
    fs::write(install_dir.join("Proxifier.exe"), "mock exe").expect("mock exe");
    let generated_config = root.join("generated").join("proxifyre-app-config.json");
    let host = DetectionHost::new()
        .with_registry("Proxifier", &install_dir)
        .with_path(&install_dir)
        .with_path(&install_dir.join("Proxifier.exe"));
    let helper = DetectedProxyApplyHelper::new(host);

    let result = helper
        .apply_proxy_config(HelperApplyRequest {
            adapter_id: "proxifyre",
            config_path: &generated_config,
            config_contents: r#"{"proxies":[]}"#,
        })
        .expect("plain Proxifier should be ignored and config should be staged");

    assert!(result.success);
    assert!(result.changed);
    assert_eq!(result.action, "proxifyre.stage-generated-config");
    assert!(result
        .message
        .contains("совместимая установка ProxiFyre не найдена"));

    cleanup(&root);
}

struct MockApplyHelper;

impl ProxyApplyHelper for MockApplyHelper {
    fn apply_proxy_config(
        &self,
        request: HelperApplyRequest<'_>,
    ) -> Result<HelperApplyResult, CommandError> {
        assert_eq!(request.adapter_id, "proxifyre");
        assert!(request.config_contents.contains("Discord"));
        assert!(request.config_path.ends_with("proxifyre-app-config.json"));

        Ok(HelperApplyResult {
            success: true,
            changed: true,
            action: "proxyfier.apply.mock".to_string(),
            message: "Mock helper accepted generated ProxiFyre config".to_string(),
        })
    }
}

struct FixedClock;

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

#[derive(Default)]
struct DetectionHost {
    paths: HashSet<String>,
    registry: Vec<RegistryInstallEntry>,
}

impl DetectionHost {
    fn new() -> Self {
        Self::default()
    }

    fn with_path(mut self, path: &Path) -> Self {
        self.paths.insert(normalize_path(path));
        self
    }

    fn with_registry(mut self, display_name: &str, install_location: &Path) -> Self {
        self.registry.push(RegistryInstallEntry {
            display_name: display_name.to_string(),
            install_location: Some(install_location.to_path_buf()),
            display_icon: None,
        });
        self
    }
}

impl ProxyfierDetectionHost for DetectionHost {
    fn env_var(&self, _name: &str) -> Option<String> {
        None
    }

    fn path_exists(&self, path: &Path) -> bool {
        self.paths.contains(&normalize_path(path))
    }

    fn process_running(&self, _process_name: &str) -> bool {
        false
    }

    fn service_running(&self, _service_name: &str) -> bool {
        false
    }

    fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
        self.registry.clone()
    }
}

fn normalize_path(path: &Path) -> String {
    path.display()
        .to_string()
        .replace('/', "\\")
        .to_ascii_lowercase()
}

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-commands-{name}-{timestamp}"))
}

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

fn discord_profile(target_id: &str) -> Profile {
    Profile {
        id: "discord".to_string(),
        name: "Discord".to_string(),
        enabled: true,
        target_id: target_id.to_string(),
        protocols: vec![Protocol::Tcp, Protocol::Udp],
        items: vec![ProfileItem {
            item_type: ProfileItemType::Process,
            value: "Discord".to_string(),
            recursive: false,
        }],
    }
}

fn external_socks5_target() -> Target {
    Target {
        id: "home-gateway".to_string(),
        name: "Домашний шлюз".to_string(),
        kind: TargetKind::External,
        protocol: ProxyProtocol::Socks5,
        host: "192.168.50.111".to_string(),
        port: 8080,
        requires_component: None,
    }
}

fn local_singbox_target() -> Target {
    Target {
        id: "local-singbox".to_string(),
        name: "Локальный sing-box".to_string(),
        kind: TargetKind::Local,
        protocol: ProxyProtocol::Socks5,
        host: "127.0.0.1".to_string(),
        port: 1080,
        requires_component: Some(ComponentId::Singbox),
    }
}

fn proxyfier_running() -> ComponentStatus {
    ComponentStatus {
        id: ComponentId::Proxyfier,
        name: "ProxiFyre".to_string(),
        state: ComponentState::Running,
        installed: true,
        running: true,
        version: Some("2.2.1".to_string()),
        path: Some(r"C:\Tools\ProxiFyre".to_string()),
        problems: Vec::new(),
        actions: vec!["Restart".to_string()],
    }
}

fn singbox_missing() -> ComponentStatus {
    ComponentStatus {
        id: ComponentId::Singbox,
        name: "Локальный sing-box".to_string(),
        state: ComponentState::Missing,
        installed: false,
        running: false,
        version: None,
        path: None,
        problems: vec!["Локальный sing-box не установлен".to_string()],
        actions: vec!["Установить локальный sing-box".to_string()],
    }
}
