// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Settings form route handlers.
//!
//! Provides a GET form to view current settings, a POST handler to update
//! them, and a POST handler for the theme toggle button in the navbar.

use http::{Response, StatusCode};

use csaf_models::settings::{
    CLASSIFICATION_STORAGE_MODES, NATO_LABELS, Settings, TLP_LABELS, VERSCHLUSSSACHE_LABELS,
    is_valid_classification_storage_mode, is_valid_nato, is_valid_publisher_category,
    is_valid_storage_path, is_valid_tlp, is_valid_verschlusssache,
};

use crate::app_state::AppState;
use crate::router::{Body, form_checkbox, form_field, html_response, parse_form_body, redirect};
use crate::routes::layout::{Nav, html_escape, wrap_page};

// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------

/// `GET /settings` -- Settings form.
pub async fn settings_form(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let settings = state.settings();
    render_settings_form(&settings, None, None)
}

/// `POST /settings` -- Update settings from the native HTML form.
pub async fn settings_update(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let form = match parse_form_body(&parts) {
        Ok(f) => f,
        Err(err) => {
            let current = state.settings();
            return render_settings_form(
                &current,
                Some(&format!("Invalid form data: {err}")),
                None,
            );
        },
    };

    let csaf_mode = form_field(&form, "csaf_mode").unwrap_or("2.1").to_owned();
    let theme = form_field(&form, "theme").unwrap_or("light").to_owned();
    let import_directory = form_field(&form, "import_directory")
        .unwrap_or("./data_import")
        .to_owned();
    let export_directory = form_field(&form, "export_directory")
        .unwrap_or("./data_export")
        .to_owned();
    let dump_directory = form_field(&form, "dump_directory")
        .unwrap_or("./data_dump")
        .to_owned();
    let log_directory = form_field(&form, "log_directory")
        .unwrap_or("./data_log")
        .to_owned();
    let naming_convention = form_field(&form, "naming_convention")
        .unwrap_or("ndaal-sa-")
        .to_owned();
    let sidecar_sha256 = form_checkbox(&form, "sidecar_sha256");
    let sidecar_sha512 = form_checkbox(&form, "sidecar_sha512");
    let sidecar_sha3_512 = form_checkbox(&form, "sidecar_sha3_512");

    let publisher_name = form_field(&form, "publisher_name")
        .unwrap_or("")
        .trim()
        .to_owned();
    let publisher_namespace = form_field(&form, "publisher_namespace")
        .unwrap_or("")
        .trim()
        .to_owned();
    let publisher_category = form_field(&form, "publisher_category")
        .unwrap_or("vendor")
        .trim()
        .to_owned();
    let publisher_contact_details = form_field(&form, "publisher_contact_details")
        .unwrap_or("")
        .trim()
        .to_owned();

    let tlp_default = form_field(&form, "tlp_default")
        .unwrap_or("AMBER")
        .trim()
        .to_owned();
    let verschlusssache_enabled = form_checkbox(&form, "verschlusssache_enabled");
    let verschlusssache_default = form_field(&form, "verschlusssache_default")
        .unwrap_or("VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)")
        .trim()
        .to_owned();
    let nato_enabled = form_checkbox(&form, "nato_enabled");
    let nato_default = form_field(&form, "nato_default")
        .unwrap_or("NR (NATO RESTRICTED)")
        .trim()
        .to_owned();
    let classification_storage_mode = form_field(&form, "classification_storage_mode")
        .unwrap_or("both")
        .trim()
        .to_owned();

    if let Err(msg) = validate_settings_form(
        &csaf_mode,
        &theme,
        &import_directory,
        &export_directory,
        &dump_directory,
        &log_directory,
        &publisher_name,
        &publisher_namespace,
        &publisher_category,
        &tlp_default,
        &verschlusssache_default,
        &nato_default,
        &classification_storage_mode,
    ) {
        let current = state.settings();
        return render_settings_form(&current, Some(msg), None);
    }

    let new_settings = Settings {
        csaf_mode,
        theme,
        import_directory,
        export_directory,
        dump_directory,
        log_directory,
        sidecar_sha256,
        sidecar_sha512,
        sidecar_sha3_512,
        naming_convention,
        publisher_name,
        publisher_namespace,
        publisher_category,
        publisher_contact_details,
        tlp_default,
        verschlusssache_enabled,
        verschlusssache_default,
        nato_enabled,
        nato_default,
        classification_storage_mode,
    };

    match state.update_settings(new_settings.clone()) {
        Ok(()) => render_settings_form(&new_settings, None, Some("Settings saved successfully.")),
        Err(err) => {
            let current = state.settings();
            render_settings_form(
                &current,
                Some(&format!("Failed to save settings: {err}")),
                None,
            )
        },
    }
}

/// Validate a parsed `/settings` form submission.
///
/// Returns `Ok(())` if every field satisfies its whitelist /
/// non-empty / path-syntax rule, or `Err(reason)` with a
/// user-facing error string on the first failure. Extracted from
/// [`settings_update`] to keep its cyclomatic complexity below the
/// project-wide threshold.
#[allow(clippy::too_many_arguments)]
fn validate_settings_form(
    csaf_mode: &str,
    theme: &str,
    import_directory: &str,
    export_directory: &str,
    dump_directory: &str,
    log_directory: &str,
    publisher_name: &str,
    publisher_namespace: &str,
    publisher_category: &str,
    tlp_default: &str,
    verschlusssache_default: &str,
    nato_default: &str,
    classification_storage_mode: &str,
) -> Result<(), &'static str> {
    if csaf_mode != "2.0" && csaf_mode != "2.1" {
        return Err("CSAF mode must be '2.0' or '2.1'.");
    }
    if theme != "light" && theme != "dark" {
        return Err("Theme must be 'light' or 'dark'.");
    }
    if !is_valid_storage_path(import_directory) {
        return Err("Import Directory contains invalid or unsafe path characters.");
    }
    if !is_valid_storage_path(export_directory) {
        return Err("Export Directory contains invalid or unsafe path characters.");
    }
    if !is_valid_storage_path(dump_directory) {
        return Err("Dump Directory contains invalid or unsafe path characters.");
    }
    if !is_valid_storage_path(log_directory) {
        return Err("Log Directory contains invalid or unsafe path characters.");
    }
    if publisher_name.is_empty() {
        return Err("Publisher name must not be empty.");
    }
    if publisher_namespace.is_empty() {
        return Err("Publisher namespace must not be empty.");
    }
    if !is_valid_publisher_category(publisher_category) {
        return Err(
            "Publisher category must be one of: vendor, discoverer, coordinator, \
             user, translator, other.",
        );
    }
    if !is_valid_tlp(tlp_default) {
        return Err("TLP 2.0 default must be one of: CLEAR, GREEN, AMBER, AMBER+STRICT, RED.");
    }
    if !is_valid_verschlusssache(verschlusssache_default) {
        return Err(
            "Verschlusssache default must be one of the four official German \
             classification labels.",
        );
    }
    if !is_valid_nato(nato_default) {
        return Err(
            "NATO default must be one of: NR (NATO RESTRICTED), NC (NATO CONFIDENTIAL), \
             NS (NATO SECRET), CTS (COSMIC TOP SECRET).",
        );
    }
    if !is_valid_classification_storage_mode(classification_storage_mode) {
        return Err("Classification storage mode must be 'distribution_text', 'notes', or 'both'.");
    }
    Ok(())
}

/// `POST /settings/reset` -- Restore every application setting to the
/// values produced by [`Settings::default`] and audit the action.
///
/// Invoked from the "Reset to default settings" button at the bottom of
/// the `/settings` page; the button is guarded by a native
/// `window.confirm` dialog, so an empty POST body is all we expect.
pub async fn settings_reset(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let defaults = Settings::default();
    match state.update_settings(defaults.clone()) {
        Ok(()) => {
            // Best-effort audit trail; a failed insert must not mask a
            // successful reset.
            let _ = state.db_pool().with_conn(|conn| {
                csaf_models::audit_log::record(
                    conn,
                    "settings_reset",
                    "all",
                    None,
                    Some("Settings restored to factory defaults."),
                )
            });
            render_settings_form(
                &defaults,
                None,
                Some("All settings were restored to defaults."),
            )
        },
        Err(err) => {
            let current = state.settings();
            render_settings_form(
                &current,
                Some(&format!("Failed to reset settings: {err}")),
                None,
            )
        },
    }
}

/// `POST /settings/toggle-theme` -- Flip between light and dark, persist, and
/// redirect back to the referring page.
pub async fn toggle_theme(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let mut new_settings = state.settings();
    new_settings.theme = if new_settings.theme == "dark" {
        "light".to_owned()
    } else {
        "dark".to_owned()
    };
    let _ = state.update_settings(new_settings);

    // Redirect back to the referring page (or / if absent) so the user stays
    // where they were with the new theme applied.
    let location = parts
        .headers
        .get("referer")
        .and_then(|v| v.to_str().ok())
        .map(str::to_owned)
        .unwrap_or_else(|| "/".to_owned());

    redirect(&location)
}

/// Render the settings form page.
fn render_settings_form(
    settings: &Settings,
    error: Option<&str>,
    success: Option<&str>,
) -> Response<Body> {
    let html = wrap_page(
        "Settings",
        &settings.theme,
        Nav::Settings,
        &render_settings_content(settings, error, success),
    );
    html_response(StatusCode::OK, html)
}

/// Render just the main content HTML for the settings form. Split out so
/// tests can inspect the rendered string without walking a hyper body.
fn render_settings_content(
    settings: &Settings,
    error: Option<&str>,
    success: Option<&str>,
) -> String {
    use std::fmt::Write as _;

    let error_html = error
        .map(|e| format!(r#"<div class="flash error">{}</div>"#, html_escape(e)))
        .unwrap_or_default();

    let success_html = success
        .map(|s| format!(r#"<div class="flash success">{}</div>"#, html_escape(s)))
        .unwrap_or_default();

    let sel = |v: &str, c: &str| if v == c { "selected" } else { "" };
    let csaf_20_selected = sel(&settings.csaf_mode, "2.0");
    let csaf_21_selected = sel(&settings.csaf_mode, "2.1");
    let theme_light_selected = sel(&settings.theme, "light");
    let theme_dark_selected = sel(&settings.theme, "dark");
    let sha256_checked = if settings.sidecar_sha256 {
        "checked"
    } else {
        ""
    };
    let sha512_checked = if settings.sidecar_sha512 {
        "checked"
    } else {
        ""
    };
    let sha3_checked = if settings.sidecar_sha3_512 {
        "checked"
    } else {
        ""
    };

    // Publisher category dropdown — use the same values as the CSAF
    // document form so the defaults match what build_document_from_form
    // accepts.
    let pc_vendor = sel(&settings.publisher_category, "vendor");
    let pc_disc = sel(&settings.publisher_category, "discoverer");
    let pc_coord = sel(&settings.publisher_category, "coordinator");
    let pc_user = sel(&settings.publisher_category, "user");
    let pc_trans = sel(&settings.publisher_category, "translator");
    let pc_other = sel(&settings.publisher_category, "other");

    // Classification dropdowns — build <option> lists from the canonical
    // whitelists so the form and the validator stay in sync.
    let tlp_options = TLP_LABELS.iter().fold(String::new(), |mut out, label| {
        let _ = write!(
            out,
            r#"<option value="{label}" {sel}>TLP:{label}</option>"#,
            label = html_escape(label),
            sel = sel(&settings.tlp_default, label),
        );
        out
    });
    let vs_options = VERSCHLUSSSACHE_LABELS
        .iter()
        .fold(String::new(), |mut out, label| {
            let _ = write!(
                out,
                r#"<option value="{esc}" {sel}>{esc}</option>"#,
                esc = html_escape(label),
                sel = sel(&settings.verschlusssache_default, label),
            );
            out
        });
    let nato_options = NATO_LABELS.iter().fold(String::new(), |mut out, label| {
        let _ = write!(
            out,
            r#"<option value="{esc}" {sel}>{esc}</option>"#,
            esc = html_escape(label),
            sel = sel(&settings.nato_default, label),
        );
        out
    });
    let storage_options =
        CLASSIFICATION_STORAGE_MODES
            .iter()
            .fold(String::new(), |mut out, mode| {
                let label = match *mode {
                    "distribution_text" => "Only document.distribution.text",
                    "notes" => "Only document.notes[]",
                    _ => "Both (distribution.text AND notes[])",
                };
                let _ = write!(
                    out,
                    r#"<option value="{mode}" {sel}>{label}</option>"#,
                    sel = sel(&settings.classification_storage_mode, mode),
                );
                out
            });

    let vs_checked = if settings.verschlusssache_enabled {
        "checked"
    } else {
        ""
    };
    let nato_checked = if settings.nato_enabled { "checked" } else { "" };

    let content = format!(
        r#"<h1>Settings</h1>
{error_html}
{success_html}
<div class="card">
  <form method="POST" action="/settings">
    <h2>General</h2>
    <div class="form-row">
      <div>
        <label for="csaf_mode">CSAF Mode *</label>
        <select name="csaf_mode" id="csaf_mode">
          <option value="2.0" {csaf_20_selected}>CSAF 2.0</option>
          <option value="2.1" {csaf_21_selected}>CSAF 2.1</option>
        </select>
        <div class="muted">Determines validation rules and the
        <code>csaf_version</code> written into exported files.</div>
      </div>
      <div>
        <label for="theme">Theme</label>
        <select name="theme" id="theme">
          <option value="light" {theme_light_selected}>Light</option>
          <option value="dark" {theme_dark_selected}>Dark</option>
        </select>
        <div class="muted">You can also use the toggle in the top navigation bar.</div>
      </div>
    </div>

    <div class="form-row">
      <div>
        <label for="import_directory">Import Directory</label>
        <input type="text" name="import_directory" id="import_directory" value="{import_dir}">
        <div class="muted">Filesystem path scanned for CSAF JSON files during import.</div>
      </div>
      <div>
        <label for="export_directory">Export Directory</label>
        <input type="text" name="export_directory" id="export_directory" value="{export_dir}">
        <div class="muted">Filesystem path where exported CSAF files and sidecars are written.</div>
      </div>
    </div>

    <div class="form-row">
      <div>
        <label for="dump_directory">Dump Directory</label>
        <input type="text" name="dump_directory" id="dump_directory" value="{dump_dir}">
        <div class="muted">Filesystem path where the database dump files and sidecars are written.</div>
      </div>
      <div>
        <label for="log_directory">Log Directory</label>
        <input type="text" name="log_directory" id="log_directory" value="{log_dir}">
        <div class="muted">Filesystem path where the application log files and sidecars are written.</div>
      </div>
    </div>

    <div class="form-row single">
      <div>
        <label for="naming_convention">Naming Convention Prefix</label>
        <input type="text" name="naming_convention" id="naming_convention" value="{naming}">
        <div class="muted">Prefix used for tracking IDs (e.g. <code>ndaal-sa-</code>).</div>
      </div>
    </div>

    <div class="form-row single">
      <div>
        <label>Sidecar Hash Files</label>
        <div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_sha256" {sha256_checked}> Generate SHA-256 hashes sidecars (extension <code>.sha-256</code>)</label></div>
        <div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_sha512" {sha512_checked}> Generate SHA-512 hashes sidecars (extension <code>.sha-512</code>)</label></div>
        <div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_sha3_512" {sha3_checked}> Generate SHA3-512 hashes sidecars (extension <code>.sha3-512</code>)</label></div>
      </div>
    </div>

    <h2>Classification</h2>
    <p class="muted">Defaults for the colored TLP 2.0 selector, the German
    Verschlusssache field, and the NATO classification field. These values
    prefill the CSAF create form; per-document overrides are still possible.</p>

    <div class="form-row">
      <div>
        <label for="tlp_default">TLP 2.0 default *</label>
        <select name="tlp_default" id="tlp_default" class="tlp-select" required>
          {tlp_options}
        </select>
        <div class="muted">Colour-coding per
        <a href="https://www.first.org/tlp/" target="_blank" rel="noopener">FIRST TLP 2.0</a>.</div>
      </div>
      <div>
        <label for="classification_storage_mode">Storage mode *</label>
        <select name="classification_storage_mode" id="classification_storage_mode" required>
          {storage_options}
        </select>
        <div class="muted">Where to write Verschlusssache / NATO values in
        the exported CSAF JSON. <code>both</code> writes to
        <code>document.distribution.text</code> AND
        <code>document.notes[]</code> so any consumer can find them.</div>
      </div>
    </div>

    <div class="form-row">
      <div>
        <label style="font-weight:normal;">
          <input type="checkbox" name="verschlusssache_enabled" {vs_checked}>
          Enable Verschlusssache
        </label>
        <label for="verschlusssache_default">Verschlusssache default</label>
        <select name="verschlusssache_default" id="verschlusssache_default" required>
          {vs_options}
        </select>
        <div class="muted">German national classification per
        <a href="https://de.wikipedia.org/wiki/Verschlusssache#Einstufung"
           target="_blank" rel="noopener">§ Verschlusssache</a>.</div>
      </div>
      <div>
        <label style="font-weight:normal;">
          <input type="checkbox" name="nato_enabled" {nato_checked}>
          Enable NATO
        </label>
        <label for="nato_default">NATO default</label>
        <select name="nato_default" id="nato_default" required>
          {nato_options}
        </select>
        <div class="muted">NATO classification per
        <a href="https://de.wikipedia.org/wiki/Geheimhaltungsgrad#NATO"
           target="_blank" rel="noopener">§ Geheimhaltungsgrad</a>.</div>
      </div>
    </div>

    <h2>Publisher Defaults</h2>
    <p class="muted">These values are prefilled into the CSAF create form and
    inherited by every new document. You can still override them per document
    at creation time.</p>

    <div class="form-row">
      <div>
        <label for="publisher_name">Name *</label>
        <input type="text" name="publisher_name" id="publisher_name" required value="{publisher_name}">
      </div>
      <div>
        <label for="publisher_namespace">Namespace *</label>
        <input type="url" name="publisher_namespace" id="publisher_namespace" required value="{publisher_namespace}">
      </div>
    </div>

    <div class="form-row">
      <div>
        <label for="publisher_category">Category *</label>
        <select name="publisher_category" id="publisher_category" required>
          <option value="vendor" {pc_vendor}>vendor</option>
          <option value="discoverer" {pc_disc}>discoverer</option>
          <option value="coordinator" {pc_coord}>coordinator</option>
          <option value="user" {pc_user}>user</option>
          <option value="translator" {pc_trans}>translator</option>
          <option value="other" {pc_other}>other</option>
        </select>
        <div class="muted">OASIS CSAF publisher category.</div>
      </div>
      <div>
        <label for="publisher_contact_details">Contact details</label>
        <input type="text" name="publisher_contact_details" id="publisher_contact_details" value="{publisher_contact}">
        <div class="muted">Usually an email address or security contact URL.</div>
      </div>
    </div>

    <button type="submit" class="btn">Save Settings</button>
  </form>

  <hr style="margin:1.5rem 0;">
  <form method="POST" action="/settings/reset"
        onsubmit="return confirm('Reset ALL settings to their default values? This cannot be undone.');">
    <button type="submit" class="btn secondary" id="reset-settings-btn">Reset to default settings</button>
    <div class="muted" style="margin-top:0.25rem;">Restores every field on this page to the application defaults.</div>
  </form>
</div>"#,
        import_dir = html_escape(&settings.import_directory),
        export_dir = html_escape(&settings.export_directory),
        dump_dir = html_escape(&settings.dump_directory),
        log_dir = html_escape(&settings.log_directory),
        naming = html_escape(&settings.naming_convention),
        publisher_name = html_escape(&settings.publisher_name),
        publisher_namespace = html_escape(&settings.publisher_namespace),
        publisher_contact = html_escape(&settings.publisher_contact_details),
    );

    content
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_render_settings_form_default() {
        let settings = Settings::default();
        let resp = render_settings_form(&settings, None, None);
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[test]
    fn test_render_settings_form_with_error() {
        let settings = Settings::default();
        let resp = render_settings_form(&settings, Some("test error"), None);
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[test]
    fn test_render_settings_form_with_success() {
        let settings = Settings::default();
        let resp = render_settings_form(&settings, None, Some("saved"));
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[test]
    fn test_render_settings_content_includes_publisher_fields() {
        let settings = Settings::default();
        let html = render_settings_content(&settings, None, None);
        assert!(html.contains(r#"name="publisher_name""#));
        assert!(html.contains(r#"name="publisher_namespace""#));
        assert!(html.contains(r#"name="publisher_category""#));
        assert!(html.contains(r#"name="publisher_contact_details""#));
        assert!(html.contains("Publisher Defaults"));
        assert!(html.contains(r#"<option value="vendor" selected>"#));
    }

    #[test]
    fn test_render_settings_content_labels_include_hashes() {
        // The three sidecar checkbox labels include the word "hashes" and
        // advertise the hyphenated extension — guard against regression.
        let settings = Settings::default();
        let html = render_settings_content(&settings, None, None);
        assert!(
            html.contains("Generate SHA-256 hashes sidecars"),
            "expected SHA-256 label in HTML"
        );
        assert!(
            html.contains("Generate SHA-512 hashes sidecars"),
            "expected SHA-512 label in HTML"
        );
        assert!(
            html.contains("Generate SHA3-512 hashes sidecars"),
            "expected SHA3-512 label in HTML"
        );
        // And the old strings must NOT appear standalone.
        assert!(!html.contains(">Generate SHA-256 sidecars<"));
        assert!(!html.contains(">Generate SHA3-512 sidecars<"));
    }

    #[test]
    fn test_render_settings_content_shows_sha512_checkbox() {
        let settings = Settings::default();
        let html = render_settings_content(&settings, None, None);
        assert!(
            html.contains(r#"name="sidecar_sha512""#),
            "expected SHA-512 checkbox input in HTML"
        );
        assert!(
            html.contains("<code>.sha-512</code>"),
            "expected SHA-512 extension help text"
        );
    }

    #[test]
    fn test_render_settings_content_shows_log_directory() {
        let settings = Settings::default();
        let html = render_settings_content(&settings, None, None);
        assert!(
            html.contains(r#"name="log_directory""#),
            "expected log_directory input in HTML"
        );
        assert!(
            html.contains(r#"value="./data_log""#),
            "expected default log directory value"
        );
        assert!(
            html.contains(
                "Filesystem path where the application log files and sidecars are written."
            )
        );
    }

    #[test]
    fn test_render_settings_content_shows_reset_button() {
        let settings = Settings::default();
        let html = render_settings_content(&settings, None, None);
        assert!(
            html.contains("/settings/reset"),
            "expected reset form action in HTML"
        );
        assert!(
            html.contains("Reset to default settings"),
            "expected reset button label"
        );
        // The confirmation dialog must be wired to the form.
        assert!(
            html.contains("onsubmit=\"return confirm(")
                || html.contains("onsubmit='return confirm("),
            "expected native confirm() guard on reset form"
        );
    }

    #[test]
    fn test_render_settings_content_shows_dump_directory() {
        let settings = Settings::default();
        let html = render_settings_content(&settings, None, None);
        assert!(
            html.contains(r#"name="dump_directory""#),
            "expected dump_directory input in HTML"
        );
        assert!(
            html.contains(r#"value="./data_dump""#),
            "expected default dump directory value"
        );
        assert!(
            html.contains(
                "Filesystem path where the database dump files and sidecars are written."
            )
        );
    }

    #[test]
    fn test_render_settings_content_prefills_non_default_dump_directory() {
        let settings = Settings {
            dump_directory: "/backup/csaf".to_owned(),
            ..Settings::default()
        };
        let html = render_settings_content(&settings, None, None);
        assert!(html.contains(r#"value="/backup/csaf""#));
    }

    #[test]
    fn test_render_settings_content_prefills_non_default_publisher() {
        let settings = Settings {
            publisher_name: "Acme Security".to_owned(),
            publisher_namespace: "https://acme.example/csaf".to_owned(),
            publisher_category: "coordinator".to_owned(),
            publisher_contact_details: "psirt@acme.example".to_owned(),
            ..Settings::default()
        };
        let html = render_settings_content(&settings, None, None);
        assert!(html.contains("Acme Security"));
        assert!(html.contains("acme.example/csaf"));
        assert!(html.contains("psirt@acme.example"));
        assert!(html.contains(r#"<option value="coordinator" selected>"#));
        assert!(!html.contains(r#"<option value="vendor" selected>"#));
    }
}
