// ==========================================================================
// Channel-Scoped Memory Privacy Tests
//
// These tests verify the privacy model: facts are scoped by channel,
// cross-channel hints work correctly, DMs access everything, and
// PublicExternal is fully locked down.
// ==========================================================================

/// Facts are NOT bulk-injected into the system prompt — accessed on demand via tools.
/// The system prompt should contain the memory capabilities summary instead.
#[tokio::test]
async fn test_same_channel_fact_recall() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    harness
        .state
        .upsert_fact(
            "project",
            "framework",
            "NextJS with TypeScript",
            "user",
            Some("slack:C_ABC"),
            crate::types::FactPrivacy::Channel,
        )
        .await
        .unwrap();

    harness
        .agent
        .handle_message(
            "same_ch_session",
            "what framework are we using?",
            None,
            UserRole::Owner,
            ChannelContext {
                visibility: ChannelVisibility::Public,
                platform: "slack".to_string(),
                channel_name: Some("#dev".to_string()),
                channel_id: Some("slack:C_ABC".to_string()),
                sender_name: None,
                sender_id: None,
                channel_member_names: vec![],
                user_id_map: std::collections::HashMap::new(),
                trusted: false,
            },
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let content_owned: String = call_log[0]
        .messages
        .iter()
        .filter(|m| m["role"] == "system")
        .filter_map(|m| m["content"].as_str())
        .collect::<Vec<_>>()
        .join("\n");
    let content = content_owned.as_str();
    // Facts are on-demand now, NOT injected into prompt
    assert!(
        !content.contains("NextJS"),
        "Facts should NOT be bulk-injected into system prompt"
    );
    assert!(
        content.contains("Your Memory"),
        "System prompt should contain memory capabilities summary"
    );
}

/// Store a channel-scoped fact, query from a DIFFERENT channel → fact NOT in prompt.
#[tokio::test]
async fn test_cross_channel_fact_blocked() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    // Store a channel-scoped fact in channel A
    harness
        .state
        .upsert_fact(
            "project",
            "secret_plan",
            "Launch product in March",
            "user",
            Some("slack:C_ALPHA"),
            crate::types::FactPrivacy::Channel,
        )
        .await
        .unwrap();

    // Query from a DIFFERENT channel B
    harness
        .agent
        .handle_message(
            "cross_ch_session",
            "what is our launch plan?",
            None,
            UserRole::Owner,
            ChannelContext {
                visibility: ChannelVisibility::Public,
                platform: "slack".to_string(),
                channel_name: Some("#general".to_string()),
                channel_id: Some("slack:C_BETA".to_string()),
                sender_name: None,
                sender_id: None,
                channel_member_names: vec![],
                user_id_map: std::collections::HashMap::new(),
                trusted: false,
            },
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let content_owned: String = call_log[0]
        .messages
        .iter()
        .filter(|m| m["role"] == "system")
        .filter_map(|m| m["content"].as_str())
        .collect::<Vec<_>>()
        .join("\n");
    let content = content_owned.as_str();

    // The fact should NOT be in the main facts section
    // (It may appear in cross-channel hints section, which is expected)
    let facts_section_end = content
        .find("Cross-Channel Context")
        .unwrap_or(content.len());
    let facts_section = &content[..facts_section_end];
    assert!(
        !facts_section.contains("Launch product in March"),
        "Channel-scoped fact from another channel should NOT appear in main facts"
    );
}

/// DM conversations: facts are accessed on demand via tools, not bulk-injected.
#[tokio::test]
async fn test_dm_recalls_all_facts() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    harness
        .agent
        .handle_message(
            "dm_all_facts",
            "what do you know about my guitar hobby",
            None,
            UserRole::Owner,
            ChannelContext::private("telegram"),
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let content_owned: String = call_log[0]
        .messages
        .iter()
        .filter(|m| m["role"] == "system")
        .filter_map(|m| m["content"].as_str())
        .collect::<Vec<_>>()
        .join("\n");
    let content = content_owned.as_str();

    // Facts are on-demand — system prompt has capabilities, not data
    assert!(
        content.contains("manage_memories"),
        "DM prompt should reference manage_memories for on-demand fact retrieval"
    );
}

/// Private facts should NEVER appear in any channel — not even hinted.
#[tokio::test]
async fn test_private_facts_never_in_channels() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    harness
        .state
        .upsert_fact(
            "preference",
            "travel",
            "Prefers window seats on flights",
            "user",
            None,
            crate::types::FactPrivacy::Private,
        )
        .await
        .unwrap();

    // Query from a public channel
    harness
        .agent
        .handle_message(
            "priv_fact_session",
            "what health info do you have about me?",
            None,
            UserRole::Owner,
            ChannelContext {
                visibility: ChannelVisibility::Public,
                platform: "slack".to_string(),
                channel_name: Some("#general".to_string()),
                channel_id: Some("slack:C_GEN".to_string()),
                sender_name: None,
                sender_id: None,
                channel_member_names: vec![],
                user_id_map: std::collections::HashMap::new(),
                trusted: false,
            },
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let content_owned: String = call_log[0]
        .messages
        .iter()
        .filter(|m| m["role"] == "system")
        .filter_map(|m| m["content"].as_str())
        .collect::<Vec<_>>()
        .join("\n");
    let content = content_owned.as_str();

    assert!(
        !content.contains("window seats") && !content.contains("Prefers window"),
        "Private facts must NEVER appear in public channel prompts"
    );
}

/// Cross-channel hints: channel-scoped fact from another channel should
/// appear in the hints section (not the main facts section).
#[tokio::test]
async fn test_cross_channel_hints() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    // Store a channel-scoped fact in channel A
    harness
        .state
        .upsert_fact(
            "project",
            "framework",
            "Uses React for frontend",
            "user",
            Some("slack:C_DEV"),
            crate::types::FactPrivacy::Channel,
        )
        .await
        .unwrap();

    // Query from a DIFFERENT public channel B about something related
    harness
        .agent
        .handle_message(
            "hints_session",
            "what frontend framework should we use?",
            None,
            UserRole::Owner,
            ChannelContext {
                visibility: ChannelVisibility::Public,
                platform: "slack".to_string(),
                channel_name: Some("#general".to_string()),
                channel_id: Some("slack:C_GEN".to_string()),
                sender_name: None,
                sender_id: None,
                channel_member_names: vec![],
                user_id_map: std::collections::HashMap::new(),
                trusted: false,
            },
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let content_owned: String = call_log[0]
        .messages
        .iter()
        .filter(|m| m["role"] == "system")
        .filter_map(|m| m["content"].as_str())
        .collect::<Vec<_>>()
        .join("\n");
    let content = content_owned.as_str();

    // The cross-channel hint section may or may not appear depending on embedding similarity.
    // What matters is that the fact does NOT appear in the main facts section as a normal fact.
    // If it does appear, it should only be in the cross-channel hints section.
    if content.contains("React") {
        assert!(
            content.contains("Cross-Channel Context"),
            "If React appears, it should be in cross-channel hints section, not main facts"
        );
    }
}

/// Legacy facts (NULL channel_id) should be accessible everywhere — backward compat.
#[tokio::test]
async fn test_legacy_facts_backward_compat() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    // Store a legacy fact (no channel_id, Global privacy — the default for old facts)
    harness
        .state
        .upsert_fact(
            "preference",
            "editor",
            "Uses Neovim",
            "agent",
            None,
            crate::types::FactPrivacy::Global,
        )
        .await
        .unwrap();

    // Query from a public channel
    harness
        .agent
        .handle_message(
            "legacy_session",
            "what editor do I use?",
            None,
            UserRole::Owner,
            ChannelContext {
                visibility: ChannelVisibility::Public,
                platform: "slack".to_string(),
                channel_name: Some("#dev".to_string()),
                channel_id: Some("slack:C_DEV".to_string()),
                sender_name: None,
                sender_id: None,
                channel_member_names: vec![],
                user_id_map: std::collections::HashMap::new(),
                trusted: false,
            },
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let content_owned: String = call_log[0]
        .messages
        .iter()
        .filter(|m| m["role"] == "system")
        .filter_map(|m| m["content"].as_str())
        .collect::<Vec<_>>()
        .join("\n");
    let content = content_owned.as_str();
    // Facts are on-demand now — not bulk-injected into system prompt
    assert!(
        !content.contains("Neovim"),
        "Facts should NOT be bulk-injected into system prompt"
    );
    assert!(
        content.contains("Your Memory"),
        "System prompt should contain memory capabilities summary"
    );
}

/// Privacy upgrade: after update_fact_privacy, a channel-scoped fact becomes globally accessible.
#[tokio::test]
async fn test_privacy_upgrade() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    // Store a channel-scoped fact
    harness
        .state
        .upsert_fact(
            "project",
            "deadline",
            "March 15th launch",
            "user",
            Some("slack:C_PROJ"),
            crate::types::FactPrivacy::Channel,
        )
        .await
        .unwrap();

    // Verify it's not accessible from another channel
    let facts_before = harness
        .state
        .get_relevant_facts_for_channel(
            "launch deadline",
            10,
            Some("slack:C_OTHER"),
            ChannelVisibility::Public,
        )
        .await
        .unwrap();
    let _has_deadline_before = facts_before.iter().any(|f| f.value.contains("March 15th"));

    // Upgrade the fact to Global
    let all_facts = harness.state.get_all_facts_with_provenance().await.unwrap();
    let deadline_fact = all_facts.iter().find(|f| f.key == "deadline").unwrap();
    harness
        .state
        .update_fact_privacy(deadline_fact.id, crate::types::FactPrivacy::Global)
        .await
        .unwrap();

    // Now it should be accessible from the other channel
    let facts_after = harness
        .state
        .get_relevant_facts_for_channel(
            "launch deadline",
            10,
            Some("slack:C_OTHER"),
            ChannelVisibility::Public,
        )
        .await
        .unwrap();
    let _has_deadline_after = facts_after.iter().any(|f| f.value.contains("March 15th"));

    // After upgrade, the fact should be findable (before it may or may not due to embedding similarity)
    // At minimum, verify the privacy was actually updated
    let updated_facts = harness.state.get_all_facts_with_provenance().await.unwrap();
    let updated = updated_facts.iter().find(|f| f.key == "deadline").unwrap();
    assert_eq!(
        updated.privacy,
        crate::types::FactPrivacy::Global,
        "Fact privacy should be upgraded to Global"
    );
}

/// ManageMemoriesTool (list action) should return facts with provenance info.
#[tokio::test]
async fn test_memory_management_list() {
    let harness = setup_test_agent(MockProvider::new()).await.unwrap();

    harness
        .state
        .upsert_fact(
            "project",
            "lang",
            "Rust",
            "user",
            Some("slack:C_DEV"),
            crate::types::FactPrivacy::Channel,
        )
        .await
        .unwrap();
    harness
        .state
        .upsert_fact(
            "personal",
            "name",
            "Alice",
            "user",
            None,
            crate::types::FactPrivacy::Global,
        )
        .await
        .unwrap();

    let all_facts = harness.state.get_all_facts_with_provenance().await.unwrap();
    assert!(all_facts.len() >= 2, "Should have at least 2 facts");

    // Verify facts have the expected provenance
    let lang_fact = all_facts.iter().find(|f| f.key == "lang").unwrap();
    assert_eq!(lang_fact.channel_id.as_deref(), Some("slack:C_DEV"));
    assert_eq!(lang_fact.privacy, crate::types::FactPrivacy::Channel);

    let name_fact = all_facts.iter().find(|f| f.key == "name").unwrap();
    assert_eq!(name_fact.channel_id, None);
    assert_eq!(name_fact.privacy, crate::types::FactPrivacy::Global);
}

