use std::collections::HashMap;

use super::*;
use crate::config::{BackendConfig, Config, ServerConfig, TransportConfig};
use notify::event::EventAttributes;

// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------

fn http_backend(url: &str) -> BackendConfig {
    BackendConfig {
        transport: TransportConfig::Http {
            http_url: url.to_string(),
            streamable_http: false,
            protocol_version: None,
        },
        enabled: true,
        ..BackendConfig::default()
    }
}

fn disabled_backend(url: &str) -> BackendConfig {
    BackendConfig {
        enabled: false,
        transport: TransportConfig::Http {
            http_url: url.to_string(),
            streamable_http: false,
            protocol_version: None,
        },
        ..BackendConfig::default()
    }
}

fn config_with_backends(backends: HashMap<String, BackendConfig>) -> Config {
    Config {
        backends,
        ..Config::default()
    }
}

// -------------------------------------------------------------------------
// compute_diff: no-op cases
// -------------------------------------------------------------------------

#[test]
fn diff_identical_configs_returns_empty_patch() {
    // GIVEN: two identical default configs
    let old = Config::default();
    let new = Config::default();
    // WHEN: diff is computed
    let patch = compute_diff(&old, &new);
    // THEN: patch is empty
    assert!(
        patch.is_empty(),
        "expected empty patch, got: {}",
        patch.summary()
    );
}

#[test]
fn diff_same_backends_returns_empty_patch() {
    // GIVEN: two configs with identical backends
    let mut backends = HashMap::new();
    backends.insert(
        "alpha".to_string(),
        http_backend("http://localhost:8001/mcp"),
    );
    let old = config_with_backends(backends.clone());
    let new = config_with_backends(backends);
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert!(patch.is_empty());
}

// -------------------------------------------------------------------------
// compute_diff: additions
// -------------------------------------------------------------------------

#[test]
fn diff_detects_added_backend() {
    // GIVEN: old has no backends, new has one
    let old = Config::default();
    let mut backends = HashMap::new();
    backends.insert(
        "new-svc".to_string(),
        http_backend("http://localhost:9000/mcp"),
    );
    let new = config_with_backends(backends);
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert_eq!(patch.backends_added.len(), 1);
    assert_eq!(patch.backends_added[0].0, "new-svc");
    assert!(patch.backends_removed.is_empty());
    assert!(patch.backends_modified.is_empty());
}

#[test]
fn diff_disabled_backend_not_treated_as_added() {
    // GIVEN: old has no backends, new has one but it is disabled
    let old = Config::default();
    let mut backends = HashMap::new();
    backends.insert(
        "ghost".to_string(),
        disabled_backend("http://localhost:9001/mcp"),
    );
    let new = config_with_backends(backends);
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN: disabled backends are invisible to the diff
    assert!(patch.backends_added.is_empty());
}

// -------------------------------------------------------------------------
// compute_diff: removals
// -------------------------------------------------------------------------

#[test]
fn diff_detects_removed_backend() {
    // GIVEN: old has a backend, new has none
    let mut backends = HashMap::new();
    backends.insert(
        "legacy".to_string(),
        http_backend("http://localhost:8002/mcp"),
    );
    let old = config_with_backends(backends);
    let new = Config::default();
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert_eq!(patch.backends_removed.len(), 1);
    assert_eq!(patch.backends_removed[0], "legacy");
    assert!(patch.backends_added.is_empty());
    assert!(patch.backends_modified.is_empty());
}

#[test]
fn diff_backend_disabled_counts_as_removed() {
    // GIVEN: old has enabled backend, new has same backend but disabled
    let mut old_backends = HashMap::new();
    old_backends.insert("svc".to_string(), http_backend("http://localhost:8003/mcp"));
    let old = config_with_backends(old_backends);

    let mut new_backends = HashMap::new();
    new_backends.insert(
        "svc".to_string(),
        disabled_backend("http://localhost:8003/mcp"),
    );
    let new = config_with_backends(new_backends);
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN: disabling is treated as removal
    assert_eq!(patch.backends_removed.len(), 1);
    assert_eq!(patch.backends_removed[0], "svc");
    assert!(patch.backends_added.is_empty());
}

// -------------------------------------------------------------------------
// compute_diff: modifications
// -------------------------------------------------------------------------

#[test]
fn diff_detects_modified_backend_url() {
    // GIVEN: same name, different URL
    let mut old_backends = HashMap::new();
    old_backends.insert("api".to_string(), http_backend("http://localhost:8080/mcp"));
    let old = config_with_backends(old_backends);

    let mut new_backends = HashMap::new();
    new_backends.insert("api".to_string(), http_backend("http://localhost:8081/mcp"));
    let new = config_with_backends(new_backends);
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert_eq!(patch.backends_modified.len(), 1);
    assert_eq!(patch.backends_modified[0].0, "api");
    assert!(patch.backends_added.is_empty());
    assert!(patch.backends_removed.is_empty());
}

#[test]
fn diff_detects_modified_backend_timeout() {
    // GIVEN: same URL, different timeout
    let mut old_cfg = http_backend("http://localhost:9090/mcp");
    old_cfg.timeout = Duration::from_secs(30);
    let mut new_cfg = http_backend("http://localhost:9090/mcp");
    new_cfg.timeout = Duration::from_secs(60);

    let old = config_with_backends([("svc".to_string(), old_cfg)].into());
    let new = config_with_backends([("svc".to_string(), new_cfg)].into());
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert_eq!(patch.backends_modified.len(), 1);
}

// -------------------------------------------------------------------------
// compute_diff: server changes
// -------------------------------------------------------------------------

#[test]
fn diff_detects_server_port_change() {
    // GIVEN: server port differs
    let old = Config {
        server: ServerConfig {
            port: 39400,
            ..ServerConfig::default()
        },
        ..Config::default()
    };
    let new = Config {
        server: ServerConfig {
            port: 39401,
            ..ServerConfig::default()
        },
        ..Config::default()
    };
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert!(patch.server_changed);
}

#[test]
fn diff_detects_public_url_only_change() {
    // GIVEN: only server.public_url differs (host/port unchanged)
    let old = Config::default();
    let new = Config {
        server: ServerConfig {
            public_url: Some("https://mcp.acme.internal".to_string()),
            ..ServerConfig::default()
        },
        ..Config::default()
    };
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN: hot-reloadable (endpoint reads live_config), so it is a profile
    // change, not a restart-required server-address change. MIK-6750.
    assert!(patch.profiles_changed, "public_url edit must be detected");
    assert!(
        !patch.server_changed,
        "public_url does not move the listener"
    );
}

#[test]
fn diff_same_server_no_server_change() {
    // GIVEN: identical server configs
    let old = Config::default();
    let new = Config::default();
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert!(!patch.server_changed);
}

// -------------------------------------------------------------------------
// ConfigPatch::is_empty / summary
// -------------------------------------------------------------------------

#[test]
fn patch_is_empty_for_default() {
    let patch = ConfigPatch::default();
    assert!(patch.is_empty());
    assert_eq!(patch.summary(), "no changes");
}

#[test]
fn patch_summary_lists_all_change_types() {
    // GIVEN: a patch with every field populated
    let patch = ConfigPatch {
        backends_added: vec![("x".to_string(), BackendConfig::default())],
        backends_removed: vec!["y".to_string()],
        backends_modified: vec![("z".to_string(), BackendConfig::default())],
        server_changed: true,
        profiles_changed: true,
    };
    let s = patch.summary();
    // THEN: all sections appear in the summary
    assert!(s.contains("added backends"), "missing added: {s}");
    assert!(s.contains("removed backends"), "missing removed: {s}");
    assert!(s.contains("modified backends"), "missing modified: {s}");
    assert!(s.contains("restart required"), "missing server: {s}");
    assert!(s.contains("profiles"), "missing profiles: {s}");
}

#[test]
fn patch_outcome_exposes_restart_required_reason() {
    let patch = ConfigPatch {
        server_changed: true,
        ..ConfigPatch::default()
    };

    let outcome = patch.outcome();

    assert!(outcome.restart_required);
    assert_eq!(outcome.restart_reason, Some("server_address_changed"));
    assert!(outcome.changes.contains("restart required"));
}

#[test]
fn reload_outcome_no_changes_is_explicit() {
    let outcome = ReloadOutcome::no_changes();

    assert_eq!(outcome.changes, "no changes detected");
    assert!(!outcome.restart_required);
    assert_eq!(outcome.restart_reason, None);
}

// -------------------------------------------------------------------------
// LiveConfig
// -------------------------------------------------------------------------

#[test]
fn live_config_get_returns_initial_config() {
    let cfg = Config::default();
    let live = LiveConfig::new(cfg.clone());
    let got = live.get();
    assert_eq!(got.server.port, cfg.server.port);
}

#[test]
fn live_config_set_updates_snapshot() {
    let live = LiveConfig::new(Config::default());
    let mut new_cfg = Config::default();
    new_cfg.server.port = 12345;
    live.set(new_cfg);
    assert_eq!(live.get().server.port, 12345);
}

// -------------------------------------------------------------------------
// diff: multiple simultaneous changes
// -------------------------------------------------------------------------

#[test]
fn diff_handles_mixed_add_remove_modify() {
    // GIVEN: old={a, b}, new={b(modified), c}
    let mut old_backends = HashMap::new();
    old_backends.insert("a".to_string(), http_backend("http://localhost:1001/mcp"));
    old_backends.insert("b".to_string(), http_backend("http://localhost:1002/mcp"));
    let old = config_with_backends(old_backends);

    let mut new_backends = HashMap::new();
    new_backends.insert("b".to_string(), http_backend("http://localhost:1099/mcp")); // modified
    new_backends.insert("c".to_string(), http_backend("http://localhost:1003/mcp")); // added
    let new = config_with_backends(new_backends);

    // WHEN
    let patch = compute_diff(&old, &new);

    // THEN
    assert_eq!(patch.backends_added.len(), 1, "expected c added");
    assert_eq!(patch.backends_added[0].0, "c");

    assert_eq!(patch.backends_removed.len(), 1, "expected a removed");
    assert_eq!(patch.backends_removed[0], "a");

    assert_eq!(patch.backends_modified.len(), 1, "expected b modified");
    assert_eq!(patch.backends_modified[0].0, "b");
}

// -------------------------------------------------------------------------
// expand_tilde
// -------------------------------------------------------------------------

#[test]
fn expand_tilde_leaves_absolute_path_unchanged() {
    // GIVEN: a path that does not start with ~
    let path = super::expand_tilde("/etc/secrets.env");
    // THEN: returned as-is
    assert_eq!(path, std::path::PathBuf::from("/etc/secrets.env"));
}

#[test]
fn expand_tilde_expands_home_prefix() {
    // GIVEN: a tilde-prefixed path
    let path = super::expand_tilde("~/.claude/secrets.env");
    // THEN: ~ is replaced — we just verify it no longer starts with ~
    let path_str = path.to_string_lossy();
    assert!(
        !path_str.starts_with('~'),
        "expected ~ to be expanded, got: {path_str}"
    );
    assert!(
        path_str.ends_with(".claude/secrets.env"),
        "expected suffix preserved, got: {path_str}"
    );
}

// -------------------------------------------------------------------------
// resolve_env_file_paths
// -------------------------------------------------------------------------

#[test]
fn resolve_env_file_paths_expands_tilde_entries() {
    // GIVEN: a mix of absolute and tilde paths
    let raw = vec![
        "/tmp/a.env".to_string(),
        "~/.claude/secrets.env".to_string(),
    ];
    // WHEN
    let resolved = super::resolve_env_file_paths(&raw);
    // THEN: two entries, first unchanged, second has ~ expanded
    assert_eq!(resolved.len(), 2);
    assert_eq!(resolved[0], std::path::PathBuf::from("/tmp/a.env"));
    assert!(!resolved[1].to_string_lossy().starts_with('~'));
}

#[test]
fn resolve_env_file_paths_empty_input_returns_empty() {
    // GIVEN: empty slice
    let resolved = super::resolve_env_file_paths(&[]);
    // THEN: empty vec
    assert!(resolved.is_empty());
}

// -------------------------------------------------------------------------
// is_config_event
// -------------------------------------------------------------------------

#[test]
fn is_config_event_matches_modify_on_exact_path() {
    use notify::{EventKind, event::ModifyKind};

    // GIVEN: a Modify event on the watched path
    let config_path = std::path::PathBuf::from("/tmp/config.yaml");
    let event = notify::Event {
        kind: EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Any)),
        paths: vec![config_path.clone()],
        attrs: EventAttributes::default(),
    };
    // WHEN / THEN
    assert!(super::is_config_event(&event, &config_path));
}

#[test]
fn is_config_event_does_not_match_different_path() {
    use notify::{EventKind, event::ModifyKind};

    // GIVEN: a Modify event on a different path
    let config_path = std::path::PathBuf::from("/tmp/config.yaml");
    let other_path = std::path::PathBuf::from("/tmp/other.yaml");
    let event = notify::Event {
        kind: EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Any)),
        paths: vec![other_path],
        attrs: EventAttributes::default(),
    };
    // WHEN / THEN
    assert!(!super::is_config_event(&event, &config_path));
}

#[test]
fn is_config_event_does_not_match_remove_event() {
    use notify::{EventKind, event::RemoveKind};

    // GIVEN: a Remove event on the exact path
    let config_path = std::path::PathBuf::from("/tmp/config.yaml");
    let event = notify::Event {
        kind: EventKind::Remove(RemoveKind::File),
        paths: vec![config_path.clone()],
        attrs: EventAttributes::default(),
    };
    // WHEN / THEN: Remove is not a trigger (only Create/Modify are)
    assert!(!super::is_config_event(&event, &config_path));
}

// -------------------------------------------------------------------------
// matching_env_file
// -------------------------------------------------------------------------

#[test]
fn matching_env_file_returns_path_when_event_matches_watched_env_file() {
    use notify::{EventKind, event::ModifyKind};

    // GIVEN: an event for a watched env file
    let env_path = std::path::PathBuf::from("/home/user/.claude/secrets.env");
    let event = notify::Event {
        kind: EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Any)),
        paths: vec![env_path.clone()],
        attrs: EventAttributes::default(),
    };
    // WHEN
    let result = super::matching_env_file(&event, std::slice::from_ref(&env_path));
    // THEN
    assert_eq!(result, Some(env_path));
}

#[test]
fn matching_env_file_returns_none_when_path_not_in_watch_list() {
    use notify::{EventKind, event::ModifyKind};

    // GIVEN: an event for a file not in the watch list
    let watched = std::path::PathBuf::from("/home/user/.claude/secrets.env");
    let other = std::path::PathBuf::from("/tmp/other.env");
    let event = notify::Event {
        kind: EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Any)),
        paths: vec![other],
        attrs: EventAttributes::default(),
    };
    // WHEN / THEN
    assert!(super::matching_env_file(&event, &[watched]).is_none());
}

#[test]
fn matching_env_file_returns_none_for_remove_event() {
    use notify::{EventKind, event::RemoveKind};

    // GIVEN: a Remove event on a watched env file
    let env_path = std::path::PathBuf::from("/home/user/.claude/secrets.env");
    let event = notify::Event {
        kind: EventKind::Remove(RemoveKind::File),
        paths: vec![env_path.clone()],
        attrs: EventAttributes::default(),
    };
    // WHEN / THEN: Remove does not trigger an env-file reload
    assert!(super::matching_env_file(&event, &[env_path]).is_none());
}

#[test]
fn matching_env_file_returns_first_matching_path_among_multiple() {
    use notify::{EventKind, event::ModifyKind};

    // GIVEN: multiple watched env files, event hits the second
    let path_a = std::path::PathBuf::from("/tmp/a.env");
    let path_b = std::path::PathBuf::from("/tmp/b.env");
    let event = notify::Event {
        kind: EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Any)),
        paths: vec![path_b.clone()],
        attrs: EventAttributes::default(),
    };
    // WHEN
    let result = super::matching_env_file(&event, &[path_a, path_b.clone()]);
    // THEN: returns the matching path
    assert_eq!(result, Some(path_b));
}

#[test]
fn load_config_patch_rejects_invalid_config() {
    let dir = tempfile::tempdir().unwrap();
    let config_path = dir.path().join("gateway.yaml");
    std::fs::write(
        &config_path,
        r#"
backends:
  invalid_backend:
    http_url: "not a url"
"#,
    )
    .unwrap();

    let live_config = std::sync::Arc::new(LiveConfig::new(Config::default()));
    let result = load_config_patch(&config_path, &live_config);

    assert!(matches!(result, Err(msg) if msg.contains("Configuration validation error")));
}

// -------------------------------------------------------------------------
// compute_diff: MetaFields coverage — previously-missing top-level fields
// -------------------------------------------------------------------------

#[test]
fn diff_detects_routing_profiles_change() {
    // GIVEN: old has no routing profiles; new adds one
    use crate::routing_profile::RoutingProfileConfig;

    let old = Config::default();
    let mut new = Config::default();
    new.routing_profiles.insert(
        "limited".to_string(),
        RoutingProfileConfig {
            description: "limited profile".to_string(),
            ..RoutingProfileConfig::default()
        },
    );
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN: profiles_changed is set (routing_profiles is now covered by MetaFields)
    assert!(
        patch.profiles_changed,
        "adding a routing profile should set profiles_changed"
    );
}

#[test]
fn diff_runtime_profile_change_restarts_referencing_backend() {
    let mut old = Config::default();
    old.runtime.profiles.insert(
        "safe".to_string(),
        crate::config::RuntimeProfileConfig::default(),
    );
    old.backends.insert(
        "docs".to_string(),
        BackendConfig {
            transport: TransportConfig::Stdio {
                command: "node server.js".to_string(),
                cwd: None,
                protocol_version: None,
            },
            runtime_profile: Some("safe".to_string()),
            ..BackendConfig::default()
        },
    );

    let mut new = old.clone();
    new.runtime.profiles.insert(
        "safe".to_string(),
        crate::config::RuntimeProfileConfig {
            privileged: true,
            ..crate::config::RuntimeProfileConfig::default()
        },
    );

    let patch = compute_diff(&old, &new);
    assert!(patch.profiles_changed);
    assert_eq!(patch.backends_modified.len(), 1);
    assert_eq!(patch.backends_modified[0].0, "docs");
}

#[test]
fn diff_detects_default_routing_profile_change() {
    // GIVEN: default_routing_profile differs
    let old = Config::default();
    let new = Config {
        default_routing_profile: "custom".to_string(),
        ..Config::default()
    };
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert!(
        patch.profiles_changed,
        "changing default_routing_profile should set profiles_changed"
    );
}

#[test]
fn diff_detects_marketplace_change() {
    // GIVEN: marketplace plugin_dir differs
    let old = Config::default();
    let mut new = Config::default();
    new.marketplace.plugin_dir = "/tmp/plugins".to_string();
    // WHEN
    let patch = compute_diff(&old, &new);
    // THEN
    assert!(
        patch.profiles_changed,
        "changing marketplace config should set profiles_changed"
    );
}

#[test]
fn diff_same_routing_profiles_in_different_order_is_not_changed() {
    use crate::routing_profile::RoutingProfileConfig;

    let mut old = Config::default();
    old.routing_profiles.insert(
        "research".to_string(),
        RoutingProfileConfig {
            description: "Research only".to_string(),
            allow_tools: Some(vec!["search_*".to_string()]),
            ..RoutingProfileConfig::default()
        },
    );
    old.routing_profiles.insert(
        "ops".to_string(),
        RoutingProfileConfig {
            description: "Operations".to_string(),
            allow_backends: Some(vec!["ops_*".to_string()]),
            ..RoutingProfileConfig::default()
        },
    );
    old.default_routing_profile = "research".to_string();

    let mut new = Config::default();
    new.routing_profiles.insert(
        "ops".to_string(),
        RoutingProfileConfig {
            description: "Operations".to_string(),
            allow_backends: Some(vec!["ops_*".to_string()]),
            ..RoutingProfileConfig::default()
        },
    );
    new.routing_profiles.insert(
        "research".to_string(),
        RoutingProfileConfig {
            description: "Research only".to_string(),
            allow_tools: Some(vec!["search_*".to_string()]),
            ..RoutingProfileConfig::default()
        },
    );
    new.default_routing_profile = "research".to_string();

    let patch = compute_diff(&old, &new);

    assert!(
        patch.is_empty(),
        "routing profile key order should not trigger reloads: {}",
        patch.summary()
    );
}

#[test]
fn diff_same_backend_maps_in_different_order_is_not_modified() {
    let mut old_cfg = http_backend("http://localhost:8080/mcp");
    old_cfg.env.insert("ALPHA".to_string(), "1".to_string());
    old_cfg.env.insert("BETA".to_string(), "2".to_string());
    old_cfg
        .headers
        .insert("X-Trace".to_string(), "enabled".to_string());
    old_cfg
        .headers
        .insert("X-Client".to_string(), "gateway".to_string());

    let mut new_cfg = http_backend("http://localhost:8080/mcp");
    new_cfg.env.insert("BETA".to_string(), "2".to_string());
    new_cfg.env.insert("ALPHA".to_string(), "1".to_string());
    new_cfg
        .headers
        .insert("X-Client".to_string(), "gateway".to_string());
    new_cfg
        .headers
        .insert("X-Trace".to_string(), "enabled".to_string());

    let old = config_with_backends([("svc".to_string(), old_cfg)].into());
    let new = config_with_backends([("svc".to_string(), new_cfg)].into());

    let patch = compute_diff(&old, &new);

    assert!(
        patch.is_empty(),
        "backend map key order should not trigger reloads: {}",
        patch.summary()
    );
}

// MIK-6702.CP.RELOAD.2 — a control_plane.role_mapping-only change is detected
// (non-empty patch), so a mapping edit triggers the reload path instead of
// being silently ignored until restart.
#[test]
fn control_plane_role_mapping_change_is_detected() {
    use crate::control_plane::{
        ControlPlaneRole, ControlPlaneRoleMappingConfig, ControlPlaneRoleRule,
    };

    let old = Config::default();
    let mut new = Config::default();
    new.control_plane.role_mapping = ControlPlaneRoleMappingConfig {
        rules: vec![ControlPlaneRoleRule {
            issuer: "https://idp".to_string(),
            group: Some("admins".to_string()),
            email: None,
            domain: None,
            role: ControlPlaneRole::Admin,
        }],
    };

    let patch = compute_diff(&old, &new);
    assert!(
        !patch.is_empty(),
        "a control_plane.role_mapping change must be detected as a reloadable diff"
    );
    assert!(
        patch.profiles_changed,
        "control_plane change should set profiles_changed"
    );
}
