// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! JetStream stream validation integration tests for the NATS target.
//!
//! These tests are ignored because they require a running NATS server with JetStream enabled. To
//! run locally:
//!
//!     docker run -d --name rustfs-nats-test -p 4222:4222 nats:2 -js
//!     cargo test -p rustfs-targets --test nats_jetstream_validation_integration -- --ignored
//!
//! Override the server URL with RUSTFS_TEST_NATS_URL. Each test provisions and removes its own
//! stream. RustFS never provisions the stream, the test harness does, mirroring the operator role.

use async_nats::jetstream::stream::Config;
use rustfs_targets::check_nats_server_available;
use rustfs_targets::target::TargetType;
use rustfs_targets::target::nats::NATSArgs;
use std::time::Duration;
use uuid::Uuid;

fn server_url() -> String {
    std::env::var("RUSTFS_TEST_NATS_URL").unwrap_or_else(|_| "nats://127.0.0.1:4222".to_string())
}

fn jetstream_args(subject: &str, stream_name: &str) -> NATSArgs {
    NATSArgs {
        enable: true,
        address: server_url(),
        subject: subject.to_string(),
        username: String::new(),
        password: String::new(),
        token: String::new(),
        credentials_file: String::new(),
        tls_ca: String::new(),
        tls_client_cert: String::new(),
        tls_client_key: String::new(),
        tls_required: false,
        queue_dir: "/tmp/rustfs-nats-jetstream-test".to_string(),
        queue_limit: 0,
        jetstream_enable: Some(true),
        jetstream_stream_name: Some(stream_name.to_string()),
        jetstream_ack_timeout_secs: Some(30),
        target_type: TargetType::NotifyEvent,
    }
}

/// A pre-provisioned writable stream: it captures the subject, returns acks, accepts writes, and its
/// duplicate window exceeds the retry lifetime. The test harness creates it, never RustFS.
async fn provision_writable_stream(stream_name: &str, subject: &str) {
    let client = async_nats::connect(server_url()).await.expect("connect to NATS");
    let context = async_nats::jetstream::new(client);
    // Clear any leftover from a prior aborted run. Absent is fine, so the result is dropped.
    let _ = context.delete_stream(stream_name).await;
    context
        .create_stream(Config {
            name: stream_name.to_string(),
            subjects: vec![subject.to_string()],
            duplicate_window: Duration::from_secs(300),
            ..Default::default()
        })
        .await
        .expect("provision the stream");
}

async fn remove_stream(stream_name: &str) {
    let client = async_nats::connect(server_url()).await.expect("connect to NATS");
    let context = async_nats::jetstream::new(client);
    // Best-effort teardown. A failure to delete does not fail the test, so the result is dropped.
    let _ = context.delete_stream(stream_name).await;
}

#[tokio::test]
#[ignore = "requires a live NATS JetStream server (see module docs; RUSTFS_TEST_NATS_URL overrides)"]
async fn missing_stream_fails_the_health_check() {
    let stream_name = format!("RUSTFS_TEST_{}", Uuid::new_v4().simple());
    let args = jetstream_args("rustfs.events", &stream_name);
    // No stream is provisioned, so the lookup must fail.
    let err = check_nats_server_available(&args)
        .await
        .expect_err("a missing stream fails the health check");
    assert!(!err.to_string().is_empty(), "the failure carries a generic message");
}

#[tokio::test]
#[ignore = "requires a live NATS JetStream server (see module docs; RUSTFS_TEST_NATS_URL overrides)"]
async fn valid_stream_passes_the_health_check() {
    let stream_name = format!("RUSTFS_TEST_{}", Uuid::new_v4().simple());
    let subject = "rustfs.events";
    provision_writable_stream(&stream_name, subject).await;
    let args = jetstream_args(subject, &stream_name);
    let result = check_nats_server_available(&args).await;
    remove_stream(&stream_name).await;
    result.expect("a pre-provisioned writable stream passes the health check");
}

#[tokio::test]
#[ignore = "requires a live NATS JetStream server (see module docs; RUSTFS_TEST_NATS_URL overrides)"]
async fn stream_not_capturing_the_subject_fails_the_health_check() {
    let stream_name = format!("RUSTFS_TEST_{}", Uuid::new_v4().simple());
    // The stream binds a different subject than the target publishes to.
    provision_writable_stream(&stream_name, "some.other.subject").await;
    let args = jetstream_args("rustfs.events", &stream_name);
    let result = check_nats_server_available(&args).await;
    remove_stream(&stream_name).await;
    result.expect_err("a stream that does not capture the subject fails the health check");
}

#[tokio::test]
#[ignore = "requires a live NATS JetStream server (see module docs; RUSTFS_TEST_NATS_URL overrides)"]
async fn too_small_duplicate_window_fails_the_health_check() {
    let stream_name = format!("RUSTFS_TEST_{}", Uuid::new_v4().simple());
    let subject = "rustfs.events";
    let client = async_nats::connect(server_url()).await.expect("connect to NATS");
    let context = async_nats::jetstream::new(client);
    // Clear any leftover from a prior aborted run. Absent is fine, so the result is dropped.
    let _ = context.delete_stream(&stream_name).await;
    context
        .create_stream(Config {
            name: stream_name.clone(),
            subjects: vec![subject.to_string()],
            // Below the worst-case retry lifetime at the default ack timeout.
            duplicate_window: Duration::from_secs(60),
            ..Default::default()
        })
        .await
        .expect("provision the stream");
    let args = jetstream_args(subject, &stream_name);
    let result = check_nats_server_available(&args).await;
    remove_stream(&stream_name).await;
    result.expect_err("a too-small duplicate window fails the health check");
}
