use super::*;

fn sample_config() -> ProxyConfig {
    ProxyConfig::default()
}

fn write_reload_config(path: &Path, ad_tag: Option<&str>, server_port: Option<u16>) {
    let mut config = String::from(
        r#"
                [censorship]
                tls_domain = "example.com"

                [access.users]
                user = "00000000000000000000000000000000"
            "#,
    );

    if ad_tag.is_some() {
        config.push_str("\n[general]\n");
        if let Some(tag) = ad_tag {
            config.push_str(&format!("ad_tag = \"{tag}\"\n"));
        }
    }

    if let Some(port) = server_port {
        config.push_str("\n[server]\n");
        config.push_str(&format!("port = {port}\n"));
    }

    std::fs::write(path, config).unwrap();
}

fn write_web_reload_config(path: &Path, carriers: &str, carrier_learning: bool) {
    let config = format!(
        r#"
                [censorship]
                tls_domain = "example.com"

                [access.users]
                user = "00000000000000000000000000000000"

                [web]
                carriers = {carriers}
                carrier_learning = {carrier_learning}
            "#,
    );
    std::fs::write(path, config).unwrap();
}

fn write_web_fasttrack_reload_config(path: &Path, mode: &str, ad_tag: &str) {
    let config = format!(
        r#"
                [general]
                ad_tag = "{ad_tag}"

                [access.users]
                alice = "000102030405060708090a0b0c0d0e0f"

                [[server.listeners]]
                ip = "127.0.0.1"
                port = 18080
                transport = "web"
                proxy_protocol = false
                web_client_ip_source = "x_forwarded_for"
                web_trusted_proxy_cidrs = ["127.0.0.1/32"]

                [web]
                enabled = true
                decoy_fasttrack_mode = "{mode}"

                [[web.vhosts]]
                host = "proxy.example.com"
                public_addr = "203.0.113.10:443"

                [web.vhosts.decoy]
                mode = "http_upstream"
                upstream = "http://127.0.0.1:18081"

                [[web.vhosts.profiles]]
                user = "alice"
                secret_mode = "plain"
            "#,
    );
    std::fs::write(path, config).unwrap();
}

fn temp_config_path(prefix: &str) -> PathBuf {
    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    std::env::temp_dir().join(format!("{prefix}_{nonce}.toml"))
}

#[test]
fn overlay_applies_hot_and_preserves_non_hot() {
    let old = sample_config();
    let mut new = old.clone();
    new.general.hardswap = !old.general.hardswap;
    new.server.port = old.server.port.saturating_add(1);

    let applied = overlay_hot_fields(&old, &new);
    assert_eq!(applied.general.hardswap, new.general.hardswap);
    assert_eq!(applied.server.port, old.server.port);
}

#[test]
fn non_hot_only_change_does_not_change_hot_snapshot() {
    let old = sample_config();
    let mut new = old.clone();
    new.server.port = old.server.port.saturating_add(1);

    let applied = overlay_hot_fields(&old, &new);
    assert_eq!(
        HotFields::from_config(&old),
        HotFields::from_config(&applied)
    );
    assert_eq!(applied.server.port, old.server.port);
}

#[test]
fn bind_stale_mode_is_hot() {
    let old = sample_config();
    let mut new = old.clone();
    new.general.me_bind_stale_mode = match old.general.me_bind_stale_mode {
        MeBindStaleMode::Never => MeBindStaleMode::Ttl,
        MeBindStaleMode::Ttl => MeBindStaleMode::Always,
        MeBindStaleMode::Always => MeBindStaleMode::Never,
    };

    let applied = overlay_hot_fields(&old, &new);
    assert_eq!(
        applied.general.me_bind_stale_mode,
        new.general.me_bind_stale_mode
    );
    assert_ne!(
        HotFields::from_config(&old),
        HotFields::from_config(&applied)
    );
}

#[test]
fn web_debug_policy_is_hot_while_debug_capacity_is_process_owned() {
    let old = sample_config();
    let mut new = old.clone();
    new.web.debug.enabled = true;
    new.web.debug.default_window_secs = 60;
    new.web.limits.debug_records_capacity += 1;

    let applied = overlay_hot_fields(&old, &new);
    assert!(applied.web.debug.enabled);
    assert_eq!(applied.web.debug.default_window_secs, 60);
    assert_eq!(
        applied.web.limits.debug_records_capacity,
        old.web.limits.debug_records_capacity
    );
    assert_ne!(
        HotFields::from_config(&old),
        HotFields::from_config(&applied)
    );
}

#[test]
fn decoy_fasttrack_mode_is_deferred_until_restart() {
    let old = sample_config();
    let mut new = old.clone();
    new.web.decoy_fasttrack_mode = crate::config::WebDecoyFastTrackMode::Enforce;

    let applied = overlay_hot_fields(&old, &new);

    assert_eq!(
        applied.web.decoy_fasttrack_mode,
        old.web.decoy_fasttrack_mode
    );
    assert_eq!(
        HotFields::from_config(&old),
        HotFields::from_config(&applied)
    );
}

#[test]
fn hot_overlay_defers_learning_that_requires_new_process_capacity() {
    let mut old = sample_config();
    old.web.limits.max_carrier_learning_entries = 1;
    old.web.carriers = crate::config::WebCarriers::Disabled;
    old.web.carrier_learning = false;
    let mut new = old.clone();
    new.web.limits.max_carrier_learning_entries = 3;
    new.web.carriers = crate::config::WebCarriers::Enabled(vec![
        crate::config::WebCarrier::Websocket,
        crate::config::WebCarrier::Https,
    ]);
    new.web.carrier_learning = true;

    let applied = overlay_hot_fields(&old, &new);

    assert_eq!(applied.web.limits.max_carrier_learning_entries, 1);
    assert!(applied.web.carrier_negotiation_enabled());
    assert!(!applied.web.carrier_learning);
}

#[test]
fn hot_overlay_defers_carriers_for_dormant_learning_with_small_capacity() {
    let mut old = sample_config();
    old.web.limits.max_carrier_learning_entries = 1;
    old.web.carriers = crate::config::WebCarriers::Disabled;
    old.web.carrier_learning = true;
    let mut new = old.clone();
    new.web.limits.max_carrier_learning_entries = 3;
    new.web.carriers = crate::config::WebCarriers::Enabled(vec![
        crate::config::WebCarrier::Websocket,
        crate::config::WebCarrier::Https,
    ]);

    let applied = overlay_hot_fields(&old, &new);

    assert_eq!(applied.web.limits.max_carrier_learning_entries, 1);
    assert!(!applied.web.carrier_negotiation_enabled());
    assert!(applied.web.carrier_learning);
}

#[test]
fn web_debug_prefix_requiring_deferred_capacity_is_not_hot_applied() {
    let old = sample_config();
    let mut new = old.clone();
    new.web.limits.max_body_bytes = 4 * 1024 * 1024;
    new.web.debug.body_prefix_bytes = 3 * 1024 * 1024;

    let applied = overlay_hot_fields(&old, &new);
    assert_eq!(
        applied.web.limits.max_body_bytes,
        old.web.limits.max_body_bytes
    );
    assert_eq!(
        applied.web.debug.body_prefix_bytes,
        old.web.debug.body_prefix_bytes
    );
}

#[test]
fn keepalive_is_not_hot() {
    let old = sample_config();
    let mut new = old.clone();
    new.general.me_keepalive_interval_secs = old.general.me_keepalive_interval_secs + 5;

    let applied = overlay_hot_fields(&old, &new);
    assert_eq!(
        applied.general.me_keepalive_interval_secs,
        old.general.me_keepalive_interval_secs
    );
    assert_eq!(
        HotFields::from_config(&old),
        HotFields::from_config(&applied)
    );
}

#[test]
fn mixed_hot_and_non_hot_change_applies_only_hot_subset() {
    let old = sample_config();
    let mut new = old.clone();
    new.general.hardswap = !old.general.hardswap;
    new.general.use_middle_proxy = !old.general.use_middle_proxy;

    let applied = overlay_hot_fields(&old, &new);
    assert_eq!(applied.general.hardswap, new.general.hardswap);
    assert_eq!(
        applied.general.use_middle_proxy,
        old.general.use_middle_proxy
    );
    assert!(!config_equal(&applied, &new));
}

#[test]
fn listener_synlimit_fields_are_process_owned() {
    let mut old = sample_config();
    old.server.listeners.push(ListenerConfig {
        ip: "0.0.0.0".parse().unwrap(),
        transport: crate::config::ListenerTransport::Mtproxy,
        port: Some(443),
        client_mss: None,
        synlimit: SynLimitMode::Iptables,
        synlimit_seconds: 60,
        synlimit_hitcount: 48,
        synlimit_burst: 1,
        synlimit_ios_seconds: 1,
        synlimit_ios_hitcount: 12,
        synlimit_ios_burst: 24,
        synlimit_hashlimit_expire_ms: 60_000,
        synlimit_hashlimit_size: 32_768,
        announce: None,
        announce_ip: None,
        proxy_protocol: None,
        reuse_allow: false,
        web_client_ip_source: crate::config::WebClientIpSource::XForwardedFor,
        web_trusted_proxy_cidrs: Vec::new(),
    });
    let mut new = old.clone();
    new.server.port = 8443;
    new.server.listeners[0].synlimit_seconds = 120;
    new.server.listeners[0].synlimit_hitcount = 96;
    new.server.listeners[0].synlimit_burst = 2;
    new.server.listeners[0].synlimit_ios_seconds = 2;
    new.server.listeners[0].synlimit_ios_hitcount = 18;
    new.server.listeners[0].synlimit_ios_burst = 36;
    new.server.listeners[0].synlimit_hashlimit_expire_ms = 90_000;
    new.server.listeners[0].synlimit_hashlimit_size = 65_536;

    let applied = overlay_hot_fields(&old, &new);
    let listener = &applied.server.listeners[0];
    assert_eq!(applied.server.port, old.server.port);
    assert_eq!(
        listener.synlimit_seconds,
        old.server.listeners[0].synlimit_seconds
    );
    assert_eq!(
        listener.synlimit_hitcount,
        old.server.listeners[0].synlimit_hitcount
    );
    assert_eq!(
        listener.synlimit_burst,
        old.server.listeners[0].synlimit_burst
    );
    assert_eq!(
        listener.synlimit_hashlimit_size,
        old.server.listeners[0].synlimit_hashlimit_size
    );
    assert!(classify_config_changes(&old, &new).restart_required);
}

#[test]
fn reload_applies_hot_change_on_first_observed_snapshot() {
    let initial_tag = "11111111111111111111111111111111";
    let final_tag = "22222222222222222222222222222222";
    let path = temp_config_path("telemt_hot_reload_stable");

    write_reload_config(&path, Some(initial_tag), None);
    let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
    let initial_hash = ProxyConfig::load_with_metadata(&path)
        .unwrap()
        .rendered_hash;
    let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
    let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
    let mut reload_state = ReloadState::new(Some(initial_hash));

    write_reload_config(&path, Some(final_tag), None);
    reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
    assert_eq!(
        config_tx.borrow().general.ad_tag.as_deref(),
        Some(final_tag)
    );

    let _ = std::fs::remove_file(path);
}

#[tokio::test]
async fn candidate_watcher_waits_for_activation_and_reconciles_disk() {
    let initial_tag = "10101010101010101010101010101010";
    let disk_tag = "20202020202020202020202020202020";
    let path = temp_config_path("telemt_hot_reload_activation_gate");
    write_reload_config(&path, Some(initial_tag), None);
    let initial = Arc::new(ProxyConfig::load(&path).unwrap());
    write_reload_config(&path, Some(disk_tag), None);
    let cancellation = tokio_util::sync::CancellationToken::new();
    let (activation_tx, activation_rx) = watch::channel(false);
    let (mut config_rx, _log_rx, watcher) = spawn_config_watcher(
        path.clone(),
        initial,
        None,
        None,
        cancellation.clone(),
        None,
        Some(activation_rx),
    );
    let watcher = tokio::spawn(watcher);

    tokio::task::yield_now().await;
    assert_eq!(
        config_rx.borrow().general.ad_tag.as_deref(),
        Some(initial_tag)
    );
    activation_tx.send_replace(true);
    tokio::time::timeout(Duration::from_secs(2), config_rx.changed())
        .await
        .unwrap()
        .unwrap();
    assert_eq!(
        config_rx.borrow_and_update().general.ad_tag.as_deref(),
        Some(disk_tag)
    );

    cancellation.cancel();
    watcher.await.unwrap();
    let _ = std::fs::remove_file(path);
}

#[test]
fn reload_keeps_hot_apply_when_non_hot_fields_change() {
    let initial_tag = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    let final_tag = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
    let path = temp_config_path("telemt_hot_reload_mixed");

    write_reload_config(&path, Some(initial_tag), None);
    let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
    let initial_hash = ProxyConfig::load_with_metadata(&path)
        .unwrap()
        .rendered_hash;
    let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
    let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
    let mut reload_state = ReloadState::new(Some(initial_hash));

    write_reload_config(&path, Some(final_tag), Some(initial_cfg.server.port + 1));
    reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();

    let applied = config_tx.borrow().clone();
    assert_eq!(applied.general.ad_tag.as_deref(), Some(final_tag));
    assert_eq!(applied.server.port, initial_cfg.server.port);

    let _ = std::fs::remove_file(path);
}

#[test]
fn reload_rebuilds_vhosts_with_the_effective_fasttrack_mode() {
    let initial_tag = "abababababababababababababababab";
    let final_tag = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd";
    let path = temp_config_path("telemt_web_fasttrack_reload");

    write_web_fasttrack_reload_config(&path, "off", initial_tag);
    let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
    let initial_hash = ProxyConfig::load_with_metadata(&path)
        .unwrap()
        .rendered_hash;
    let (config_tx, _config_rx) = watch::channel(Arc::clone(&initial_cfg));
    let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
    let mut reload_state = ReloadState::new(Some(initial_hash));

    write_web_fasttrack_reload_config(&path, "enforce", final_tag);
    reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();

    let applied = config_tx.borrow().clone();
    assert_eq!(applied.general.ad_tag.as_deref(), Some(final_tag));
    assert_eq!(
        applied.web.decoy_fasttrack_mode,
        crate::config::WebDecoyFastTrackMode::Off
    );
    let runtime = applied.web.runtime.as_ref().unwrap();
    assert_eq!(
        runtime.vhosts["proxy.example.com"].decoy_fasttrack_mode,
        crate::config::WebDecoyFastTrackMode::Off
    );

    let _ = std::fs::remove_file(path);
}

#[test]
fn reload_publishes_web_negotiation_policy_outside_hot_field_reporting() {
    let path = temp_config_path("telemt_web_negotiation_reload");

    write_web_reload_config(&path, "false", true);
    let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
    let initial_hash = ProxyConfig::load_with_metadata(&path)
        .unwrap()
        .rendered_hash;
    let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
    let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
    let mut reload_state = ReloadState::new(Some(initial_hash));

    write_web_reload_config(&path, "[\"websocket\", \"https\"]", false);
    reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();

    let applied = config_tx.borrow().clone();
    assert!(applied.web.carrier_negotiation_enabled());
    assert!(!applied.web.carrier_learning);

    let _ = std::fs::remove_file(path);
}

#[test]
fn classify_sni_change_requires_restart() {
    // censorship.* is not in overlay_hot_fields -> restart.
    let old = ProxyConfig::default();
    let mut new = ProxyConfig::default();
    new.censorship.tls_domain = "front.example".to_string();

    let class = classify_config_changes(&old, &new);
    assert!(class.restart_required);
    assert!(class.changed.iter().any(|c| c == "censorship"));
}

#[test]
fn classify_dns_overrides_change_is_hot() {
    // network.dns_overrides IS in overlay_hot_fields -> no restart.
    let old = ProxyConfig::default();
    let mut new = ProxyConfig::default();
    new.network.dns_overrides.push("1.1.1.1".to_string());

    let class = classify_config_changes(&old, &new);
    assert!(!class.restart_required);
    assert!(class.changed.iter().any(|c| c == "network"));
}

#[test]
fn classify_timeouts_change_requires_restart() {
    // timeouts.* is NOT in overlay_hot_fields -> restart.
    let old = ProxyConfig::default();
    let mut new = ProxyConfig::default();
    new.timeouts.client_handshake = old.timeouts.client_handshake + 1;

    let class = classify_config_changes(&old, &new);
    assert!(class.restart_required);
}

#[test]
fn reload_recovers_after_parse_error_on_next_attempt() {
    let initial_tag = "cccccccccccccccccccccccccccccccc";
    let final_tag = "dddddddddddddddddddddddddddddddd";
    let path = temp_config_path("telemt_hot_reload_parse_recovery");

    write_reload_config(&path, Some(initial_tag), None);
    let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
    let initial_hash = ProxyConfig::load_with_metadata(&path)
        .unwrap()
        .rendered_hash;
    let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
    let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
    let mut reload_state = ReloadState::new(Some(initial_hash));

    std::fs::write(&path, "[access.users\nuser = \"broken\"\n").unwrap();
    assert!(reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).is_none());
    assert_eq!(
        config_tx.borrow().general.ad_tag.as_deref(),
        Some(initial_tag)
    );

    write_reload_config(&path, Some(final_tag), None);
    reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
    assert_eq!(
        config_tx.borrow().general.ad_tag.as_deref(),
        Some(final_tag)
    );

    let _ = std::fs::remove_file(path);
}
