//! Integration tests for [`LakeFSBackend`] against a real provisioned lakeFS instance.
//!
//! Every test is marked `#[ignore]` and is skipped on normal PR runs. Run them
//! explicitly against your provisioned instance with:
//!
//! ```text
//! cargo test -p briefcase-core --no-default-features \
//!   --features "async,storage,networking" \
//!   integration_tests -- --ignored --test-threads=4
//! ```
//!
//! # Required environment variables
//!
//! All four must be set, or every test returns early (no failure):
//!
//! | Variable                      | Example                          |
//! |-------------------------------|----------------------------------|
//! | `BRIEFCASE_LAKEFS_ENDPOINT`   | `https://your-org.lakefs.cloud`  |
//! | `BRIEFCASE_LAKEFS_REPOSITORY` | `briefcase-prod`                 |
//! | `BRIEFCASE_LAKEFS_ACCESS_KEY` | `AKIAIOSFODNN7EXAMPLE`           |
//! | `BRIEFCASE_LAKEFS_SECRET_KEY` | `wJalrXUtnFEMI/K7MDENG/...`      |
//!
//! Optional:
//!
//! | Variable                  | Default |
//! |---------------------------|---------|
//! | `BRIEFCASE_LAKEFS_BRANCH` | `main`  |
//!
//! The branch in `BRIEFCASE_LAKEFS_BRANCH` is the *source* branch that each
//! test forks from. It must already exist and have at least one commit.
//!
//! # Test isolation
//!
//! Each test creates a uniquely named branch (`integration-test-<uuid>`),
//! runs entirely against that branch, then deletes it. Cleanup runs via an
//! explicit pattern — the async test body returns a `Result`, cleanup fires
//! unconditionally, and only then is the result unwrapped. This ensures the
//! branch is deleted even when an assertion fails.

use super::super::{SnapshotQuery, StorageBackend, StorageError};
use super::{LakeFSBackend, LakeFSConfig};
use crate::models::{DecisionSnapshot, Input, ModelParameters, Output, Snapshot, SnapshotType};
use chrono::{DateTime, Duration, Utc};
use serde_json::json;
use uuid::Uuid;

// ── Config & isolation helpers ────────────────────────────────────────────────

/// Load connection config from environment variables.
/// Returns `None` (causing a silent skip) if any required variable is absent.
fn integration_config() -> Option<LakeFSConfig> {
    let endpoint = std::env::var("BRIEFCASE_LAKEFS_ENDPOINT").ok()?;
    let repository = std::env::var("BRIEFCASE_LAKEFS_REPOSITORY").ok()?;
    let access_key = std::env::var("BRIEFCASE_LAKEFS_ACCESS_KEY").ok()?;
    let secret_key = std::env::var("BRIEFCASE_LAKEFS_SECRET_KEY").ok()?;
    let branch = std::env::var("BRIEFCASE_LAKEFS_BRANCH").unwrap_or_else(|_| "main".to_string());
    Some(LakeFSConfig::new(
        endpoint, repository, branch, access_key, secret_key,
    ))
}

/// Generate a unique branch name for one test run.
fn test_branch() -> String {
    format!("integration-test-{}", Uuid::new_v4())
}

/// Create a backend pointed at the source branch (for branch management) and a
/// second backend pointed at a freshly created test branch.
///
/// The caller is responsible for deleting the test branch via
/// `main_backend.delete_branch(&branch).await.ok()` before asserting results.
async fn setup_backends(
    config: &LakeFSConfig,
    branch: &str,
) -> Result<(LakeFSBackend, LakeFSBackend), StorageError> {
    let main_backend = LakeFSBackend::new(config.clone())?;
    main_backend.create_branch(branch, &config.branch).await?;

    // Reuse all fields from the base config, only override branch.
    let test_config = LakeFSConfig {
        branch: branch.to_string(),
        ..config.clone()
    };
    let test_backend = LakeFSBackend::new(test_config)?;

    Ok((main_backend, test_backend))
}

// ── Test data helpers ─────────────────────────────────────────────────────────

fn make_decision(fn_name: &str, model: &str) -> DecisionSnapshot {
    DecisionSnapshot::new(fn_name)
        .with_module("integration_test")
        .add_input(Input::new("query", json!("test input"), "string"))
        .add_output(Output::new("result", json!("test output"), "string"))
        .with_model_parameters(ModelParameters::new(model))
        .with_execution_time(42.0)
        .add_tag("test", "true")
}

fn make_snapshot(decisions: Vec<DecisionSnapshot>) -> Snapshot {
    let mut snap = Snapshot::new(SnapshotType::Decision);
    for d in decisions {
        snap.add_decision(d);
    }
    snap
}

fn make_snapshot_at(decisions: Vec<DecisionSnapshot>, t: DateTime<Utc>) -> Snapshot {
    let mut snap = make_snapshot(decisions);
    snap.metadata.timestamp = t;
    snap
}

// ── 1. Bootstrap / connectivity ───────────────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_health_check_live() {
    let Some(config) = integration_config() else {
        return;
    };
    let backend = LakeFSBackend::new(config).unwrap();
    assert!(
        backend.health_check().await.unwrap(),
        "health_check returned false — is the instance reachable and the repo configured?"
    );
}

// ── 2. save + flush (batch writes) ───────────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_save_batches_before_flush() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let snap = make_snapshot(vec![make_decision("classify", "gpt-4")]);
        let id = backend.save(&snap).await?;

        // Before flush: the object has not been written to lakeFS yet.
        let pre_flush = backend.load(&id).await;
        assert!(
            matches!(pre_flush, Err(StorageError::NotFound(_))),
            "expected NotFound before flush, got: {:?}",
            pre_flush
        );

        backend.flush().await?;

        // After flush: must be retrievable with the same snapshot_id.
        let loaded = backend.load(&id).await?;
        assert_eq!(
            loaded.metadata.snapshot_id, snap.metadata.snapshot_id,
            "loaded snapshot_id does not match the saved one"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_flush_returns_accurate_metadata() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        backend
            .save(&make_snapshot(vec![make_decision("fn1", "model-a")]))
            .await?;
        backend
            .save(&make_snapshot(vec![make_decision("fn2", "model-b")]))
            .await?;

        let flush = backend.flush().await?;

        assert_eq!(flush.snapshots_written, 2, "expected 2 snapshots written");
        assert!(
            flush.checkpoint_id.is_some(),
            "expected a non-None checkpoint_id (lakeFS commit ID)"
        );
        assert!(flush.bytes_written > 0, "expected bytes_written > 0");

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_flush_empty_noop() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let flush = backend.flush().await?;

        assert_eq!(flush.snapshots_written, 0);
        assert_eq!(flush.bytes_written, 0);
        assert!(
            flush.checkpoint_id.is_none(),
            "empty flush must not create a lakeFS commit"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_multiple_flushes_create_separate_commits() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        backend
            .save(&make_snapshot(vec![make_decision("fn1", "m")]))
            .await?;
        let r1 = backend.flush().await?;

        backend
            .save(&make_snapshot(vec![make_decision("fn2", "m")]))
            .await?;
        let r2 = backend.flush().await?;

        let id1 = r1.checkpoint_id.unwrap();
        let id2 = r2.checkpoint_id.unwrap();
        assert_ne!(
            id1, id2,
            "each flush must produce a distinct lakeFS commit ID"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

// ── 3. save_decision (immediate writes) ──────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_save_decision_immediate_and_load() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let decision = make_decision("classify", "gpt-4");
        let id = backend.save_decision(&decision).await?;

        // save_decision bypasses the batch — must be readable without a flush.
        let loaded = backend.load_decision(&id).await?;
        assert_eq!(
            loaded.function_name, "classify",
            "function_name mismatch after save_decision"
        );
        assert_eq!(
            loaded.model_parameters.unwrap().model_name,
            "gpt-4",
            "model_name mismatch after save_decision"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

// ── 4. query filtering ────────────────────────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_query_no_filters_returns_all() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        for i in 0..3 {
            backend
                .save(&make_snapshot(vec![make_decision(
                    &format!("fn{}", i),
                    "model",
                )]))
                .await?;
        }
        backend.flush().await?;

        let results = backend.query(SnapshotQuery::new()).await?;
        assert_eq!(results.len(), 3, "expected all 3 snapshots with no filter");

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_query_function_filter() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        backend
            .save(&make_snapshot(vec![make_decision("classify", "m")]))
            .await?;
        backend
            .save(&make_snapshot(vec![make_decision("summarize", "m")]))
            .await?;
        backend.flush().await?;

        let results = backend
            .query(SnapshotQuery::new().with_function_name("classify"))
            .await?;

        assert_eq!(results.len(), 1, "expected exactly 1 match for classify");
        assert_eq!(
            results[0].decisions[0].function_name, "classify",
            "wrong snapshot returned by function_name filter"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_query_model_filter() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        backend
            .save(&make_snapshot(vec![make_decision("fn1", "gpt-4")]))
            .await?;
        backend
            .save(&make_snapshot(vec![make_decision("fn2", "claude-3")]))
            .await?;
        backend.flush().await?;

        let results = backend
            .query(SnapshotQuery::new().with_model_name("gpt-4"))
            .await?;

        assert_eq!(results.len(), 1, "expected exactly 1 match for gpt-4");
        assert_eq!(
            results[0].decisions[0]
                .model_parameters
                .as_ref()
                .unwrap()
                .model_name,
            "gpt-4",
            "wrong model returned by model_name filter"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_query_module_filter() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let billing = DecisionSnapshot::new("process_payment")
            .with_module("billing")
            .with_model_parameters(ModelParameters::new("m"));
        let auth = DecisionSnapshot::new("verify_token")
            .with_module("auth")
            .with_model_parameters(ModelParameters::new("m"));

        backend.save(&make_snapshot(vec![billing])).await?;
        backend.save(&make_snapshot(vec![auth])).await?;
        backend.flush().await?;

        let results = backend
            .query(SnapshotQuery::new().with_module_name("billing"))
            .await?;

        assert_eq!(
            results.len(),
            1,
            "expected exactly 1 match for billing module"
        );
        assert_eq!(
            results[0].decisions[0].module_name.as_deref(),
            Some("billing"),
            "wrong module returned by module_name filter"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_query_tag_filter() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let prod = DecisionSnapshot::new("infer")
            .with_model_parameters(ModelParameters::new("m"))
            .add_tag("env", "prod");
        let staging = DecisionSnapshot::new("infer")
            .with_model_parameters(ModelParameters::new("m"))
            .add_tag("env", "staging");

        backend.save(&make_snapshot(vec![prod])).await?;
        backend.save(&make_snapshot(vec![staging])).await?;
        backend.flush().await?;

        let results = backend
            .query(SnapshotQuery::new().with_tag("env", "prod"))
            .await?;

        assert_eq!(results.len(), 1, "expected exactly 1 match for env=prod");
        assert_eq!(
            results[0].decisions[0].tags.get("env").map(String::as_str),
            Some("prod"),
            "wrong tag value returned by tag filter"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_query_combined_filters() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        // Target: classify + gpt-4 + env=prod — should be the only match.
        let target = DecisionSnapshot::new("classify")
            .with_model_parameters(ModelParameters::new("gpt-4"))
            .add_tag("env", "prod");
        // Same fn + tag, wrong model.
        let wrong_model = DecisionSnapshot::new("classify")
            .with_model_parameters(ModelParameters::new("claude-3"))
            .add_tag("env", "prod");
        // Same model + tag, wrong function.
        let wrong_fn = DecisionSnapshot::new("summarize")
            .with_model_parameters(ModelParameters::new("gpt-4"))
            .add_tag("env", "prod");

        backend.save(&make_snapshot(vec![target])).await?;
        backend.save(&make_snapshot(vec![wrong_model])).await?;
        backend.save(&make_snapshot(vec![wrong_fn])).await?;
        backend.flush().await?;

        let results = backend
            .query(
                SnapshotQuery::new()
                    .with_function_name("classify")
                    .with_model_name("gpt-4")
                    .with_tag("env", "prod"),
            )
            .await?;

        assert_eq!(
            results.len(),
            1,
            "combined filter (function + model + tag) must return exactly 1 match"
        );
        assert_eq!(results[0].decisions[0].function_name, "classify");
        assert_eq!(
            results[0].decisions[0]
                .model_parameters
                .as_ref()
                .unwrap()
                .model_name,
            "gpt-4"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_query_time_range() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let t_old = Utc::now() - Duration::hours(3);
        let t_mid = Utc::now() - Duration::hours(1);
        let t_new = Utc::now();

        backend
            .save(&make_snapshot_at(vec![make_decision("old_fn", "m")], t_old))
            .await?;
        backend
            .save(&make_snapshot_at(vec![make_decision("mid_fn", "m")], t_mid))
            .await?;
        backend
            .save(&make_snapshot_at(vec![make_decision("new_fn", "m")], t_new))
            .await?;
        backend.flush().await?;

        // Window covers t_mid and t_new but not t_old (3 hours ago).
        let results = backend
            .query(SnapshotQuery::new().with_time_range(
                Utc::now() - Duration::hours(2),
                Utc::now() + Duration::minutes(1),
            ))
            .await?;

        assert_eq!(
            results.len(),
            2,
            "expected 2 snapshots within last 2 hours, got {}",
            results.len()
        );
        for r in &results {
            assert_ne!(
                r.decisions[0].function_name, "old_fn",
                "snapshot from t-3h must not appear in a 2-hour window"
            );
        }

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_query_pagination() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        for i in 0..5 {
            backend
                .save(&make_snapshot(vec![make_decision(
                    &format!("fn{}", i),
                    "model",
                )]))
                .await?;
        }
        backend.flush().await?;

        // First page: limit=2 → 2 results.
        let page1 = backend.query(SnapshotQuery::new().with_limit(2)).await?;
        assert_eq!(page1.len(), 2, "limit=2 must return exactly 2 results");

        // Offset into middle: offset=3, limit=2 → items 4 and 5 → 2 results.
        let page2 = backend
            .query(SnapshotQuery::new().with_offset(3).with_limit(2))
            .await?;
        assert_eq!(
            page2.len(),
            2,
            "offset=3 + limit=2 on 5 total must return 2 results"
        );

        // Offset past the end → empty.
        let past_end = backend
            .query(SnapshotQuery::new().with_offset(100).with_limit(5))
            .await?;
        assert_eq!(
            past_end.len(),
            0,
            "offset beyond total must return an empty result set"
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

// ── 5. delete ─────────────────────────────────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_delete_existing() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let snap = make_snapshot(vec![make_decision("fn1", "m")]);
        let id = snap.metadata.snapshot_id.to_string();

        backend.save(&snap).await?;
        backend.flush().await?;

        // Confirm existence.
        backend.load(&id).await?;

        let deleted = backend.delete(&id).await?;
        assert!(deleted, "delete must return true for an existing snapshot");

        // Object must be gone.
        let after = backend.load(&id).await;
        assert!(
            matches!(after, Err(StorageError::NotFound(_))),
            "expected NotFound after delete, got: {:?}",
            after
        );

        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_delete_nonexistent_returns_false() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let fake_id = Uuid::new_v4().to_string();
        let deleted = backend.delete(&fake_id).await?;
        assert!(
            !deleted,
            "delete of a nonexistent ID must return false, not an error"
        );
        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

// ── 6. error cases ────────────────────────────────────────────────────────────

#[tokio::test]
#[ignore]
async fn test_load_nonexistent_returns_not_found() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let fake_id = Uuid::new_v4().to_string();
        let load_result = backend.load(&fake_id).await;
        assert!(
            matches!(load_result, Err(StorageError::NotFound(_))),
            "load of nonexistent snapshot_id must return NotFound, got: {:?}",
            load_result
        );
        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}

#[tokio::test]
#[ignore]
async fn test_load_decision_nonexistent_returns_not_found() {
    let Some(config) = integration_config() else {
        return;
    };
    let branch = test_branch();
    let (main_backend, backend) = setup_backends(&config, &branch).await.unwrap();

    let result: Result<(), StorageError> = async {
        let fake_id = Uuid::new_v4().to_string();
        let load_result = backend.load_decision(&fake_id).await;
        assert!(
            matches!(load_result, Err(StorageError::NotFound(_))),
            "load_decision of nonexistent decision_id must return NotFound, got: {:?}",
            load_result
        );
        Ok(())
    }
    .await;

    main_backend.delete_branch(&branch).await.ok();
    result.unwrap();
}
