use super::*;

#[test]
fn json_object_converts_to_toml_table() {
    let j: Json = serde_json::json!({"censorship": {"tls_domain": "a.com"}, "default_dc": 2});
    let t = json_to_toml(&j).expect("convertible");
    let table = t.as_table().unwrap();
    assert_eq!(table["censorship"]["tls_domain"].as_str(), Some("a.com"));
    assert_eq!(table["default_dc"].as_integer(), Some(2));
}

#[test]
fn deep_merge_overlays_tables_and_replaces_scalars() {
    let mut base: Toml =
        toml::from_str("[censorship]\ntls_domain = \"old\"\nfake_cert_len = 100\n").unwrap();
    let patch: Toml = toml::from_str("[censorship]\ntls_domain = \"new\"\n").unwrap();

    deep_merge(&mut base, &patch);

    let cens = base["censorship"].as_table().unwrap();
    assert_eq!(cens["tls_domain"].as_str(), Some("new"));
    assert_eq!(cens["fake_cert_len"].as_integer(), Some(100));
}

use std::path::PathBuf;

fn temp_config(body: &str) -> (PathBuf, tempfile::TempDir) {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("config.toml");
    std::fs::write(&path, body).unwrap();
    (path, dir)
}

#[tokio::test]
async fn patch_rejects_access_section() {
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
    let patch: Json = serde_json::json!({"access": {"users": {"x": "y"}}});
    let err = apply_patch_to_path(&path, &patch, None).await.unwrap_err();
    assert_eq!(err.code, "access_not_editable");
}

#[tokio::test]
async fn patch_revision_conflict() {
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
    let patch: Json = serde_json::json!({"censorship": {"tls_domain": "b"}});
    let err = apply_patch_to_path(&path, &patch, Some("deadbeef".into()))
        .await
        .unwrap_err();
    assert_eq!(err.code, "revision_conflict");
}

#[tokio::test]
async fn patch_sni_reports_restart_required() {
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a.com\"\n[server]\nport = 443\n");
    let patch: Json = serde_json::json!({"censorship": {"tls_domain": "b.com"}});
    let resp = apply_patch_to_path(&path, &patch, None).await.unwrap();
    assert!(resp.restart_required);
    assert!(resp.runtime_reload_required);
    assert!(!resp.process_restart_required);
    assert!(resp.deferred_process_fields.is_empty());
    assert!(resp.changed.iter().any(|c| c == "censorship"));
    let written = std::fs::read_to_string(&path).unwrap();
    assert!(written.contains("tls_domain = \"b.com\""));
    assert_eq!(
        resp.revision,
        crate::api::config_store::current_revision(&path)
            .await
            .unwrap()
    );
}

#[tokio::test]
async fn read_managed_config_strips_access() {
    let (path, _d) = temp_config(
        "[censorship]\ntls_domain = \"a.com\"\n[access.users]\nbob = \"00000000000000000000000000000000\"\n",
    );
    let (value, revision) = read_managed_config(&path).await.unwrap();
    let table = value.as_table().unwrap();
    assert!(table.contains_key("censorship"));
    // Secrets never leave the box through this endpoint.
    assert!(!table.contains_key("access"));
    assert_eq!(
        revision,
        crate::api::config_store::current_revision(&path)
            .await
            .unwrap()
    );
}

#[tokio::test]
async fn read_managed_config_exposes_web_without_runtime_or_access_secrets() {
    let (path, _directory) = temp_config(concat!(
        "[web]\nenabled = false\ncarrier = \"https\"\n",
        "[web.debug]\nenabled = true\ndefault_window_secs = 180\n",
        "[access.users]\nbob = \"00000000000000000000000000000000\"\n",
    ));

    let (value, _revision) = read_managed_config(&path).await.unwrap();
    let table = value.as_table().unwrap();

    assert!(table.contains_key("web"));
    assert!(table["web"].get("debug").is_some());
    assert!(table["web"].get("runtime").is_none());
    assert!(!table.contains_key("access"));
}

#[tokio::test]
async fn patch_web_debug_is_hot_and_limits_are_process_deferred() {
    let (path, _directory) = temp_config("[web]\nenabled = false\n");
    let debug_patch: Json = serde_json::json!({
        "web": {"debug": {"enabled": true, "capture_headers": false}}
    });
    let debug = apply_patch_to_path(&path, &debug_patch, None)
        .await
        .unwrap();
    assert!(!debug.process_restart_required);
    assert!(debug.changed.iter().any(|section| section == "web"));

    let limits_patch: Json = serde_json::json!({
        "web": {"limits": {"max_http_connections": 2049}}
    });
    let limits = apply_patch_to_path(&path, &limits_patch, None)
        .await
        .unwrap();
    assert!(limits.process_restart_required);
    assert!(
        limits
            .deferred_process_fields
            .iter()
            .any(|field| field == "web.limits")
    );
}

#[tokio::test]
async fn patch_web_decoy_fasttrack_requires_only_process_restart() {
    let (path, _directory) = temp_config("[web]\nenabled = false\n");
    let active = ProxyConfig::load(&path).unwrap();
    let patch: Json = serde_json::json!({
        "web": {"decoy_fasttrack_mode": "shadow"}
    });

    let mut prepared = prepare_patch_to_path(&path, &patch, None).await.unwrap();
    reconcile_runtime_effect(&mut prepared.response, &active, &prepared.desired_config).unwrap();
    let response = prepared.response;

    assert!(response.restart_required);
    assert!(!response.runtime_reload_required);
    assert!(response.process_restart_required);
    assert_eq!(
        response.deferred_process_fields,
        vec!["web.decoy_fasttrack_mode".to_string()]
    );
}

#[tokio::test]
async fn invalid_web_patch_does_not_modify_the_source() {
    let (path, _directory) = temp_config("[web]\nenabled = false\n");
    let original = tokio::fs::read_to_string(&path).await.unwrap();
    let patch: Json = serde_json::json!({
        "web": {"debug": {"default_window_secs": 181, "max_window_secs": 180}}
    });

    let error = apply_patch_to_path(&path, &patch, None).await.unwrap_err();

    assert_eq!(error.status, hyper::StatusCode::BAD_REQUEST);
    assert_eq!(tokio::fs::read_to_string(&path).await.unwrap(), original);
}

#[tokio::test]
async fn read_managed_config_returns_only_editable_sections() {
    // Full server (api/port) and network must not leak. Listeners-only server
    // is returned via the nested allowlist (covered in a dedicated test).
    let (path, _d) = temp_config(concat!(
        "[censorship]\ntls_domain = \"a\"\n",
        "[server]\nport = 443\n[server.api]\nauth_header = \"SECRET\"\n",
        "[network]\nipv4 = true\n",
        "[access.users]\nbob = \"00000000000000000000000000000000\"\n",
    ));
    let (value, _rev) = read_managed_config(&path).await.unwrap();
    let table = value.as_table().unwrap();
    assert!(table.contains_key("censorship"));
    let server = table["server"].as_table().unwrap();
    assert!(server.contains_key("listeners"));
    assert!(!server.contains_key("api"));
    assert!(!server.contains_key("port"));
    assert!(!table.contains_key("network"));
    assert!(!table.contains_key("access"));
}

#[tokio::test]
async fn read_managed_config_returns_server_listeners_only() {
    let (path, _d) = temp_config(concat!(
        "[censorship]\ntls_domain = \"a\"\n",
        "[server]\nport = 443\n",
        "[server.api]\nauth_header = \"SECRET\"\n",
        "[[server.listeners]]\nip = \"0.0.0.0\"\nport = 443\n",
    ));
    let (value, _rev) = read_managed_config(&path).await.unwrap();
    let table = value.as_table().unwrap();
    let server = table
        .get("server")
        .expect("server.listeners present")
        .as_table()
        .unwrap();
    assert!(server.contains_key("listeners"));
    assert!(!server.contains_key("api"));
    assert!(!server.contains_key("port"));
    let listeners = server["listeners"].as_array().unwrap();
    assert_eq!(listeners.len(), 1);
    assert_eq!(listeners[0]["port"].as_integer(), Some(443));
}

#[tokio::test]
async fn patch_rejects_forbidden_server_fields() {
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
    let patch: Json = serde_json::json!({"server": {"port": 1}});
    let err = apply_patch_to_path(&path, &patch, None).await.unwrap_err();
    assert_eq!(err.code, "field_not_editable");
}

#[tokio::test]
async fn patch_rejects_server_api_field() {
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
    let patch: Json = serde_json::json!({"server": {"api": {"enabled": false}}});
    let err = apply_patch_to_path(&path, &patch, None).await.unwrap_err();
    assert_eq!(err.code, "field_not_editable");
}

#[tokio::test]
async fn patch_server_listeners_preserves_api() {
    let (path, _d) = temp_config(concat!(
        "[censorship]\ntls_domain = \"a\"\n",
        "[server]\nport = 443\n",
        "[server.api]\nenabled = true\nauth_header = \"SECRET\"\n",
        "[[server.listeners]]\nip = \"0.0.0.0\"\nport = 443\n",
    ));
    let patch: Json = serde_json::json!({
        "server": {
            "listeners": [
                {"ip": "0.0.0.0", "port": 8443, "client_mss": "92"}
            ]
        }
    });
    let resp = apply_patch_to_path(&path, &patch, None).await.unwrap();
    assert!(resp.changed.iter().any(|c| c == "server"));
    let written = tokio::fs::read_to_string(&path).await.unwrap();
    let parsed: toml::Value = toml::from_str(&written).unwrap();
    assert_eq!(
        parsed["server"]["api"]["auth_header"].as_str(),
        Some("SECRET"),
        "{written}"
    );
    let listeners = parsed["server"]["listeners"].as_array().unwrap();
    assert_eq!(listeners.len(), 1, "{written}");
    assert_eq!(listeners[0]["port"].as_integer(), Some(8443), "{written}");
    assert_eq!(listeners[0]["client_mss"].as_str(), Some("92"), "{written}");
}

#[tokio::test]
async fn patch_rejects_show_link_section() {
    // show_link is a legacy top-level scalar/array (not a [table]); it cannot
    // be upserted safely and is superseded by the editable general.links.show.
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
    let patch: Json = serde_json::json!({"show_link": "*"});
    let err = apply_patch_to_path(&path, &patch, None).await.unwrap_err();
    assert_eq!(err.code, "section_not_editable");
}

#[tokio::test]
async fn patch_general_links_show_is_editable() {
    // The supported replacement path: edit show via the general.links sub-table.
    let (path, _d) = temp_config(
        "[general]\nprefer_ipv6 = false\n[general.links]\nshow = \"*\"\n\
         [censorship]\ntls_domain = \"a\"\n",
    );
    let patch: Json = serde_json::json!({"general": {"links": {"show": ["alice"]}}});
    let resp = apply_patch_to_path(&path, &patch, None).await.unwrap();
    assert!(resp.changed.iter().any(|c| c == "general"));
    let written = tokio::fs::read_to_string(&path).await.unwrap();
    let parsed: toml::Value = toml::from_str(&written).unwrap();
    assert_eq!(
        parsed["general"]["links"]["show"][0].as_str(),
        Some("alice"),
        "{written}"
    );
    // No leaked top-level [links]/[modes] and no duplicate sub-tables.
    assert_eq!(written.matches("[general.links]").count(), 1, "{written}");
}

#[tokio::test]
async fn patch_writes_the_included_section_owner_only() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("config.toml");
    let included = dir.path().join("censorship.toml");
    let root_body = "include = \"censorship.toml\"\n[server]\nport = 443\n";
    let included_body = "[censorship]\ntls_domain = \"old.example\"\n";
    tokio::fs::write(&root, root_body).await.unwrap();
    tokio::fs::write(&included, included_body).await.unwrap();
    let patch: Json = serde_json::json!({
        "censorship": {"tls_domain": "new.example"}
    });

    let response = apply_patch_to_path(&root, &patch, None).await.unwrap();

    assert_eq!(tokio::fs::read_to_string(&root).await.unwrap(), root_body);
    let written = tokio::fs::read_to_string(&included).await.unwrap();
    assert!(written.contains("tls_domain = \"new.example\""));
    assert_eq!(
        response.revision,
        crate::api::config_store::current_revision(&root)
            .await
            .unwrap()
    );
}

#[tokio::test]
async fn patch_rejects_multiple_source_owners_without_writing() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("config.toml");
    let included = dir.path().join("censorship.toml");
    let root_body = concat!(
        "include = \"censorship.toml\"\n",
        "[general]\nprefer_ipv6 = false\n"
    );
    let included_body = "[censorship]\ntls_domain = \"old.example\"\n";
    tokio::fs::write(&root, root_body).await.unwrap();
    tokio::fs::write(&included, included_body).await.unwrap();
    let patch: Json = serde_json::json!({
        "general": {"prefer_ipv6": true},
        "censorship": {"tls_domain": "new.example"}
    });

    let error = apply_patch_to_path(&root, &patch, None).await.unwrap_err();

    assert_eq!(error.code, "config_patch_not_atomic");
    assert_eq!(tokio::fs::read_to_string(&root).await.unwrap(), root_body);
    assert_eq!(
        tokio::fs::read_to_string(&included).await.unwrap(),
        included_body
    );
}

#[tokio::test]
async fn unavailable_reload_coordinator_is_detected_before_config_write() {
    let (path, _dir) = temp_config("[censorship]\ntls_domain = \"old.example\"\n");
    let original = tokio::fs::read_to_string(&path).await.unwrap();
    let patch: Json = serde_json::json!({
        "censorship": {"tls_domain": "new.example"}
    });
    let prepared = prepare_patch_to_path(&path, &patch, None).await.unwrap();
    let (control, receiver) = crate::maestro::reload::ReloadControl::channel(1);
    drop(receiver);

    let error = match control
        .reserve(prepared.response.revision.clone(), ReloadRequest::default())
        .await
    {
        Ok(_) => panic!("closed reload coordinator must reject the reservation"),
        Err(error) => error,
    };

    assert_eq!(error, ReloadSubmitError::MaestroUnavailable);
    assert_eq!(tokio::fs::read_to_string(&path).await.unwrap(), original);
}

#[tokio::test]
async fn failed_config_write_releases_reload_reservation() {
    let dir = tempfile::tempdir().unwrap();
    let (control, _receiver) = crate::maestro::reload::ReloadControl::channel(1);
    let reservation = control
        .reserve("candidate-revision".to_string(), ReloadRequest::default())
        .await
        .unwrap();

    let result = write_atomic(dir.path().to_path_buf(), "invalid target".to_string()).await;
    drop(reservation);

    assert!(result.is_err());
    assert_eq!(control.in_progress().await, None);
}

#[tokio::test]
async fn patch_links_public_port_written_as_integer_not_float_or_string() {
    // A JSON integer must land on disk as a bare TOML integer (443), never
    // 443.0 nor "443". The write re-renders from the typed config, so the
    // u16 field dictates the output format regardless of JSON quirks.
    let (path, _d) = temp_config("[general]\nprefer_ipv6 = false\n");
    let patch: Json = serde_json::json!({"general": {"links": {"public_port": 443}}});
    apply_patch_to_path(&path, &patch, None).await.unwrap();

    let written = tokio::fs::read_to_string(&path).await.unwrap();
    assert!(written.contains("public_port = 443"), "{written}");
    assert!(
        !written.contains("443.0"),
        "must not be a float:\n{written}"
    );
    assert!(
        !written.contains("\"443\""),
        "must not be a string:\n{written}"
    );

    let parsed: toml::Value = toml::from_str(&written).unwrap();
    assert_eq!(
        parsed["general"]["links"]["public_port"].as_integer(),
        Some(443),
        "{written}"
    );
}

#[tokio::test]
async fn patch_links_public_port_rejects_float() {
    // 443.0 cannot deserialize into u16 -> rejected, not silently coerced.
    let (path, _d) = temp_config("[general]\nprefer_ipv6 = false\n");
    let patch: Json = serde_json::json!({"general": {"links": {"public_port": 443.0}}});
    let err = apply_patch_to_path(&path, &patch, None).await.unwrap_err();
    assert_eq!(err.status, hyper::StatusCode::BAD_REQUEST, "{:?}", err);
}

#[tokio::test]
async fn patch_links_public_port_rejects_string() {
    // "443" is a string, not a u16 -> rejected.
    let (path, _d) = temp_config("[general]\nprefer_ipv6 = false\n");
    let patch: Json = serde_json::json!({"general": {"links": {"public_port": "443"}}});
    let err = apply_patch_to_path(&path, &patch, None).await.unwrap_err();
    assert_eq!(err.status, hyper::StatusCode::BAD_REQUEST, "{:?}", err);
}

#[tokio::test]
async fn patch_empty_is_rejected() {
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
    let patch: Json = serde_json::json!({});
    assert!(apply_patch_to_path(&path, &patch, None).await.is_err());
}

#[tokio::test]
async fn patch_log_level_is_hot() {
    // general.log_level is hot-reloadable -> a patch changing only it must
    // report restart_required = false (exercises the full apply path, not
    // just the classifier). Default LogLevel is Normal; patch to "debug".
    let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
    let patch: Json = serde_json::json!({"general": {"log_level": "debug"}});
    let resp = apply_patch_to_path(&path, &patch, None).await.unwrap();
    assert!(!resp.restart_required);
    assert!(!resp.runtime_reload_required);
    assert!(!resp.process_restart_required);
    assert!(resp.changed.iter().any(|c| c == "general"));
}
