//! Integration test: Matrix migration onto StreamRegistry (Phase 4, Task 18).
//!
//! Scope:
//! - `MatrixFactory::kind()` reports `"matrix"`.
//! - A fresh `StreamFactoryRegistry` with the factory registered can look it
//!   up and rejects unknown kinds.
//! - Building a Matrix stream from a config with an `archive_wide` scope is
//!   rejected with `StreamBuildError::Config`.
//! - Building a Matrix stream against a not-logged-in service is rejected
//!   with `StreamBuildError::Transport` (does not panic, does not spin up
//!   a real Matrix connection).
//!
//! This does NOT exercise end-to-end Matrix delivery — that requires a
//! live homeserver or a stub client, which is outside Task 18's scope.
//! The share-side `SessionStream` impl is covered separately by
//! `dirigent_matrix` unit tests.

#![cfg(feature = "server")]

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

use uuid::Uuid;

use dirigent_auth::{Account, AccountKind, AccountProfile, SecretSource};
use dirigent_core::sharing::{
    MatrixFactory, StreamBuildError, StreamConfig, StreamFactory, StreamFactoryRegistry,
};
use dirigent_matrix::{MatrixBehaviorConfig, MatrixService};
use dirigent_protocol::streaming::StreamScope;

// ─── Helpers ────────────────────────────────────────────────────────────────

fn sample_matrix_account() -> Account {
    let mut credentials = HashMap::new();
    credentials.insert(
        "password".to_string(),
        SecretSource::Inline {
            value: "bot-pass".to_string(),
        },
    );

    let mut properties = HashMap::new();
    properties.insert(
        "homeserver".to_string(),
        serde_json::json!("https://matrix.example.com"),
    );
    properties.insert(
        "device_id".to_string(),
        serde_json::json!("DIRIGENT_TEST"),
    );

    Account {
        kind: AccountKind::Matrix,
        config_name: "matrix-test".to_string(),
        user_id: None,
        credentials,
        profile: AccountProfile {
            username: Some("bot".to_string()),
            display_name: Some("Test Bot".to_string()),
            ..Default::default()
        },
        properties,
    }
}

fn behavior() -> MatrixBehaviorConfig {
    MatrixBehaviorConfig {
        account: "matrix-test".to_string(),
        mode: Default::default(),
        default_invite: vec![],
        store_path: "matrix/test/store".to_string(),
        rooms: vec![],
    }
}

/// Build a `MatrixService` without calling `login()`. Any code path that
/// needs a live Client will surface a clean error (not a panic).
fn not_logged_in_service() -> Arc<MatrixService> {
    let account = sample_matrix_account();
    let tmp = tempfile::tempdir().expect("tempdir");
    let data_dir: PathBuf = tmp.path().to_path_buf();
    // Leak the TempDir so the path survives the life of the service for
    // the duration of the test. The sqlite store is only created when
    // login() runs — we never call it in these tests.
    std::mem::forget(tmp);
    let service = MatrixService::from_account(&account, behavior(), data_dir)
        .expect("from_account");
    Arc::new(service)
}

// ─── Tests ──────────────────────────────────────────────────────────────────

#[test]
fn matrix_factory_kind_is_matrix() {
    let service = not_logged_in_service();
    let f = MatrixFactory::new(service);
    assert_eq!(f.kind(), "matrix");
}

#[test]
fn registry_returns_registered_matrix_factory() {
    let service = not_logged_in_service();
    let reg = StreamFactoryRegistry::new().register(MatrixFactory::new(service));
    assert!(reg.get("matrix").is_some(), "matrix factory should be found");
    assert!(
        reg.get("langfuse").is_none(),
        "unregistered kinds must return None"
    );
}

#[tokio::test]
async fn build_rejects_archive_wide_scope_with_config_error() {
    let service = not_logged_in_service();
    let factory = MatrixFactory::new(service);

    let params_toml = r#"
connector_id = "opencode-1"
session_id = "native-abc"
room_id = "!room:example.com"
"#;
    let params: toml::Value = toml::from_str(params_toml).unwrap();

    let cfg = StreamConfig {
        name: "matrix-wrong-scope".to_string(),
        kind: "matrix".to_string(),
        scope: StreamScope::ArchiveWide { acknowledged: false },
        enabled: true,
        params,
    };

    let err = factory.build(&cfg).await.err().expect("build should fail");
    match err {
        StreamBuildError::Config(msg) => {
            assert!(
                msg.contains("session"),
                "expected 'session' hint in error, got: {msg}"
            );
        }
        other => panic!("expected Config error, got {other:?}"),
    }
}

#[tokio::test]
async fn build_rejects_missing_params_with_config_error() {
    let service = not_logged_in_service();
    let factory = MatrixFactory::new(service);

    // Missing room_id — required field.
    let params_toml = r#"
connector_id = "opencode-1"
session_id = "native-abc"
"#;
    let params: toml::Value = toml::from_str(params_toml).unwrap();

    let cfg = StreamConfig {
        name: "matrix-missing-room".to_string(),
        kind: "matrix".to_string(),
        scope: StreamScope::Session {
            scroll_id: Uuid::now_v7(),
        },
        enabled: true,
        params,
    };

    let err = factory.build(&cfg).await.err().expect("build should fail");
    assert!(
        matches!(err, StreamBuildError::Config(_)),
        "expected Config error, got {err:?}"
    );
}

#[tokio::test]
async fn build_reports_transport_error_when_service_not_logged_in() {
    let service = not_logged_in_service();
    let factory = MatrixFactory::new(service);

    let params_toml = r#"
connector_id = "opencode-1"
session_id = "native-abc"
room_id = "!room:example.com"
"#;
    let params: toml::Value = toml::from_str(params_toml).unwrap();

    let cfg = StreamConfig {
        name: "matrix-not-logged-in".to_string(),
        kind: "matrix".to_string(),
        scope: StreamScope::Session {
            scroll_id: Uuid::now_v7(),
        },
        enabled: true,
        params,
    };

    let err = factory.build(&cfg).await.err().expect("build should fail");
    match err {
        StreamBuildError::Transport(msg) => {
            assert!(
                msg.to_lowercase().contains("logged in")
                    || msg.to_lowercase().contains("matrix service"),
                "expected transport error to mention login state, got: {msg}"
            );
        }
        other => panic!("expected Transport error, got {other:?}"),
    }
}
