use proxywarden_lib::adapters::proxifyre::{ProxiFyreAdapter, ProxiFyreConfig};
use proxywarden_lib::adapters::proxy_router::{ProxyRouterAdapter, ProxyRouterRequest};
use proxywarden_lib::adapters::singbox::{
    SingBoxAdapter, SingBoxCheckResult, SingBoxConfigChecker, SingBoxConfigError,
    SingBoxConfigErrorKind, SingBoxGenerationRequest, DEFAULT_VPN_OUTBOUND_TAG,
    SINGBOX_OUTPUT_FILE,
};
use proxywarden_lib::models::{
    ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig, Profile, ProfileItem,
    ProfileItemType, Protocol, ProxyProtocol, SubscriptionCache, SubscriptionServer, Target,
    TargetKind,
};
use std::{
    cell::RefCell,
    path::{Path, PathBuf},
};

#[test]
fn generates_selected_outbound_config_and_runs_check_when_binary_path_is_supplied() {
    let adapter = SingBoxAdapter::default();
    let config = local_singbox_config("nl-1");
    let cache = subscription_cache();
    let checker = RecordingChecker::ok("configuration OK");
    let binary_path = Path::new(r"C:\Tools\ProxyWarden\sing-box\sing-box.exe");

    let generated = adapter
        .generate_config(
            SingBoxGenerationRequest::new(&config, &cache, Some(binary_path)),
            &checker,
        )
        .expect("selected outbound should generate config");
    let generated_config: serde_json::Value =
        serde_json::from_str(&generated.contents).expect("generated sing-box json");

    assert_eq!(generated.adapter_id, "singbox");
    assert_eq!(generated.output_file_name, SINGBOX_OUTPUT_FILE);
    assert_eq!(generated.selected_server_tag, "nl-1");
    assert_eq!(generated.listen, "127.0.0.1");
    assert_eq!(generated.listen_port, 1080);
    assert_eq!(
        generated.check,
        Some(SingBoxCheckResult {
            checked: true,
            success: true,
            message: "configuration OK".to_string(),
        })
    );
    assert_eq!(generated_config["log"]["level"], "info");
    assert_eq!(generated_config["inbounds"][0]["type"], "mixed");
    assert_eq!(generated_config["inbounds"][0]["listen"], "127.0.0.1");
    assert_eq!(generated_config["inbounds"][0]["listen_port"], 1080);
    assert_eq!(generated_config["inbounds"][0]["set_system_proxy"], false);
    assert_eq!(generated_config["outbounds"][0]["type"], "vless");
    assert_eq!(
        generated_config["outbounds"][0]["tag"],
        DEFAULT_VPN_OUTBOUND_TAG
    );
    assert_eq!(
        generated_config["outbounds"][0]["server"],
        "nl.example.test"
    );
    assert_eq!(generated_config["outbounds"][0]["packet_encoding"], "xudp");
    assert_eq!(generated_config["route"]["final"], DEFAULT_VPN_OUTBOUND_TAG);
    let calls = checker.calls.borrow();
    assert_eq!(calls.len(), 1);
    assert_eq!(calls[0].0.as_path(), binary_path);
    assert!(calls[0].1.contains(r#""type": "mixed""#));
    assert!(calls[0].1.contains(r#""tag": "vpn""#));
}

#[test]
fn skips_singbox_check_when_binary_path_is_not_supplied() {
    let adapter = SingBoxAdapter::default();
    let config = local_singbox_config("nl-1");
    let cache = subscription_cache();
    let checker = RecordingChecker::ok("should not run");

    let generated = adapter
        .generate_config(
            SingBoxGenerationRequest::new(&config, &cache, None),
            &checker,
        )
        .expect("binary path is optional");

    assert_eq!(generated.check, None);
    assert!(checker.calls.borrow().is_empty());
}

#[test]
fn blocks_config_when_server_is_not_selected() {
    let adapter = SingBoxAdapter::default();
    let mut config = local_singbox_config("nl-1");
    config.selected_server_tag = None;
    let cache = subscription_cache();
    let checker = RecordingChecker::ok("should not run");

    let error = adapter
        .generate_config(
            SingBoxGenerationRequest::new(&config, &cache, None),
            &checker,
        )
        .expect_err("missing selected server should block config");

    assert_eq!(error.kind, SingBoxConfigErrorKind::MissingSelectedServer);
    assert!(checker.calls.borrow().is_empty());
}

#[test]
fn blocks_config_when_selected_outbound_is_missing() {
    let adapter = SingBoxAdapter::default();
    let config = local_singbox_config("missing-server");
    let cache = subscription_cache();
    let checker = RecordingChecker::ok("should not run");

    let error = adapter
        .generate_config(
            SingBoxGenerationRequest::new(&config, &cache, None),
            &checker,
        )
        .expect_err("missing outbound should block config");

    assert_eq!(error.kind, SingBoxConfigErrorKind::MissingSelectedOutbound);
    assert!(error.message.contains("missing-server"));
    assert!(checker.calls.borrow().is_empty());
}

#[test]
fn propagates_failed_singbox_check_as_structured_error() {
    let adapter = SingBoxAdapter::default();
    let config = local_singbox_config("nl-1");
    let cache = subscription_cache();
    let checker = RecordingChecker::err("invalid config");

    let error = adapter
        .generate_config(
            SingBoxGenerationRequest::new(&config, &cache, Some(Path::new("sing-box.exe"))),
            &checker,
        )
        .expect_err("failed sing-box check should block generated config");

    assert_eq!(error.kind, SingBoxConfigErrorKind::CheckFailed);
    assert!(error.message.contains("invalid config"));
}

#[test]
fn external_proxifyre_apply_does_not_require_singbox_component() {
    let adapter = ProxiFyreAdapter::default();
    let profiles = vec![discord_profile("home-gateway")];
    let targets = vec![external_socks5_target()];
    let components = vec![missing_singbox_component()];

    let generated = adapter
        .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components))
        .expect("external SOCKS5 target should not require local sing-box");
    let config: ProxiFyreConfig =
        serde_json::from_str(&generated.contents).expect("generated proxifyre json");

    assert_eq!(config.proxies.len(), 1);
    assert_eq!(
        config.proxies[0].socks5_proxy_endpoint,
        "192.168.50.111:8080"
    );
}

struct RecordingChecker {
    calls: RefCell<Vec<(PathBuf, String)>>,
    result: Result<SingBoxCheckResult, SingBoxConfigError>,
}

impl RecordingChecker {
    fn ok(message: &str) -> Self {
        Self {
            calls: RefCell::new(Vec::new()),
            result: Ok(SingBoxCheckResult {
                checked: true,
                success: true,
                message: message.to_string(),
            }),
        }
    }

    fn err(message: &str) -> Self {
        Self {
            calls: RefCell::new(Vec::new()),
            result: Err(SingBoxConfigError::new(
                SingBoxConfigErrorKind::CheckFailed,
                message,
            )),
        }
    }
}

impl SingBoxConfigChecker for RecordingChecker {
    fn check_config(
        &self,
        binary_path: &Path,
        config_json: &str,
    ) -> Result<SingBoxCheckResult, SingBoxConfigError> {
        self.calls
            .borrow_mut()
            .push((binary_path.to_path_buf(), config_json.to_string()));
        self.result.clone()
    }
}

fn local_singbox_config(selected_server_tag: &str) -> LocalSingBoxConfig {
    LocalSingBoxConfig {
        subscription_url: Some("https://sub.example.test/list".to_string()),
        device_hwid: None,
        selected_server_tag: Some(selected_server_tag.to_string()),
        listen_host: "127.0.0.1".to_string(),
        listen_port: 1080,
        service_name: "ProxyWardenSingBox".to_string(),
        install_root: r"C:\Program Files\ProxyWarden\sing-box".to_string(),
        updated_at: Some("2026-07-07T10:00:00Z".to_string()),
    }
}

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

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: "Home Gateway".to_string(),
        kind: TargetKind::External,
        protocol: ProxyProtocol::Socks5,
        host: "192.168.50.111".to_string(),
        port: 8080,
        requires_component: None,
    }
}

fn missing_singbox_component() -> ComponentStatus {
    ComponentStatus {
        id: ComponentId::Singbox,
        name: "Local sing-box".to_string(),
        state: ComponentState::Missing,
        installed: false,
        running: false,
        version: None,
        path: None,
        problems: vec!["Local sing-box is not installed".to_string()],
        actions: vec!["Install Local sing-box".to_string()],
    }
}
