#![cfg(feature = "discovery-kube")]

use rs_zero::discovery::Discovery;
use rs_zero::discovery_kube::{
    KubeDiscovery, KubeDiscoveryCache, KubeDiscoveryConfig, KubeSliceCacheUpdate,
    map_endpoint_slice_json, map_endpoints,
};

const ENDPOINTS: &str = r#"
[
  {"service":"api","id":"api-a","address":"10.0.0.1","port":8080,"port_name":"http","ready":true},
  {"service":"api","id":"api-b","address":"10.0.0.2","port":8081,"port_name":"admin","ready":true},
  {"service":"api","id":"api-c","address":"10.0.0.3","port":8080,"port_name":"http","ready":false}
]
"#;

const ENDPOINT_SLICE: &str = r#"
{
  "apiVersion":"discovery.k8s.io/v1",
  "kind":"EndpointSlice",
  "metadata":{
    "name":"api-abc",
    "namespace":"default",
    "labels":{"kubernetes.io/service-name":"api"}
  },
  "addressType":"IPv4",
  "ports":[
    {"name":"http","port":8080,"protocol":"TCP"},
    {"name":"admin","port":9090,"protocol":"TCP"}
  ],
  "endpoints":[
    {"addresses":["10.0.0.1"],"conditions":{"ready":true,"serving":true},"targetRef":{"kind":"Pod","name":"api-a","uid":"pod-a"},"nodeName":"node-a","zone":"local-a"},
    {"addresses":["10.0.0.2"],"conditions":{"ready":false,"serving":true},"targetRef":{"kind":"Pod","name":"api-b","uid":"pod-b"}},
    {"addresses":["10.0.0.3"],"conditions":{"ready":true,"serving":true,"terminating":true},"targetRef":{"kind":"Pod","name":"api-c","uid":"pod-c"}},
    {"addresses":["10.0.0.4"],"conditions":{"ready":true,"serving":false},"targetRef":{"kind":"Pod","name":"api-d","uid":"pod-d"}}
  ]
}
"#;

#[tokio::test]
async fn kube_fixture_mapper_stays_available_for_offline_tests() {
    let config = KubeDiscoveryConfig {
        port_name: Some("http".to_string()),
        ..KubeDiscoveryConfig::default()
    };
    let instances = map_endpoints(&config, ENDPOINTS).expect("map");
    assert_eq!(instances.len(), 1);
    assert_eq!(instances[0].id, "api-a");

    let discovery = KubeDiscovery::from_snapshot(config, "api", ENDPOINTS);
    assert_eq!(discovery.discover("api").await.expect("discover").len(), 1);
}

#[test]
fn endpoint_slice_mapper_filters_ready_named_ports_and_adds_metadata() {
    let mut config = KubeDiscoveryConfig {
        namespace: "default".to_string(),
        service_name: Some("api".to_string()),
        port_name: Some("http".to_string()),
        ..KubeDiscoveryConfig::default()
    };
    config
        .metadata
        .insert("cluster".to_string(), "docker-desktop".to_string());

    let update = map_endpoint_slice_json(&config, ENDPOINT_SLICE).expect("slice");
    assert_eq!(update.service, "api");
    assert_eq!(update.slice, "api-abc");
    assert_eq!(update.instances.len(), 1);

    let instance = &update.instances[0];
    assert_eq!(instance.id, "pod-a");
    assert_eq!(instance.endpoint.authority(), "10.0.0.1:8080");
    assert_eq!(instance.metadata["cluster"], "docker-desktop");
    assert_eq!(instance.metadata["kubernetes.namespace"], "default");
    assert_eq!(instance.metadata["kubernetes.endpoint_slice"], "api-abc");
    assert_eq!(instance.metadata["kubernetes.node"], "node-a");
}

#[test]
fn endpoint_slice_mapper_reports_missing_named_port() {
    let config = KubeDiscoveryConfig {
        port_name: Some("grpc".to_string()),
        ..KubeDiscoveryConfig::default()
    };

    let error = map_endpoint_slice_json(&config, ENDPOINT_SLICE).expect_err("missing port");
    assert!(error.to_string().contains("port"));
}

#[test]
fn kube_cache_applies_init_apply_and_delete_without_stale_instances() {
    let mut cache = KubeDiscoveryCache::default();
    let update = KubeSliceCacheUpdate {
        service: "api".to_string(),
        slice: "api-abc".to_string(),
        instances: map_endpoint_slice_json(&KubeDiscoveryConfig::default(), ENDPOINT_SLICE)
            .expect("slice")
            .instances,
    };

    cache.begin_init();
    cache.buffer_init(update.clone());
    cache.finish_init();
    assert_eq!(cache.discover("api").expect("instances").len(), 1);
    assert!(cache.status().synced);

    cache.delete_slice("api", "api-abc");
    assert!(cache.discover("api").is_err());
}

#[test]
fn kube_cache_exposes_not_synced_and_last_error_status() {
    let mut cache = KubeDiscoveryCache::default();

    let error = cache.discover("api").expect_err("not synced");
    assert!(error.to_string().contains("not synchronized"));

    cache.mark_error("watch stream failed");
    let status = cache.status();
    assert!(!status.synced);
    assert_eq!(status.resource_kind, "EndpointSlice");
    assert_eq!(status.last_error.as_deref(), Some("watch stream failed"));
}
