//! Comprehensive tests for el-identity.
//!
//! All tests use `MockEngramClient` — no running Engram instance needed.

#[cfg(test)]
mod tests {
    use std::sync::Arc;
    use uuid::Uuid;

    use crate::{
        context::IdentityContext,
        engram::{EngramClient, MockEngramClient},
        error::IdentityError,
        guard::AuthGuard,
        nodes::{
            OAuthToken, Role, Scope, Session, User,
            EDGE_GRANTS, EDGE_HAS_ROLE, EDGE_HAS_SESSION,
            NODE_USER, NODE_ROLE, NODE_SCOPE,
        },
        oauth::{PkceChallenge, hash_token},
        provider::{GoogleOAuth, AppleOAuth, GithubOAuth, OAuthProvider},
        session::SessionManager,
    };

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

    fn make_client() -> Arc<MockEngramClient> {
        Arc::new(MockEngramClient::new())
    }

    fn make_session_manager(client: Arc<MockEngramClient>) -> Arc<SessionManager> {
        Arc::new(SessionManager::new(client as Arc<dyn crate::EngramClient>))
    }

    fn setup_auth_guard() -> (Arc<MockEngramClient>, Arc<SessionManager>, AuthGuard) {
        let client = make_client();
        let sm = make_session_manager(client.clone());
        let guard = AuthGuard::new(
            client.clone() as Arc<dyn crate::EngramClient>,
            sm.clone(),
        );
        (client, sm, guard)
    }

    fn make_user() -> User {
        User::new("alice@example.com", "Alice")
    }

    fn make_role_admin() -> Role {
        Role::new("admin")
            .with_permissions(vec!["users:read", "users:write", "orders:delete"])
    }

    fn make_role_viewer() -> Role {
        Role::new("viewer")
            .with_permissions(vec!["users:read", "orders:read"])
    }

    fn make_scope_email() -> Scope {
        Scope::new("email", "Access email address")
    }

    fn make_scope_profile() -> Scope {
        Scope::new("profile", "Access profile information")
    }

    // ── Node tests ────────────────────────────────────────────────────────────

    // Test 1: User node creation and serialization round-trip
    #[test]
    fn test_user_node_round_trip() {
        let user = User::new("bob@example.com", "Bob");
        let value = user.to_value();
        let decoded = User::from_value(&value).expect("should decode User");
        assert_eq!(decoded.email, "bob@example.com");
        assert_eq!(decoded.display_name, "Bob");
        assert_eq!(decoded.id, user.id);
    }

    // Test 2: Role node with permissions serializes correctly
    #[test]
    fn test_role_node_permissions() {
        let role = make_role_admin();
        assert!(role.has_permission("users:write"));
        assert!(role.has_permission("orders:delete"));
        assert!(!role.has_permission("superadmin"));

        let value = role.to_value();
        let decoded = Role::from_value(&value).expect("should decode Role");
        assert_eq!(decoded.name, "admin");
        assert_eq!(decoded.permissions.len(), 3);
    }

    // Test 3: Scope node round-trip
    #[test]
    fn test_scope_node_round_trip() {
        let scope = make_scope_email();
        let value = scope.to_value();
        let decoded = Scope::from_value(&value).expect("should decode Scope");
        assert_eq!(decoded.name, "email");
        assert_eq!(decoded.description, "Access email address");
    }

    // Test 4: Session expiry detection
    #[test]
    fn test_session_expiry() {
        let user_id = Uuid::new_v4();
        // TTL = -1 → already expired
        let session = Session::new(user_id, -1, None);
        assert!(session.is_expired(), "session with negative TTL should be expired");

        let fresh = Session::new(user_id, 3600, None);
        assert!(!fresh.is_expired(), "fresh session should not be expired");
    }

    // Test 5: OAuthToken expiry detection
    #[test]
    fn test_oauth_token_expiry() {
        use chrono::{Duration, Utc};
        let past = Utc::now() - Duration::seconds(1);
        let token = OAuthToken::new("google", "hash123", None, past, vec![]);
        assert!(token.is_expired());

        let future = Utc::now() + Duration::seconds(3600);
        let fresh = OAuthToken::new("google", "hash456", None, future, vec!["email".into()]);
        assert!(!fresh.is_expired());
        assert!(fresh.has_scope("email"));
        assert!(!fresh.has_scope("openid"));
    }

    // ── MockEngramClient tests ─────────────────────────────────────────────────

    // Test 6: MockEngramClient create and retrieve node
    #[test]
    fn test_mock_client_create_and_get() {
        let client = make_client();
        let user = make_user();
        let id = client.create_node(NODE_USER, user.to_value()).unwrap();
        assert_eq!(id, user.id.to_string());

        let retrieved = client.get_node(&id).unwrap().expect("should find node");
        let decoded = User::from_value(&retrieved).expect("should decode");
        assert_eq!(decoded.email, user.email);
    }

    // Test 7: MockEngramClient get_node returns None for unknown ID
    #[test]
    fn test_mock_client_get_unknown_returns_none() {
        let client = make_client();
        let result = client.get_node("nonexistent-id").unwrap();
        assert!(result.is_none());
    }

    // Test 8: MockEngramClient find_nodes with query
    #[test]
    fn test_mock_client_find_nodes() {
        let client = make_client();
        let u1 = User::new("alice@example.com", "Alice");
        let u2 = User::new("bob@example.com", "Bob");
        client.create_node(NODE_USER, u1.to_value()).unwrap();
        client.create_node(NODE_USER, u2.to_value()).unwrap();

        let query = serde_json::json!({"email": "alice@example.com"});
        let results = client.find_nodes(NODE_USER, query).unwrap();
        assert_eq!(results.len(), 1);
        let found = User::from_value(&results[0]).unwrap();
        assert_eq!(found.display_name, "Alice");
    }

    // Test 9: MockEngramClient delete_node removes node and edges
    #[test]
    fn test_mock_client_delete_node() {
        let client = make_client();
        let user = make_user();
        let role = make_role_admin();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        client.create_node(NODE_ROLE, role.to_value()).unwrap();
        client.create_edge(&user.id.to_string(), &role.id.to_string(), EDGE_HAS_ROLE).unwrap();
        assert_eq!(client.count_edges(EDGE_HAS_ROLE), 1);

        client.delete_node(&user.id.to_string()).unwrap();
        assert!(client.get_node(&user.id.to_string()).unwrap().is_none());
        // Edge should also be gone
        assert_eq!(client.count_edges(EDGE_HAS_ROLE), 0);
    }

    // Test 10: MockEngramClient find_connected traverses edges
    #[test]
    fn test_mock_client_find_connected() {
        let client = make_client();
        let user = make_user();
        let role1 = make_role_admin();
        let role2 = make_role_viewer();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        client.create_node(NODE_ROLE, role1.to_value()).unwrap();
        client.create_node(NODE_ROLE, role2.to_value()).unwrap();
        client.create_edge(&user.id.to_string(), &role1.id.to_string(), EDGE_HAS_ROLE).unwrap();
        client.create_edge(&user.id.to_string(), &role2.id.to_string(), EDGE_HAS_ROLE).unwrap();

        let roles = client.find_connected(&user.id.to_string(), EDGE_HAS_ROLE).unwrap();
        assert_eq!(roles.len(), 2);
    }

    // ── SessionManager tests ───────────────────────────────────────────────────

    // Test 11: SessionManager creates session and connects to user
    #[test]
    fn test_session_manager_create() {
        let client = make_client();
        let user = make_user();
        client.create_node(NODE_USER, user.to_value()).unwrap();

        let sm = make_session_manager(client.clone());
        let session = sm.create(user.id, Some("127.0.0.1".into())).unwrap();
        assert_eq!(session.user_id, user.id);
        assert_eq!(session.ip_address, Some("127.0.0.1".to_string()));

        // Should have created a has_session edge
        assert_eq!(client.count_edges(EDGE_HAS_SESSION), 1);
    }

    // Test 12: SessionManager validate succeeds for fresh session
    #[test]
    fn test_session_manager_validate_fresh() {
        let client = make_client();
        let user = make_user();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        let sm = make_session_manager(client.clone());
        let session = sm.create(user.id, None).unwrap();

        let validated = sm.validate(&session.id.to_string()).unwrap();
        assert_eq!(validated.id, session.id);
    }

    // Test 13: SessionManager validate fails for expired session
    #[test]
    fn test_session_manager_validate_expired() {
        let client = make_client();
        let sm = Arc::new(SessionManager::new(client.clone() as Arc<dyn crate::EngramClient>).with_ttl(-1));
        let user = make_user();
        client.create_node(NODE_USER, user.to_value()).unwrap();

        let session = sm.create(user.id, None).unwrap();
        let result = sm.validate(&session.id.to_string());
        assert!(matches!(result, Err(IdentityError::SessionExpired)));
    }

    // Test 14: SessionManager validate fails for missing session
    #[test]
    fn test_session_manager_validate_missing() {
        let client = make_client();
        let sm = make_session_manager(client.clone());
        let result = sm.validate("nonexistent-session-id");
        assert!(matches!(result, Err(IdentityError::SessionNotFound)));
    }

    // Test 15: SessionManager invalidate removes session
    #[test]
    fn test_session_manager_invalidate() {
        let client = make_client();
        let user = make_user();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        let sm = make_session_manager(client.clone());
        let session = sm.create(user.id, None).unwrap();
        let session_id = session.id.to_string();

        sm.invalidate(&session_id).unwrap();
        assert!(matches!(sm.validate(&session_id), Err(IdentityError::SessionNotFound)));
    }

    // Test 16: SessionManager list_for_user returns active sessions
    #[test]
    fn test_session_manager_list_for_user() {
        let client = make_client();
        let user = make_user();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        let sm = make_session_manager(client.clone());

        sm.create(user.id, Some("1.1.1.1".into())).unwrap();
        sm.create(user.id, Some("2.2.2.2".into())).unwrap();

        let sessions = sm.list_for_user(&user.id.to_string()).unwrap();
        assert_eq!(sessions.len(), 2);
    }

    // ── AuthGuard tests ───────────────────────────────────────────────────────

    // Test 17: AuthGuard authenticates user with role and scope
    #[test]
    fn test_auth_guard_full_resolution() {
        let (client, sm, guard) = setup_auth_guard();

        let user = make_user();
        let role = make_role_admin();
        let scope = make_scope_email();

        // Register user, role, scope, and wire edges
        client.create_node(NODE_USER, user.to_value()).unwrap();
        client.create_node(NODE_ROLE, role.to_value()).unwrap();
        client.create_node(NODE_SCOPE, scope.to_value()).unwrap();
        client.create_edge(&user.id.to_string(), &role.id.to_string(), EDGE_HAS_ROLE).unwrap();
        client.create_edge(&role.id.to_string(), &scope.id.to_string(), EDGE_GRANTS).unwrap();

        // Create session
        let session = sm.create(user.id, None).unwrap();

        // Authenticate
        let ctx = guard.authenticate(&session.id.to_string()).unwrap();
        assert_eq!(ctx.user.email, "alice@example.com");
        assert_eq!(ctx.roles.len(), 1);
        assert!(ctx.has_role("admin"));
        assert!(ctx.has_scope("email"));
        assert!(ctx.has_permission("users:write"));
    }

    // Test 18: AuthGuard rejects unknown session
    #[test]
    fn test_auth_guard_rejects_unknown_session() {
        let (_, _, guard) = setup_auth_guard();
        let result = guard.authenticate("no-such-session");
        assert!(matches!(result, Err(IdentityError::SessionNotFound)));
    }

    // Test 19: AuthGuard require_role succeeds for correct role
    #[test]
    fn test_auth_guard_require_role_success() {
        let (client, sm, guard) = setup_auth_guard();
        let user = make_user();
        let role = make_role_admin();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        client.create_node(NODE_ROLE, role.to_value()).unwrap();
        client.create_edge(&user.id.to_string(), &role.id.to_string(), EDGE_HAS_ROLE).unwrap();
        let session = sm.create(user.id, None).unwrap();
        let ctx = guard.authenticate(&session.id.to_string()).unwrap();

        assert!(guard.require_role(&ctx, "admin").is_ok());
    }

    // Test 20: AuthGuard require_role fails for missing role
    #[test]
    fn test_auth_guard_require_role_forbidden() {
        let (client, sm, guard) = setup_auth_guard();
        let user = make_user();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        let session = sm.create(user.id, None).unwrap();
        let ctx = guard.authenticate(&session.id.to_string()).unwrap();

        let result = guard.require_role(&ctx, "superadmin");
        assert!(matches!(result, Err(IdentityError::Forbidden(_))));
    }

    // Test 21: AuthGuard require_scope fails for missing scope
    #[test]
    fn test_auth_guard_require_scope_forbidden() {
        let (client, sm, guard) = setup_auth_guard();
        let user = make_user();
        client.create_node(NODE_USER, user.to_value()).unwrap();
        let session = sm.create(user.id, None).unwrap();
        let ctx = guard.authenticate(&session.id.to_string()).unwrap();

        let result = guard.require_scope(&ctx, "admin:write");
        assert!(matches!(result, Err(IdentityError::ScopeForbidden(_))));
    }

    // Test 22: AuthGuard register_user and assign_role
    #[test]
    fn test_auth_guard_register_and_assign_role() {
        let (_client, sm, guard) = setup_auth_guard();
        let user = make_user();
        let role = make_role_viewer();

        guard.register_user(&user).unwrap();
        guard.assign_role(&user.id.to_string(), &role).unwrap();

        let session = sm.create(user.id, None).unwrap();
        let ctx = guard.authenticate(&session.id.to_string()).unwrap();
        assert!(ctx.has_role("viewer"));
        assert!(ctx.has_permission("users:read"));
        assert!(!ctx.has_permission("orders:delete"));
    }

    // ── PKCE tests ────────────────────────────────────────────────────────────

    // Test 23: PkceChallenge generates valid verifier/challenge pair
    #[test]
    fn test_pkce_challenge_generates_valid_pair() {
        let pkce = PkceChallenge::generate();
        assert!(!pkce.verifier.is_empty(), "verifier should not be empty");
        assert!(!pkce.challenge.is_empty(), "challenge should not be empty");
        assert_ne!(pkce.verifier, pkce.challenge, "verifier and challenge must differ");
        assert_eq!(pkce.method, "S256");
    }

    // Test 24: PkceChallenge verify matches correct verifier
    #[test]
    fn test_pkce_challenge_verify_correct() {
        let pkce = PkceChallenge::generate();
        assert!(pkce.verify(&pkce.verifier.clone()), "should verify correct verifier");
    }

    // Test 25: PkceChallenge verify rejects wrong verifier
    #[test]
    fn test_pkce_challenge_verify_rejects_wrong() {
        let pkce = PkceChallenge::generate();
        assert!(!pkce.verify("wrong-verifier-value"), "should reject wrong verifier");
    }

    // Test 26: hash_token is deterministic
    #[test]
    fn test_hash_token_deterministic() {
        let token = "my-secret-access-token";
        let h1 = hash_token(token);
        let h2 = hash_token(token);
        assert_eq!(h1, h2, "hash must be deterministic");
        assert_ne!(h1, token, "hash must differ from input");
        assert_eq!(h1.len(), 64, "SHA-256 hex output is 64 chars");
    }

    // ── Provider tests ────────────────────────────────────────────────────────

    // Test 27: GoogleOAuth builds correct authorization URL
    #[test]
    fn test_google_oauth_authorization_url() {
        let provider = GoogleOAuth::new("client-id-123", "secret");
        let url = provider.authorization_url(
            "https://myapp.com/callback",
            "pkce-challenge-abc",
            "state-xyz",
            &[],
        );
        assert!(url.starts_with("https://accounts.google.com/o/oauth2/v2/auth"));
        assert!(url.contains("client_id=client-id-123"));
        assert!(url.contains("code_challenge=pkce-challenge-abc"));
        assert!(url.contains("code_challenge_method=S256"));
        assert!(url.contains("state=state-xyz"));
        assert!(url.contains("access_type=offline"), "Google needs offline for refresh tokens");
    }

    // Test 28: GoogleOAuth default scopes include openid, email, profile
    #[test]
    fn test_google_oauth_default_scopes() {
        let provider = GoogleOAuth::new("id", "secret");
        let scopes = provider.default_scopes();
        assert!(scopes.contains(&"openid"));
        assert!(scopes.contains(&"email"));
        assert!(scopes.contains(&"profile"));
    }

    // Test 29: AppleOAuth builds correct authorization URL with form_post
    #[test]
    fn test_apple_oauth_authorization_url() {
        let provider = AppleOAuth::new("com.example.app", "jwt-secret", "TEAMID");
        let url = provider.authorization_url(
            "https://myapp.com/callback",
            "challenge",
            "state",
            &[],
        );
        assert!(url.starts_with("https://appleid.apple.com/auth/authorize"));
        assert!(url.contains("response_mode=form_post"), "Apple requires form_post");
    }

    // Test 30: GithubOAuth ignores PKCE in URL (GitHub doesn't support it)
    #[test]
    fn test_github_oauth_no_pkce_in_url() {
        let provider = GithubOAuth::new("gh-client-id", "gh-secret");
        let url = provider.authorization_url(
            "https://myapp.com/callback",
            "pkce-challenge-ignored",
            "state",
            &[],
        );
        assert!(url.starts_with("https://github.com/login/oauth/authorize"));
        assert!(!url.contains("code_challenge"), "GitHub doesn't support PKCE");
        assert!(url.contains("scope=user%3Aemail+read%3Auser") || url.contains("scope="));
    }

    // Test 31: GithubOAuth refresh_token returns error
    #[test]
    fn test_github_oauth_refresh_returns_error() {
        let provider = GithubOAuth::new("id", "secret");
        let result = provider.refresh_token("some-refresh-token");
        assert!(matches!(result, Err(IdentityError::TokenRefreshFailed(_))));
    }

    // ── IdentityContext tests ─────────────────────────────────────────────────

    // Test 32: IdentityContext role_names and scope_names
    #[test]
    fn test_identity_context_names() {
        let user = make_user();
        let session = Session::new(user.id, 3600, None);
        let roles = vec![make_role_admin(), make_role_viewer()];
        let scopes = vec![make_scope_email(), make_scope_profile()];

        let ctx = IdentityContext::new(user.clone(), session, roles, scopes);
        let mut role_names = ctx.role_names();
        role_names.sort();
        assert_eq!(role_names, vec!["admin", "viewer"]);

        let mut scope_names = ctx.scope_names();
        scope_names.sort();
        assert_eq!(scope_names, vec!["email", "profile"]);
    }

    // Test 33: IdentityContext has_permission across multiple roles
    #[test]
    fn test_identity_context_permission_across_roles() {
        let user = make_user();
        let session = Session::new(user.id, 3600, None);
        let roles = vec![make_role_viewer()]; // viewer has users:read and orders:read
        let ctx = IdentityContext::new(user, session, roles, vec![]);

        assert!(ctx.has_permission("users:read"));
        assert!(ctx.has_permission("orders:read"));
        assert!(!ctx.has_permission("users:write"));
        assert!(!ctx.has_permission("orders:delete"));
    }

    // Test 34: AuthGuard assign_scope_to_role wires scope into context
    #[test]
    fn test_auth_guard_assign_scope_to_role() {
        let (_client, sm, guard) = setup_auth_guard();
        let user = make_user();
        let role = make_role_admin();
        let scope = make_scope_profile();

        guard.register_user(&user).unwrap();
        guard.assign_role(&user.id.to_string(), &role).unwrap();
        guard.assign_scope_to_role(&role, &scope).unwrap();

        let session = sm.create(user.id, None).unwrap();
        let ctx = guard.authenticate(&session.id.to_string()).unwrap();
        assert!(ctx.has_scope("profile"));
    }

    // Test 35: Two different users have independent sessions
    #[test]
    fn test_independent_user_sessions() {
        let (client, sm, guard) = setup_auth_guard();

        let alice = User::new("alice@example.com", "Alice");
        let bob = User::new("bob@example.com", "Bob");
        let admin_role = make_role_admin();

        client.create_node(NODE_USER, alice.to_value()).unwrap();
        client.create_node(NODE_USER, bob.to_value()).unwrap();
        client.create_node(NODE_ROLE, admin_role.to_value()).unwrap();
        // Only Alice gets the admin role
        client.create_edge(&alice.id.to_string(), &admin_role.id.to_string(), EDGE_HAS_ROLE).unwrap();

        let alice_session = sm.create(alice.id, None).unwrap();
        let bob_session = sm.create(bob.id, None).unwrap();

        let alice_ctx = guard.authenticate(&alice_session.id.to_string()).unwrap();
        let bob_ctx = guard.authenticate(&bob_session.id.to_string()).unwrap();

        assert!(alice_ctx.has_role("admin"), "Alice should have admin");
        assert!(!bob_ctx.has_role("admin"), "Bob should not have admin");
        assert_eq!(alice_ctx.email(), "alice@example.com");
        assert_eq!(bob_ctx.email(), "bob@example.com");
    }
}
