// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use super::*;
use crate::config::{global_plugin_config_path, project_plugin_config_path};
use nemo_flow::observability::plugin_component::OBSERVABILITY_PLUGIN_KIND;
use nemo_flow::plugin::{ConfigPolicy, PluginComponentSpec, PluginConfig};
use nemo_flow_adaptive::AdaptiveConfig;
use nemo_flow_adaptive::plugin_component::ADAPTIVE_PLUGIN_KIND;

fn adaptive_component_config(agent_id: &str) -> serde_json::Map<String, Value> {
    json!({
        "agent_id": agent_id,
        "state": {
            "backend": {
                "kind": "in_memory",
                "config": {}
            }
        },
        "telemetry": {
            "learners": ["tool_parallelism"]
        },
        "adaptive_hints": {
            "priority": 100,
            "break_chain": false,
            "inject_header": true,
            "inject_body_path": "nvext.agent_hints"
        }
    })
    .as_object()
    .unwrap()
    .clone()
}

#[test]
fn target_scope_defaults_to_user_and_rejects_conflicts() {
    assert_eq!(
        target_scope(&PluginsEditCommand::default()).unwrap(),
        TargetScope::User
    );
    assert_eq!(
        target_scope(&PluginsEditCommand {
            project: true,
            ..PluginsEditCommand::default()
        })
        .unwrap(),
        TargetScope::Project
    );
    assert_eq!(
        target_scope(&PluginsEditCommand {
            global: true,
            ..PluginsEditCommand::default()
        })
        .unwrap(),
        TargetScope::Global
    );

    let error = target_scope(&PluginsEditCommand {
        user: true,
        project: true,
        ..PluginsEditCommand::default()
    })
    .unwrap_err()
    .to_string();
    assert!(error.contains("choose only one"), "error was: {error}");
}

#[test]
fn typed_editor_model_contains_observability_sections() {
    let schema = ObservabilityConfig::editor_schema();
    let atof = schema.field("atof").unwrap().schema().unwrap();
    let atif = schema.field("atif").unwrap().schema().unwrap();
    let openinference = schema.field("openinference").unwrap().schema().unwrap();
    assert!(atof.fields.iter().any(|field| field.name == "mode"));
    assert!(
        atif.fields
            .iter()
            .any(|field| field.name == "filename_template")
    );
    assert!(
        openinference
            .fields
            .iter()
            .any(|field| field.name == "endpoint")
    );
}

#[test]
fn typed_editor_model_contains_adaptive_options() {
    let schema = AdaptiveConfig::editor_schema();
    assert!(!schema.fields.iter().any(|field| field.name == "version"));
    let agent_id = schema.field("agent_id").unwrap();
    assert_eq!(agent_id.label, "fallback_agent_id");

    let state = schema.field("state").unwrap().schema().unwrap();
    let backend = state.field("backend").unwrap().schema().unwrap();
    assert_eq!(
        backend.field("kind").unwrap().enum_values,
        &["in_memory", "redis"]
    );
    assert_eq!(backend.field("config").unwrap().kind, EditorFieldKind::Json);

    let telemetry = schema.field("telemetry").unwrap().schema().unwrap();
    assert_eq!(
        telemetry.field("learners").unwrap().kind,
        EditorFieldKind::Json
    );

    let acg = schema.field("acg").unwrap().schema().unwrap();
    let thresholds = acg.field("stability_thresholds").unwrap().schema().unwrap();
    assert_eq!(
        thresholds.field("stable_threshold").unwrap().kind,
        EditorFieldKind::Float
    );
}

#[test]
fn plugin_menu_uses_setup_theme_markers() {
    let theme = ColorfulTheme::default();
    let lines = render_menu(
        &theme,
        "plugins.toml",
        &[MenuItem::new("First"), MenuItem::new("Second")],
        0,
    );
    let rendered = lines.join("\n");

    assert!(rendered.contains('?'));
    assert!(rendered.contains('›'));
    assert!(rendered.contains('❯'));
    assert!(rendered.contains("↑/↓"));
    assert!(!rendered.contains("> First"));
}

#[test]
fn menu_response_index_tracks_selected_and_shortcut_positions() {
    assert_eq!(menu_response_index(&MenuResponse::Selected(3)), Some(3));
    assert_eq!(
        menu_response_index(&MenuResponse::Shortcut(MenuShortcut::Reset, 4)),
        Some(4)
    );
    assert_eq!(menu_response_index(&MenuResponse::Cancel), None);
}

#[test]
fn plugin_menu_marks_configured_sections_and_fields() {
    let mut observability = ObservabilityConfig::default();
    let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
    let mode = atof.schema().unwrap().field("mode").unwrap();
    let output_directory = atof.schema().unwrap().field("output_directory").unwrap();

    assert!(!section_configured(&observability, atof));
    ensure_section(&mut observability, atof);
    assert!(section_configured(&observability, atof));
    assert!(!section_field_configured(&observability, atof, mode).unwrap());
    assert!(!section_field_configured(&observability, atof, output_directory).unwrap());

    set_section_field(&mut observability, atof, "output_directory", json!("logs")).unwrap();
    assert!(section_field_configured(&observability, atof, output_directory).unwrap());
    assert!(configured_label(true, "Edit ATOF").contains('✓'));
    assert!(!configured_label(false, "Edit ATIF").contains('✓'));
}

#[test]
fn editor_model_renders_valid_observability_plugin_config() {
    let mut config = PluginConfig::default();
    ensure_observability_component(&mut config).unwrap();
    let mut observability = component_observability_config(&config).unwrap();
    let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
    toggle_section(&mut observability, atof);
    set_section_field(&mut observability, atof, "output_directory", json!("logs")).unwrap();
    set_section_field(&mut observability, atof, "filename", json!("events.jsonl")).unwrap();
    store_observability_config(&mut config, &observability).unwrap();

    validate_config(&config).unwrap();
}

#[test]
fn editor_model_adds_disabled_adaptive_component() {
    let mut config = PluginConfig::default();

    ensure_adaptive_component(&mut config).unwrap();

    let component = adaptive_component(&config).unwrap();
    assert_eq!(component.kind, ADAPTIVE_PLUGIN_KIND);
    assert!(!component.enabled);
    assert!(!component.config.contains_key("version"));
    assert!(component.config.contains_key("policy"));
}

#[test]
fn typed_editor_serializes_explicit_observability_overrides() {
    let mut observability = ObservabilityConfig::default();
    let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
    toggle_section(&mut observability, atof);
    set_section_field(&mut observability, atof, "output_directory", json!("logs")).unwrap();

    let map = observability_config_map(&observability).unwrap();
    let atof = map
        .get("atof")
        .and_then(Value::as_object)
        .expect("atof section is serialized");
    assert_eq!(atof.get("enabled"), Some(&Value::Bool(true)));
    assert_eq!(atof.get("output_directory"), Some(&json!("logs")));
    assert_eq!(atof.get("mode"), Some(&json!("append")));
    assert!(map.contains_key("policy"));
}

#[test]
fn typed_editor_serializes_disabled_section_override() {
    let mut observability = ObservabilityConfig::default();
    let atif = ObservabilityConfig::editor_schema().field("atif").unwrap();
    toggle_section(&mut observability, atif);
    toggle_section(&mut observability, atif);

    let map = observability_config_map(&observability).unwrap();
    let atif = map
        .get("atif")
        .and_then(Value::as_object)
        .expect("disabled atif section is serialized");
    assert_eq!(atif.get("enabled"), Some(&Value::Bool(false)));
    assert_eq!(
        atif.get("filename_template"),
        Some(&json!("nemo-flow-atif-{session_id}.json"))
    );
}

#[test]
fn editor_save_preserves_unknown_observability_fields() {
    let mut config = PluginConfig {
        components: vec![PluginComponentSpec {
            kind: OBSERVABILITY_PLUGIN_KIND.to_string(),
            enabled: true,
            config: json!({
                "version": 1,
                "future_top_level": "preserve",
                "atof": {
                    "enabled": true,
                    "output_directory": "old-logs",
                    "future_atof_field": "preserve"
                }
            })
            .as_object()
            .unwrap()
            .clone(),
        }],
        ..PluginConfig::default()
    };
    let mut observability = component_observability_config(&config).unwrap();
    let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
    remove_section_field(&mut observability, atof, "output_directory").unwrap();
    set_section_field(&mut observability, atof, "filename", json!("events.jsonl")).unwrap();

    store_observability_config(&mut config, &observability).unwrap();

    let component = observability_component(&config).unwrap();
    assert_eq!(
        component.config.get("future_top_level"),
        Some(&json!("preserve"))
    );
    let atof_config = component
        .config
        .get("atof")
        .and_then(Value::as_object)
        .unwrap();
    assert_eq!(
        atof_config.get("future_atof_field"),
        Some(&json!("preserve"))
    );
    assert_eq!(atof_config.get("filename"), Some(&json!("events.jsonl")));
    assert!(!atof_config.contains_key("output_directory"));
}

#[test]
fn editor_save_preserves_unknown_adaptive_fields_and_all_sections() {
    let mut config = PluginConfig {
        components: vec![PluginComponentSpec {
            kind: ADAPTIVE_PLUGIN_KIND.to_string(),
            enabled: true,
            config: json!({
                "version": 1,
                "future_top_level": "preserve",
                "state": {
                    "future_state": "preserve",
                    "backend": {
                        "kind": "in_memory",
                        "config": {},
                        "future_backend": "preserve"
                    }
                }
            })
            .as_object()
            .unwrap()
            .clone(),
        }],
        ..PluginConfig::default()
    };
    let mut adaptive = component_adaptive_config(&config).unwrap();
    let schema = AdaptiveConfig::editor_schema();
    let state = schema.field("state").unwrap();
    let telemetry = schema.field("telemetry").unwrap();
    let adaptive_hints = schema.field("adaptive_hints").unwrap();
    let tool_parallelism = schema.field("tool_parallelism").unwrap();
    let acg = schema.field("acg").unwrap();

    set_struct_field(&mut adaptive, "agent_id", json!("planner")).unwrap();
    set_section_field(
        &mut adaptive,
        state,
        "backend",
        json!({
            "kind": "redis",
            "config": {
                "url": "redis://127.0.0.1/",
                "key_prefix": "adaptive:"
            }
        }),
    )
    .unwrap();
    set_section_field(
        &mut adaptive,
        telemetry,
        "learners",
        json!(["tool_parallelism", "acg"]),
    )
    .unwrap();
    set_section_field(
        &mut adaptive,
        telemetry,
        "subscriber_name",
        json!("adaptive"),
    )
    .unwrap();
    set_section_field(
        &mut adaptive,
        adaptive_hints,
        "inject_body_path",
        json!("nvext.agent_hints"),
    )
    .unwrap();
    set_section_field(
        &mut adaptive,
        tool_parallelism,
        "mode",
        json!("inject_hints"),
    )
    .unwrap();
    set_section_field(&mut adaptive, acg, "provider", json!("anthropic")).unwrap();
    set_section_field(
        &mut adaptive,
        acg,
        "stability_thresholds",
        json!({
            "stable_threshold": 0.9,
            "semi_stable_threshold": 0.4,
            "min_observations_for_full_confidence": 10
        }),
    )
    .unwrap();

    store_adaptive_config(&mut config, &adaptive).unwrap();

    let component = adaptive_component(&config).unwrap();
    assert!(!component.config.contains_key("version"));
    assert_eq!(
        component.config.get("future_top_level"),
        Some(&json!("preserve"))
    );
    let state = component.config["state"].as_object().unwrap();
    assert_eq!(state.get("future_state"), Some(&json!("preserve")));
    let backend = state["backend"].as_object().unwrap();
    assert_eq!(backend.get("kind"), Some(&json!("redis")));
    assert_eq!(backend.get("future_backend"), Some(&json!("preserve")));
    assert_eq!(backend["config"]["key_prefix"], json!("adaptive:"));
    assert_eq!(
        component.config["telemetry"]["learners"],
        json!(["tool_parallelism", "acg"])
    );
    assert_eq!(
        component.config["adaptive_hints"]["inject_body_path"],
        json!("nvext.agent_hints")
    );
    assert_eq!(
        component.config["tool_parallelism"]["mode"],
        json!("inject_hints")
    );
    assert_eq!(
        component.config["acg"]["stability_thresholds"]["stable_threshold"],
        json!(0.9)
    );
}

#[test]
fn adaptive_config_field_reset_handles_optional_and_default_fields() {
    let mut adaptive = AdaptiveConfig {
        agent_id: Some("planner".into()),
        acg: Some(Default::default()),
        ..AdaptiveConfig::default()
    };
    let schema = AdaptiveConfig::editor_schema();

    reset_config_field(&mut adaptive, schema.field("agent_id").unwrap()).unwrap();
    reset_config_field(&mut adaptive, schema.field("acg").unwrap()).unwrap();

    assert!(adaptive.agent_id.is_none());
    assert!(adaptive.acg.is_none());
}

#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
struct OptionalSectionHarness {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    optional: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    parent: Option<Value>,
}

fn optional_section_without_default(name: &'static str) -> EditorFieldSpec {
    EditorFieldSpec {
        name,
        label: name,
        kind: EditorFieldKind::Section,
        enum_values: &[],
        optional: true,
        nested_schema: None,
        nested_default: None,
    }
}

#[test]
fn reset_section_clears_optional_section_without_default() {
    let section = optional_section_without_default("optional");
    let mut config = OptionalSectionHarness {
        optional: Some(json!({})),
        parent: None,
    };

    reset_section(&mut config, section);

    assert!(config.optional.is_none());
}

#[test]
fn nested_edit_empty_optional_section_without_default_clears_field() {
    let optional = optional_section_without_default("optional");
    let parent = optional_section_without_default("parent");
    let child = optional_section_without_default("child");
    let mut config = OptionalSectionHarness {
        optional: Some(json!({ "old": true })),
        parent: Some(json!({
            "child": {},
            "kept": true
        })),
    };

    store_edited_config_section(&mut config, optional, json!({})).unwrap();
    assert!(config.optional.is_none());

    store_edited_section_field(&mut config, parent, child, json!({})).unwrap();
    let parent = config.parent.as_ref().unwrap().as_object().unwrap();
    assert!(!parent.contains_key("child"));
    assert_eq!(parent.get("kept"), Some(&json!(true)));

    let mut value = json!({ "child": {}, "kept": true });
    store_edited_value_section(&mut value, child, json!({}));
    let value = value.as_object().unwrap();
    assert!(!value.contains_key("child"));
    assert_eq!(value.get("kept"), Some(&json!(true)));
}

#[test]
fn observability_config_field_reset_clears_optional_section() {
    let mut observability = ObservabilityConfig::default();
    let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
    toggle_section(&mut observability, atof);

    reset_config_field(&mut observability, atof).unwrap();

    assert!(observability.atof.is_none());
}

#[test]
fn adaptive_summary_tracks_component_and_configured_fields() {
    let mut config = PluginConfig::default();
    ensure_adaptive_component(&mut config).unwrap();
    let mut adaptive = component_adaptive_config(&config).unwrap();

    assert_eq!(
        adaptive_summary(&config, &adaptive),
        "component disabled, fields none"
    );

    set_adaptive_component_enabled(&mut config, true);
    set_struct_field(&mut adaptive, "agent_id", json!("planner")).unwrap();
    let adaptive_hints = AdaptiveConfig::editor_schema()
        .field("adaptive_hints")
        .unwrap();
    set_section_field(&mut adaptive, adaptive_hints, "inject_header", json!(true)).unwrap();

    assert_eq!(
        adaptive_summary(&config, &adaptive),
        "component enabled, fields fallback_agent_id, adaptive_hints"
    );
}

#[test]
fn component_enablement_and_summary_track_config_state() {
    let mut config = PluginConfig::default();
    ensure_observability_component(&mut config).unwrap();
    let mut observability = component_observability_config(&config).unwrap();

    assert!(component_enabled(&config));
    assert_eq!(
        observability_summary(&config, &observability),
        "component enabled, sections none"
    );

    set_component_enabled(&mut config, false);
    let atif = ObservabilityConfig::editor_schema().field("atif").unwrap();
    toggle_section(&mut observability, atif);

    assert!(!component_enabled(&config));
    assert_eq!(
        observability_summary(&config, &observability),
        "component disabled, sections ATIF"
    );
}

#[test]
fn reset_selected_field_accounts_for_section_toggle_offset() {
    let mut observability = ObservabilityConfig::default();
    let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
    let fields = atof.schema().unwrap().fields;

    set_section_field(&mut observability, atof, "output_directory", json!("logs")).unwrap();
    assert!(
        section_field_value(&observability, atof, "output_directory")
            .unwrap()
            .is_some()
    );

    let output_directory_index = fields
        .iter()
        .position(|field| field.name == "output_directory")
        .unwrap();
    assert!(
        reset_selected_field(&mut observability, atof, fields, output_directory_index + 1,)
            .unwrap()
    );
    assert_eq!(
        section_field_value(&observability, atof, "output_directory").unwrap(),
        None
    );
    assert!(!reset_selected_field(&mut observability, atof, fields, 0).unwrap());
}

#[test]
fn read_plugin_config_handles_missing_and_invalid_files() {
    let temp = tempfile::tempdir().unwrap();
    let missing = temp.path().join("plugins.toml");
    let config = read_plugin_config(&missing).unwrap();
    assert!(config.components.is_empty());

    std::fs::write(&missing, "components = [\n").unwrap();
    let error = read_plugin_config(&missing).unwrap_err().to_string();
    assert!(error.contains("invalid plugin TOML"), "error was: {error}");
}

#[test]
fn write_plugin_config_prunes_defaults_and_round_trips() {
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("plugins.toml");
    let mut config = PluginConfig::default();
    ensure_observability_component(&mut config).unwrap();
    config.components.push(PluginComponentSpec {
        kind: ADAPTIVE_PLUGIN_KIND.to_string(),
        enabled: true,
        config: adaptive_component_config("cli-roundtrip"),
    });

    write_plugin_config(&path, &config).unwrap();

    let rendered = std::fs::read_to_string(&path).unwrap();
    assert!(rendered.contains("kind = \"observability\""));
    assert!(rendered.contains("kind = \"adaptive\""));
    assert!(!rendered.contains("enabled = true"));
    let round_tripped = read_plugin_config(&path).unwrap();
    assert_eq!(round_tripped.components.len(), 2);
    assert_eq!(round_tripped.components[0].kind, OBSERVABILITY_PLUGIN_KIND);
    let adaptive = round_tripped
        .components
        .iter()
        .find(|component| component.kind == ADAPTIVE_PLUGIN_KIND)
        .unwrap();
    assert_eq!(
        adaptive.config.get("agent_id"),
        Some(&json!("cli-roundtrip"))
    );
    let adaptive_hints = adaptive
        .config
        .get("adaptive_hints")
        .and_then(Value::as_object)
        .unwrap();
    assert_eq!(
        adaptive_hints.get("inject_body_path"),
        Some(&json!("nvext.agent_hints"))
    );
}

#[test]
fn prune_plugin_defaults_removes_default_policy_and_enabled_true_only() {
    let mut value = json!({
        "version": 1,
        "policy": ConfigPolicy::default(),
        "components": [
            { "kind": "observability", "enabled": true, "config": {} },
            { "kind": "other", "enabled": false, "config": {} }
        ]
    });

    prune_plugin_defaults(&mut value);

    let object = value.as_object().unwrap();
    assert!(!object.contains_key("policy"));
    let components = object["components"].as_array().unwrap();
    assert!(!components[0].as_object().unwrap().contains_key("enabled"));
    assert_eq!(components[1]["enabled"], json!(false));
}

#[test]
fn validate_config_reports_plugin_diagnostics() {
    let config = PluginConfig {
        components: vec![PluginComponentSpec {
            kind: OBSERVABILITY_PLUGIN_KIND.to_string(),
            enabled: true,
            config: json!({
                "version": 1,
                "atof": {
                    "enabled": true,
                    "mode": "not-a-mode"
                }
            })
            .as_object()
            .unwrap()
            .clone(),
        }],
        ..PluginConfig::default()
    };

    let error = validate_config(&config).unwrap_err().to_string();

    assert!(
        error.contains("plugin validation failed"),
        "error was: {error}"
    );
    assert!(error.contains("ATOF mode"), "error was: {error}");
}

#[test]
fn validate_config_accepts_adaptive_component() {
    let config = PluginConfig {
        components: vec![PluginComponentSpec {
            kind: ADAPTIVE_PLUGIN_KIND.to_string(),
            enabled: true,
            config: adaptive_component_config("cli-validation"),
        }],
        ..PluginConfig::default()
    };

    validate_config(&config).unwrap();
}

#[test]
fn display_helpers_render_scalars_json_and_defaults() {
    assert_eq!(display_value(&json!("logs")), "logs");
    assert_eq!(display_value(&json!(true)), "true");
    assert_eq!(display_value(&json!(7)), "7");
    assert_eq!(display_value(&json!({ "a": 1 })), r#"{"a":1}"#);

    let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
    let mode = atof.schema().unwrap().field("mode").unwrap();
    assert_eq!(
        display_field_value(atof, mode, &json!("append")),
        "append (default)"
    );
    assert_eq!(
        display_field_value(atof, mode, &json!("overwrite")),
        "overwrite"
    );
}

#[test]
fn parse_float_value_rejects_non_finite_numbers() {
    let field = EditorFieldSpec {
        name: "stable_threshold",
        label: "Stable threshold",
        kind: EditorFieldKind::Float,
        enum_values: &[],
        optional: false,
        nested_schema: None,
        nested_default: None,
    };

    assert_eq!(parse_float_value(&field, "0.75").unwrap(), json!(0.75));

    for value in ["inf", "-inf", "NaN"] {
        let error = parse_float_value(&field, value).unwrap_err().to_string();
        assert!(
            error.contains("stable_threshold must be a finite number"),
            "error was: {error}"
        );
        assert!(error.contains(value), "error was: {error}");
    }
}

#[test]
fn target_path_resolves_project_and_global_without_user_env() {
    let cwd = std::env::current_dir().unwrap();

    assert_eq!(
        target_path(TargetScope::Project).unwrap(),
        project_plugin_config_path(&cwd)
    );
    assert_eq!(
        target_path(TargetScope::Global).unwrap(),
        global_plugin_config_path()
    );
}
