//! Live Qdrant round-trip via testcontainers.
//!
//! Uses `GenericImage` with the official `qdrant/qdrant` image. The
//! `testcontainers-modules` crate has no `qdrant` feature in the
//! version pinned by this workspace (`0.11`), so we configure the
//! image inline.

use klieo_core::memory::{Fact, Scope};
use klieo_memory_qdrant::{FakeEmbedder, MemoryQdrant, QdrantConfig};
use std::sync::Arc;
use testcontainers::{
    core::{IntoContainerPort, WaitFor},
    runners::AsyncRunner,
    ContainerAsync, GenericImage, ImageExt,
};

const QDRANT_GRPC_PORT: u16 = 6334;
const QDRANT_HTTP_PORT: u16 = 6333;

/// Start qdrant + return a `MemoryQdrant` whose `collection_prefix` is
/// a fresh UUID — guarantees the underlying collection has never been
/// created. Used by the missing-collection tests below.
async fn start_qdrant_with_missing_prefix() -> (ContainerAsync<GenericImage>, MemoryQdrant) {
    let (container, cfg) = start_qdrant().await;
    let cfg =
        cfg.with_collection_prefix(format!("klieo_missing_{}", uuid::Uuid::new_v4().simple()));
    let mem = MemoryQdrant::new(cfg, Arc::new(FakeEmbedder::new(64)))
        .await
        .expect("connect");
    (container, mem)
}

async fn start_qdrant() -> (ContainerAsync<GenericImage>, QdrantConfig) {
    let container = GenericImage::new("qdrant/qdrant", "v1.12.4")
        .with_exposed_port(QDRANT_GRPC_PORT.tcp())
        .with_exposed_port(QDRANT_HTTP_PORT.tcp())
        .with_wait_for(WaitFor::message_on_stdout("Qdrant gRPC listening on"))
        .with_env_var("QDRANT__LOG_LEVEL", "INFO")
        .start()
        .await
        .expect("failed to start qdrant container");
    let port = container
        .get_host_port_ipv4(QDRANT_GRPC_PORT)
        .await
        .expect("failed to read grpc port");
    let cfg = QdrantConfig::new(format!("http://127.0.0.1:{port}"));
    (container, cfg)
}

#[tokio::test]
#[ignore = "requires docker; run with --ignored"]
async fn remember_and_recall_round_trip() {
    let (_c, cfg) = start_qdrant().await;
    let mem = MemoryQdrant::new(cfg, Arc::new(FakeEmbedder::new(64)))
        .await
        .expect("connect");
    let scope = Scope::Workspace("ws-1".into());

    mem.long_term
        .remember(
            scope.clone(),
            Fact {
                text: "the cat sat on the mat".into(),
                metadata: serde_json::Value::Null,
            },
        )
        .await
        .expect("remember 1");
    mem.long_term
        .remember(
            scope.clone(),
            Fact {
                text: "rust is a systems language".into(),
                metadata: serde_json::Value::Null,
            },
        )
        .await
        .expect("remember 2");

    let hits = mem
        .long_term
        .recall(scope, "the cat sat on the mat", 5)
        .await
        .expect("recall");
    assert!(!hits.is_empty(), "recall returned 0 hits");
    assert_eq!(hits[0].text, "the cat sat on the mat");
}

#[tokio::test]
#[ignore = "requires docker; run with --ignored"]
async fn scope_isolation_works() {
    let (_c, cfg) = start_qdrant().await;
    let mem = MemoryQdrant::new(cfg, Arc::new(FakeEmbedder::new(64)))
        .await
        .expect("connect");
    let ws_a = Scope::Workspace("a".into());
    let ws_b = Scope::Workspace("b".into());

    mem.long_term
        .remember(
            ws_a.clone(),
            Fact {
                text: "secret-A".into(),
                metadata: serde_json::Value::Null,
            },
        )
        .await
        .unwrap();
    mem.long_term
        .remember(
            ws_b.clone(),
            Fact {
                text: "secret-B".into(),
                metadata: serde_json::Value::Null,
            },
        )
        .await
        .unwrap();

    let from_a = mem.long_term.recall(ws_a, "secret-A", 5).await.unwrap();
    assert!(from_a.iter().any(|f| f.text == "secret-A"));
    assert!(!from_a.iter().any(|f| f.text == "secret-B"));
}

#[tokio::test]
#[ignore = "requires docker; run with --ignored"]
async fn forget_removes_fact() {
    let (_c, cfg) = start_qdrant().await;
    let mem = MemoryQdrant::new(cfg, Arc::new(FakeEmbedder::new(64)))
        .await
        .expect("connect");
    let scope = Scope::Workspace("forget-test".into());

    let id = mem
        .long_term
        .remember(
            scope.clone(),
            Fact {
                text: "to be forgotten".into(),
                metadata: serde_json::Value::Null,
            },
        )
        .await
        .unwrap();

    let before = mem
        .long_term
        .recall(scope.clone(), "to be forgotten", 5)
        .await
        .unwrap();
    assert!(before.iter().any(|f| f.text == "to be forgotten"));

    mem.long_term.forget(id).await.expect("forget");

    let after = mem
        .long_term
        .recall(scope, "to be forgotten", 5)
        .await
        .unwrap();
    assert!(!after.iter().any(|f| f.text == "to be forgotten"));
}

/// Recalling against a collection that was never bootstrapped (i.e.
/// `remember()` has never run for this `QdrantConfig`) must return an
/// empty `Vec` rather than propagating a Qdrant NotFound error. This
/// is the live counterpart to the unit-level `is_missing_collection`
/// tests in `long_term.rs` and guards against regressions if Qdrant
/// changes its error-message wording.
#[tokio::test]
#[ignore = "requires docker; run with --ignored"]
async fn recall_returns_empty_on_missing_collection() {
    let (_c, mem) = start_qdrant_with_missing_prefix().await;
    let hits = mem
        .long_term
        .recall(Scope::Global, "anything", 10)
        .await
        .expect("recall against missing collection must succeed");
    assert!(
        hits.is_empty(),
        "expected empty Vec on missing collection, got {} hits",
        hits.len()
    );
}

/// Forgetting a `FactId` against a never-bootstrapped collection must
/// return `Ok(())` (idempotent no-op), not an error. Matches the
/// pre-pool behaviour where `collection_exists` was prechecked; the
/// pool refactor relies on `is_missing_collection` mapping the natural
/// `delete_points` error.
#[tokio::test]
#[ignore = "requires docker; run with --ignored"]
async fn forget_is_noop_on_missing_collection() {
    use klieo_core::ids::FactId;

    let (_c, mem) = start_qdrant_with_missing_prefix().await;
    let fake_id = FactId::new(uuid::Uuid::new_v4().to_string());
    mem.long_term
        .forget(fake_id)
        .await
        .expect("forget on missing collection must be Ok(())");
}

/// W2.A9: `QdrantConfig::with_api_key` accepts `impl Into<SecretString>`
/// and the secret is only exposed at the gRPC builder boundary inside
/// `QdrantHandle::connect`. This test exercises the `with_api_key`
/// code path end-to-end against a live Qdrant container.
///
/// The bundled `qdrant/qdrant` image does not require auth, so the
/// supplied key is accepted-but-ignored server-side; what matters here
/// is that the client-side `SecretString` wrapping, the
/// `expose_secret()` call site, and the builder hand-off all wire up
/// correctly and the remember/recall round-trip succeeds with a key
/// configured. A regression that broke the `expose_secret` call
/// (e.g. swallowed the key, mutated the request to fail auth) would
/// surface as a transport error here.
#[tokio::test]
#[ignore = "requires docker; run with --ignored"]
async fn remember_and_recall_with_api_key_configured() {
    let (_c, cfg) = start_qdrant().await;
    // `String: Into<SecretString>` via the `secrecy` crate — no new
    // dev-dep needed. Verifies the `impl Into<SecretString>` bound on
    // `with_api_key` accepts a plain owned string at the call site.
    let cfg_with_key = cfg.with_api_key("test-api-key-not-required-by-image".to_string());
    let mem = MemoryQdrant::new(cfg_with_key, Arc::new(FakeEmbedder::new(64)))
        .await
        .expect("connect with api_key");
    let scope = Scope::Workspace("ws-secret".into());

    mem.long_term
        .remember(
            scope.clone(),
            Fact {
                text: "fact-via-secret-string-path".into(),
                metadata: serde_json::Value::Null,
            },
        )
        .await
        .expect("remember with api_key");

    let hits = mem
        .long_term
        .recall(scope, "fact-via-secret-string-path", 5)
        .await
        .expect("recall with api_key");
    assert!(
        hits.iter().any(|f| f.text == "fact-via-secret-string-path"),
        "round-trip must succeed when api_key is configured via SecretString",
    );
}
