use base64::{engine::general_purpose, Engine};
use proxywarden_lib::models::redact_subscription_url;
use proxywarden_lib::subscription::{
    self, parse_subscription_body, parse_user_info, SubscriptionFetchIdentity,
};
use std::io::{Read, Write};
use std::net::TcpListener;
use std::time::Duration;

#[test]
fn parses_singbox_json_config_servers() {
    let parsed = parse_subscription_body(
        r#"{
          "outbounds": [
            { "type": "direct", "tag": "direct" },
            { "type": "vless", "tag": "nl-1", "server": "nl.example.test", "server_port": 443 },
            { "type": "trojan", "tag": "de-1", "server": "de.example.test", "server_port": 8443 }
          ]
        }"#,
    )
    .expect("json subscription should parse");

    assert_eq!(parsed.servers.len(), 2);
    assert_eq!(parsed.servers[0].tag, "nl-1");
    assert_eq!(parsed.servers[0].server_type, "vless");
    assert_eq!(parsed.servers[1].server_port, 8443);
}

#[test]
fn parses_base64_vless_link_list() {
    let link = sample_vless_link("nl-1");
    let encoded = general_purpose::STANDARD.encode(format!("{link}\n"));

    let parsed = parse_subscription_body(&encoded).expect("base64 vless list should parse");
    let outbound = &parsed.config["outbounds"][0];

    assert_eq!(parsed.servers.len(), 1);
    assert_eq!(parsed.servers[0].tag, "nl-1");
    assert_eq!(outbound["type"], "vless");
    assert_eq!(outbound["server"], "nl.example.test");
    assert_eq!(outbound["packet_encoding"], "xudp");
}

#[test]
fn decodes_percent_encoded_vless_fragment_tag() {
    let link = sample_vless_link(
        "%D0%A3%D0%BC%D0%BD%D1%8B%D0%B9%20%F0%9F%87%B3%F0%9F%87%B1-%3E%F0%9F%87%B7%F0%9F%87%BA",
    );

    let parsed = parse_subscription_body(&link).expect("vless link should parse");
    let outbound = &parsed.config["outbounds"][0];

    assert_eq!(parsed.servers[0].tag, "Умный 🇳🇱->🇷🇺");
    assert_eq!(outbound["tag"], "Умный 🇳🇱->🇷🇺");
}

#[test]
fn rejects_body_without_supported_outbounds() {
    let error = parse_subscription_body(r#"{"outbounds":[{"type":"direct","tag":"direct"}]}"#)
        .expect_err("unsupported subscription should fail");

    assert!(error.message.contains("No supported proxy outbounds found"));
}

#[test]
fn rejects_vless_without_reality_parameters() {
    let error = parse_subscription_body("vless://uuid@nl.example.test:443#nl-1")
        .expect_err("missing reality params should fail");

    assert!(error.message.contains("pbk and sid"));
}

#[test]
fn parses_subscription_user_info_header() {
    let user_info = parse_user_info(Some("upload=10; download=20; total=30; expire=bad"));

    assert_eq!(user_info["upload"], 10);
    assert_eq!(user_info["download"], 20);
    assert_eq!(user_info["total"], 30);
    assert!(!user_info.contains_key("expire"));
}

#[test]
fn rejects_invalid_or_non_http_subscription_url_before_network() {
    let invalid = subscription::fetch_subscription("not a url")
        .expect_err("invalid url should fail before request");
    let unsupported = subscription::fetch_subscription("file:///C:/subscription.txt")
        .expect_err("non-http url should fail before request");

    assert!(invalid.message.contains("Invalid subscription URL"));
    assert!(unsupported.message.contains("http or https"));
}

#[test]
fn fetch_subscription_sends_device_hwid_header_when_identity_is_set() {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind local test listener");
    let url = format!("http://{}/subscription", listener.local_addr().unwrap());
    let request_thread = std::thread::spawn(move || {
        let (mut stream, _) = listener.accept().expect("accept test request");
        stream
            .set_read_timeout(Some(Duration::from_secs(2)))
            .expect("set read timeout");

        let mut request = Vec::new();
        let mut buffer = [0_u8; 512];
        loop {
            let bytes_read = stream.read(&mut buffer).expect("read request");
            if bytes_read == 0 {
                break;
            }
            request.extend_from_slice(&buffer[..bytes_read]);
            if request.windows(4).any(|window| window == b"\r\n\r\n") {
                break;
            }
        }

        let body = r#"{"outbounds":[{"type":"vless","tag":"nl-1","server":"nl.example.test","server_port":443}]}"#;
        let response = format!(
            "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{}",
            body.len(),
            body
        );
        stream
            .write_all(response.as_bytes())
            .expect("write response");

        String::from_utf8_lossy(&request).to_ascii_lowercase()
    });

    let mut identity = SubscriptionFetchIdentity::with_device_hwid(Some("hwid-abc123"));
    identity.device_os_version = Some("Windows 11 Pro | 25H2 | build 26200.8655".to_string());
    let cache = subscription::fetch_subscription_with_identity(&url, &identity)
        .expect("fetch subscription through local test server");
    let request = request_thread.join().expect("request thread");

    assert_eq!(cache.servers[0].tag, "nl-1");
    assert!(request.contains("x-hwid: hwid-abc123"));
    assert!(request.contains("user-agent: proxywarden/"));
    assert!(request.contains("x-app-name: proxywarden"));
    assert!(request.contains("x-device-os:"));
    assert!(request.contains("x-device-os-version: windows 11 pro | 25h2 | build 26200.8655"));
    assert!(request.contains("x-ver-os: windows 11 pro | 25h2 | build 26200.8655"));
    assert!(request.contains("x-device-model: proxywarden"));
}

#[test]
fn redacts_subscription_url_for_display() {
    assert_eq!(
        redact_subscription_url("https://sub.example.test/path?token=secret"),
        "https://sub.example.test/..."
    );
    assert_eq!(
        redact_subscription_url("vless://uuid@example.test"),
        "vless://uuid@example.test/..."
    );
}

fn sample_vless_link(tag: &str) -> String {
    format!(
        "vless://{}@nl.example.test:443?security=reality&sni=example.test&fp=chrome&pbk=public-key&sid=short-id&flow=xtls-rprx-vision#{}",
        "11111111-1111-1111-1111-111111111111",
        tag
    )
}
