use std::sync::{Arc, Mutex};

use axum::Router;
use chrono::Utc;
use forage_core::auth::*;
use forage_core::platform::{
    Artifact, ArtifactContext, CreatePolicyInput, CreateReleasePipelineInput, CreateTriggerInput,
    Destination, Environment, ForestPlatform, NotificationPreference, Organisation, OrgMember,
    PlatformError, Policy, ReleasePipeline, Trigger, UpdatePolicyInput, UpdateReleasePipelineInput,
    UpdateTriggerInput,
};
use forage_core::integrations::InMemoryIntegrationStore;
use forage_core::session::{
    CachedOrg, CachedUser, InMemorySessionStore, SessionData, SessionStore,
};

use crate::state::AppState;
use crate::templates::TemplateEngine;

/// Configurable mock behavior for testing different scenarios.
#[derive(Default)]
pub(crate) struct MockBehavior {
    pub register_result: Option<Result<AuthTokens, AuthError>>,
    pub login_result: Option<Result<AuthTokens, AuthError>>,
    pub refresh_result: Option<Result<AuthTokens, AuthError>>,
    pub get_user_result: Option<Result<User, AuthError>>,
    pub list_tokens_result: Option<Result<Vec<PersonalAccessToken>, AuthError>>,
    pub create_token_result: Option<Result<CreatedToken, AuthError>>,
    pub delete_token_result: Option<Result<(), AuthError>>,
    pub update_username_result: Option<Result<User, AuthError>>,
    pub change_password_result: Option<Result<(), AuthError>>,
    pub add_email_result: Option<Result<UserEmail, AuthError>>,
    pub remove_email_result: Option<Result<(), AuthError>>,
}

/// Configurable mock behavior for platform (orgs, projects, artifacts).
#[derive(Default)]
pub(crate) struct MockPlatformBehavior {
    pub list_orgs_result: Option<Result<Vec<Organisation>, PlatformError>>,
    pub list_projects_result: Option<Result<Vec<String>, PlatformError>>,
    pub list_artifacts_result: Option<Result<Vec<Artifact>, PlatformError>>,
    pub create_organisation_result: Option<Result<String, PlatformError>>,
    pub list_members_result: Option<Result<Vec<OrgMember>, PlatformError>>,
    pub add_member_result: Option<Result<OrgMember, PlatformError>>,
    pub remove_member_result: Option<Result<(), PlatformError>>,
    pub update_member_role_result: Option<Result<OrgMember, PlatformError>>,
    pub get_artifact_by_slug_result: Option<Result<Artifact, PlatformError>>,
    pub list_environments_result: Option<Result<Vec<Environment>, PlatformError>>,
    pub list_destinations_result: Option<Result<Vec<Destination>, PlatformError>>,
    pub list_triggers_result: Option<Result<Vec<Trigger>, PlatformError>>,
    pub create_trigger_result: Option<Result<Trigger, PlatformError>>,
    pub update_trigger_result: Option<Result<Trigger, PlatformError>>,
    pub delete_trigger_result: Option<Result<(), PlatformError>>,
    pub list_release_pipelines_result: Option<Result<Vec<ReleasePipeline>, PlatformError>>,
    pub create_release_pipeline_result: Option<Result<ReleasePipeline, PlatformError>>,
    pub update_release_pipeline_result: Option<Result<ReleasePipeline, PlatformError>>,
    pub delete_release_pipeline_result: Option<Result<(), PlatformError>>,
    pub get_artifact_spec_result: Option<Result<String, PlatformError>>,
    pub get_notification_preferences_result: Option<Result<Vec<NotificationPreference>, PlatformError>>,
    pub set_notification_preference_result: Option<Result<(), PlatformError>>,
}

pub(crate) fn ok_tokens() -> AuthTokens {
    AuthTokens {
        access_token: "mock-access".into(),
        refresh_token: "mock-refresh".into(),
        expires_in_seconds: 3600,
    }
}

pub(crate) fn ok_user() -> User {
    User {
        user_id: "user-123".into(),
        username: "testuser".into(),
        emails: vec![UserEmail {
            email: "test@example.com".into(),
            verified: true,
        }],
    }
}

/// Mock forest client with per-test configurable behavior.
pub(crate) struct MockForestClient {
    behavior: Mutex<MockBehavior>,
}

impl MockForestClient {
    pub fn new() -> Self {
        Self {
            behavior: Mutex::new(MockBehavior::default()),
        }
    }

    pub fn with_behavior(behavior: MockBehavior) -> Self {
        Self {
            behavior: Mutex::new(behavior),
        }
    }
}

#[async_trait::async_trait]
impl ForestAuth for MockForestClient {
    async fn register(
        &self,
        _username: &str,
        _email: &str,
        _password: &str,
    ) -> Result<AuthTokens, AuthError> {
        let b = self.behavior.lock().unwrap();
        b.register_result.clone().unwrap_or(Ok(ok_tokens()))
    }

    async fn login(
        &self,
        identifier: &str,
        password: &str,
    ) -> Result<AuthTokens, AuthError> {
        let b = self.behavior.lock().unwrap();
        if let Some(result) = b.login_result.clone() {
            return result;
        }
        if identifier == "testuser" && password == "CorrectPass123" {
            Ok(ok_tokens())
        } else {
            Err(AuthError::InvalidCredentials)
        }
    }

    async fn refresh_token(&self, _refresh_token: &str) -> Result<AuthTokens, AuthError> {
        let b = self.behavior.lock().unwrap();
        b.refresh_result.clone().unwrap_or(Ok(AuthTokens {
            access_token: "refreshed-access".into(),
            refresh_token: "refreshed-refresh".into(),
            expires_in_seconds: 3600,
        }))
    }

    async fn logout(&self, _refresh_token: &str) -> Result<(), AuthError> {
        Ok(())
    }

    async fn get_user(&self, access_token: &str) -> Result<User, AuthError> {
        let b = self.behavior.lock().unwrap();
        if let Some(result) = b.get_user_result.clone() {
            return result;
        }
        if access_token == "mock-access" || access_token == "refreshed-access" {
            Ok(ok_user())
        } else {
            Err(AuthError::NotAuthenticated)
        }
    }

    async fn list_tokens(
        &self,
        _access_token: &str,
        _user_id: &str,
    ) -> Result<Vec<PersonalAccessToken>, AuthError> {
        let b = self.behavior.lock().unwrap();
        b.list_tokens_result.clone().unwrap_or(Ok(vec![]))
    }

    async fn create_token(
        &self,
        _access_token: &str,
        _user_id: &str,
        name: &str,
    ) -> Result<CreatedToken, AuthError> {
        let b = self.behavior.lock().unwrap();
        b.create_token_result.clone().unwrap_or(Ok(CreatedToken {
            token: PersonalAccessToken {
                token_id: "tok-1".into(),
                name: name.into(),
                scopes: vec![],
                created_at: None,
                last_used: None,
                expires_at: None,
            },
            raw_token: "forg_abcdef1234567890".into(),
        }))
    }

    async fn delete_token(
        &self,
        _access_token: &str,
        _token_id: &str,
    ) -> Result<(), AuthError> {
        let b = self.behavior.lock().unwrap();
        b.delete_token_result.clone().unwrap_or(Ok(()))
    }

    async fn update_username(
        &self,
        _access_token: &str,
        _user_id: &str,
        new_username: &str,
    ) -> Result<User, AuthError> {
        let b = self.behavior.lock().unwrap();
        b.update_username_result.clone().unwrap_or(Ok(User {
            user_id: "user-123".into(),
            username: new_username.into(),
            emails: vec![UserEmail {
                email: "test@example.com".into(),
                verified: true,
            }],
        }))
    }

    async fn change_password(
        &self,
        _access_token: &str,
        _user_id: &str,
        _current_password: &str,
        _new_password: &str,
    ) -> Result<(), AuthError> {
        let b = self.behavior.lock().unwrap();
        b.change_password_result.clone().unwrap_or(Ok(()))
    }

    async fn add_email(
        &self,
        _access_token: &str,
        _user_id: &str,
        email: &str,
    ) -> Result<UserEmail, AuthError> {
        let b = self.behavior.lock().unwrap();
        b.add_email_result.clone().unwrap_or(Ok(UserEmail {
            email: email.into(),
            verified: false,
        }))
    }

    async fn get_user_by_username(
        &self,
        _access_token: &str,
        username: &str,
    ) -> Result<UserProfile, AuthError> {
        Ok(UserProfile {
            user_id: "user-123".into(),
            username: username.into(),
            created_at: Some("2025-01-15T10:00:00Z".into()),
        })
    }

    async fn remove_email(
        &self,
        _access_token: &str,
        _user_id: &str,
        _email: &str,
    ) -> Result<(), AuthError> {
        let b = self.behavior.lock().unwrap();
        b.remove_email_result.clone().unwrap_or(Ok(()))
    }
}

pub(crate) struct MockPlatformClient {
    behavior: Mutex<MockPlatformBehavior>,
}

impl MockPlatformClient {
    pub fn new() -> Self {
        Self {
            behavior: Mutex::new(MockPlatformBehavior::default()),
        }
    }

    pub fn with_behavior(behavior: MockPlatformBehavior) -> Self {
        Self {
            behavior: Mutex::new(behavior),
        }
    }
}

pub(crate) fn default_orgs() -> Vec<Organisation> {
    vec![Organisation {
        organisation_id: "org-1".into(),
        name: "testorg".into(),
        role: "admin".into(),
    }]
}

#[async_trait::async_trait]
impl ForestPlatform for MockPlatformClient {
    async fn list_my_organisations(
        &self,
        _access_token: &str,
    ) -> Result<Vec<Organisation>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_orgs_result.clone().unwrap_or(Ok(default_orgs()))
    }

    async fn list_projects(
        &self,
        _access_token: &str,
        _organisation: &str,
    ) -> Result<Vec<String>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_projects_result
            .clone()
            .unwrap_or(Ok(vec!["my-api".into()]))
    }

    async fn list_artifacts(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
    ) -> Result<Vec<Artifact>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_artifacts_result.clone().unwrap_or(Ok(vec![Artifact {
            artifact_id: "art-1".into(),
            slug: "my-api-abc123".into(),
            context: ArtifactContext {
                title: "Deploy v1.0".into(),
                description: Some("Initial release".into()),
                web: None,
                pr: None,
            },
            source: None,
            git_ref: None,
            destinations: vec![],
            created_at: "2026-03-07T12:00:00Z".into(),
        }]))
    }

    async fn create_organisation(
        &self,
        _access_token: &str,
        name: &str,
    ) -> Result<String, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.create_organisation_result
            .clone()
            .unwrap_or(Ok(format!("org-{name}")))
    }

    async fn list_members(
        &self,
        _access_token: &str,
        _organisation_id: &str,
    ) -> Result<Vec<OrgMember>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_members_result.clone().unwrap_or(Ok(vec![OrgMember {
            user_id: "user-123".into(),
            username: "testuser".into(),
            role: "owner".into(),
            joined_at: Some("2026-01-01T00:00:00Z".into()),
        }]))
    }

    async fn add_member(
        &self,
        _access_token: &str,
        _organisation_id: &str,
        user_id: &str,
        role: &str,
    ) -> Result<OrgMember, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.add_member_result.clone().unwrap_or(Ok(OrgMember {
            user_id: user_id.into(),
            username: "newuser".into(),
            role: role.into(),
            joined_at: Some("2026-03-07T00:00:00Z".into()),
        }))
    }

    async fn remove_member(
        &self,
        _access_token: &str,
        _organisation_id: &str,
        _user_id: &str,
    ) -> Result<(), PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.remove_member_result.clone().unwrap_or(Ok(()))
    }

    async fn update_member_role(
        &self,
        _access_token: &str,
        _organisation_id: &str,
        user_id: &str,
        role: &str,
    ) -> Result<OrgMember, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.update_member_role_result.clone().unwrap_or(Ok(OrgMember {
            user_id: user_id.into(),
            username: "testuser".into(),
            role: role.into(),
            joined_at: Some("2026-01-01T00:00:00Z".into()),
        }))
    }

    async fn get_artifact_by_slug(
        &self,
        _access_token: &str,
        slug: &str,
    ) -> Result<Artifact, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.get_artifact_by_slug_result
            .clone()
            .unwrap_or(Ok(Artifact {
                artifact_id: "art-1".into(),
                slug: slug.into(),
                context: ArtifactContext {
                    title: "Deploy v1.0".into(),
                    description: Some("Initial release".into()),
                    web: None,
                    pr: None,
                },
                source: None,
                git_ref: None,
                destinations: vec![],
                created_at: "2026-03-07T12:00:00Z".into(),
            }))
    }

    async fn list_environments(
        &self,
        _access_token: &str,
        _organisation: &str,
    ) -> Result<Vec<Environment>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_environments_result.clone().unwrap_or(Ok(vec![]))
    }

    async fn list_destinations(
        &self,
        _access_token: &str,
        _organisation: &str,
    ) -> Result<Vec<Destination>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_destinations_result.clone().unwrap_or(Ok(vec![]))
    }

    async fn create_environment(
        &self,
        _access_token: &str,
        organisation: &str,
        name: &str,
        description: Option<&str>,
        sort_order: i32,
    ) -> Result<Environment, PlatformError> {
        Ok(Environment {
            id: format!("env-{name}"),
            organisation: organisation.into(),
            name: name.into(),
            description: description.map(|s| s.to_string()),
            sort_order,
            created_at: "2026-03-08T00:00:00Z".into(),
        })
    }

    async fn create_destination(
        &self,
        _access_token: &str,
        _organisation: &str,
        _name: &str,
        _environment: &str,
        _metadata: &std::collections::HashMap<String, String>,
        _dest_type: Option<&forage_core::platform::DestinationType>,
    ) -> Result<(), PlatformError> {
        Ok(())
    }

    async fn update_destination(
        &self,
        _access_token: &str,
        _name: &str,
        _metadata: &std::collections::HashMap<String, String>,
    ) -> Result<(), PlatformError> {
        Ok(())
    }

    async fn get_destination_states(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: Option<&str>,
    ) -> Result<forage_core::platform::DeploymentStates, PlatformError> {
        Ok(forage_core::platform::DeploymentStates {
            destinations: vec![],
        })
    }

    async fn get_release_intent_states(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: Option<&str>,
        _include_completed: bool,
    ) -> Result<Vec<forage_core::platform::ReleaseIntentState>, PlatformError> {
        Ok(vec![])
    }

    async fn release_artifact(
        &self,
        _access_token: &str,
        _artifact_id: &str,
        _destinations: &[String],
        _environments: &[String],
        _use_pipeline: bool,
    ) -> Result<(), PlatformError> {
        Ok(())
    }

    async fn list_triggers(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
    ) -> Result<Vec<Trigger>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_triggers_result.clone().unwrap_or(Ok(vec![]))
    }

    async fn create_trigger(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        input: &CreateTriggerInput,
    ) -> Result<Trigger, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.create_trigger_result.clone().unwrap_or(Ok(Trigger {
            id: "trigger-1".into(),
            name: input.name.clone(),
            enabled: true,
            branch_pattern: input.branch_pattern.clone(),
            title_pattern: input.title_pattern.clone(),
            author_pattern: input.author_pattern.clone(),
            commit_message_pattern: input.commit_message_pattern.clone(),
            source_type_pattern: input.source_type_pattern.clone(),
            target_environments: input.target_environments.clone(),
            target_destinations: input.target_destinations.clone(),
            force_release: input.force_release,
            use_pipeline: input.use_pipeline,
            created_at: "2026-03-08T00:00:00Z".into(),
            updated_at: "2026-03-08T00:00:00Z".into(),
        }))
    }

    async fn update_trigger(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        name: &str,
        input: &UpdateTriggerInput,
    ) -> Result<Trigger, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.update_trigger_result.clone().unwrap_or(Ok(Trigger {
            id: "trigger-1".into(),
            name: name.into(),
            enabled: input.enabled.unwrap_or(true),
            branch_pattern: input.branch_pattern.clone(),
            title_pattern: input.title_pattern.clone(),
            author_pattern: input.author_pattern.clone(),
            commit_message_pattern: input.commit_message_pattern.clone(),
            source_type_pattern: input.source_type_pattern.clone(),
            target_environments: input.target_environments.clone(),
            target_destinations: input.target_destinations.clone(),
            force_release: input.force_release.unwrap_or(false),
            use_pipeline: input.use_pipeline.unwrap_or(false),
            created_at: "2026-03-08T00:00:00Z".into(),
            updated_at: "2026-03-08T00:00:00Z".into(),
        }))
    }

    async fn delete_trigger(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        _name: &str,
    ) -> Result<(), PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.delete_trigger_result.clone().unwrap_or(Ok(()))
    }

    async fn list_policies(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
    ) -> Result<Vec<Policy>, PlatformError> {
        Ok(vec![])
    }

    async fn create_policy(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        _input: &CreatePolicyInput,
    ) -> Result<Policy, PlatformError> {
        Err(PlatformError::Other("not implemented in mock".into()))
    }

    async fn update_policy(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        _name: &str,
        _input: &UpdatePolicyInput,
    ) -> Result<Policy, PlatformError> {
        Err(PlatformError::Other("not implemented in mock".into()))
    }

    async fn delete_policy(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        _name: &str,
    ) -> Result<(), PlatformError> {
        Ok(())
    }

    async fn list_release_pipelines(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
    ) -> Result<Vec<ReleasePipeline>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.list_release_pipelines_result
            .clone()
            .unwrap_or(Ok(vec![]))
    }

    async fn create_release_pipeline(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        input: &CreateReleasePipelineInput,
    ) -> Result<ReleasePipeline, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.create_release_pipeline_result
            .clone()
            .unwrap_or(Ok(ReleasePipeline {
                id: "pipeline-1".into(),
                name: input.name.clone(),
                enabled: true,
                stages: input.stages.clone(),
                created_at: "2026-03-08T00:00:00Z".into(),
                updated_at: "2026-03-08T00:00:00Z".into(),
            }))
    }

    async fn update_release_pipeline(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        name: &str,
        input: &UpdateReleasePipelineInput,
    ) -> Result<ReleasePipeline, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.update_release_pipeline_result
            .clone()
            .unwrap_or(Ok(ReleasePipeline {
                id: "pipeline-1".into(),
                name: name.into(),
                enabled: input.enabled.unwrap_or(true),
                stages: input.stages.clone().unwrap_or_default(),
                created_at: "2026-03-08T00:00:00Z".into(),
                updated_at: "2026-03-08T00:00:00Z".into(),
            }))
    }

    async fn delete_release_pipeline(
        &self,
        _access_token: &str,
        _organisation: &str,
        _project: &str,
        _name: &str,
    ) -> Result<(), PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.delete_release_pipeline_result.clone().unwrap_or(Ok(()))
    }

    async fn get_artifact_spec(
        &self,
        _access_token: &str,
        _artifact_id: &str,
    ) -> Result<String, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.get_artifact_spec_result
            .clone()
            .unwrap_or(Ok(String::new()))
    }

    async fn get_notification_preferences(
        &self,
        _access_token: &str,
    ) -> Result<Vec<NotificationPreference>, PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.get_notification_preferences_result
            .clone()
            .unwrap_or(Ok(Vec::new()))
    }

    async fn set_notification_preference(
        &self,
        _access_token: &str,
        _notification_type: &str,
        _channel: &str,
        _enabled: bool,
    ) -> Result<(), PlatformError> {
        let b = self.behavior.lock().unwrap();
        b.set_notification_preference_result
            .clone()
            .unwrap_or(Ok(()))
    }
}

pub(crate) fn make_templates() -> TemplateEngine {
    let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .parent()
        .unwrap();
    TemplateEngine::from_path(&workspace_root.join("templates"))
        .expect("templates must load for tests")
}

pub(crate) fn test_state() -> (AppState, Arc<InMemorySessionStore>) {
    test_state_with(MockForestClient::new(), MockPlatformClient::new())
}

pub(crate) fn test_state_with(
    mock: MockForestClient,
    platform: MockPlatformClient,
) -> (AppState, Arc<InMemorySessionStore>) {
    let sessions = Arc::new(InMemorySessionStore::new());
    let state = AppState::new(
        make_templates(),
        Arc::new(mock),
        Arc::new(platform),
        sessions.clone(),
    );
    (state, sessions)
}

pub(crate) fn test_state_with_integrations(
    mock: MockForestClient,
    platform: MockPlatformClient,
) -> (AppState, Arc<InMemorySessionStore>, Arc<InMemoryIntegrationStore>) {
    let sessions = Arc::new(InMemorySessionStore::new());
    let integrations = Arc::new(InMemoryIntegrationStore::new());
    let state = AppState::new(
        make_templates(),
        Arc::new(mock),
        Arc::new(platform),
        sessions.clone(),
    )
    .with_integration_store(integrations.clone());
    (state, sessions, integrations)
}

pub(crate) fn test_app() -> Router {
    let (state, _) = test_state();
    crate::build_router(state)
}

pub(crate) fn test_app_with(mock: MockForestClient) -> Router {
    let (state, _) = test_state_with(mock, MockPlatformClient::new());
    crate::build_router(state)
}

pub(crate) fn default_test_orgs() -> Vec<CachedOrg> {
    vec![CachedOrg {
        organisation_id: "org-1".into(),
        name: "testorg".into(),
        role: "owner".into(),
    }]
}

/// Create a test session and return the cookie header value.
pub(crate) async fn create_test_session(sessions: &Arc<InMemorySessionStore>) -> String {
    let now = Utc::now();
    let data = SessionData {
        access_token: "mock-access".into(),
        refresh_token: "mock-refresh".into(),
        csrf_token: "test-csrf".into(),
        access_expires_at: now + chrono::Duration::hours(1),
        user: Some(CachedUser {
            user_id: "user-123".into(),
            username: "testuser".into(),
            emails: vec![UserEmail {
                email: "test@example.com".into(),
                verified: true,
            }],
            orgs: default_test_orgs(),
        }),
        created_at: now,
        last_seen_at: now,
    };
    let session_id = sessions.create(data).await.unwrap();
    format!("forage_session={}", session_id)
}

/// Create a test session with an expired access token but valid refresh token.
pub(crate) async fn create_expired_session(sessions: &Arc<InMemorySessionStore>) -> String {
    let now = Utc::now();
    let data = SessionData {
        access_token: "expired-access".into(),
        refresh_token: "mock-refresh".into(),
        csrf_token: "test-csrf".into(),
        access_expires_at: now - chrono::Duration::seconds(10),
        user: Some(CachedUser {
            user_id: "user-123".into(),
            username: "testuser".into(),
            emails: vec![UserEmail {
                email: "test@example.com".into(),
                verified: true,
            }],
            orgs: default_test_orgs(),
        }),
        created_at: now,
        last_seen_at: now,
    };
    let session_id = sessions.create(data).await.unwrap();
    format!("forage_session={}", session_id)
}

/// Create a test session with "member" role (non-admin, for authorization tests).
pub(crate) async fn create_test_session_member(sessions: &Arc<InMemorySessionStore>) -> String {
    let now = Utc::now();
    let data = SessionData {
        access_token: "mock-access".into(),
        refresh_token: "mock-refresh".into(),
        csrf_token: "test-csrf".into(),
        access_expires_at: now + chrono::Duration::hours(1),
        user: Some(CachedUser {
            user_id: "user-123".into(),
            username: "testuser".into(),
            emails: vec![UserEmail {
                email: "test@example.com".into(),
                verified: true,
            }],
            orgs: vec![CachedOrg {
                organisation_id: "org-1".into(),
                name: "testorg".into(),
                role: "member".into(),
            }],
        }),
        created_at: now,
        last_seen_at: now,
    };
    let session_id = sessions.create(data).await.unwrap();
    format!("forage_session={}", session_id)
}

/// Create a test session with no cached orgs (for onboarding tests).
pub(crate) async fn create_test_session_no_orgs(sessions: &Arc<InMemorySessionStore>) -> String {
    let now = Utc::now();
    let data = SessionData {
        access_token: "mock-access".into(),
        refresh_token: "mock-refresh".into(),
        csrf_token: "test-csrf".into(),
        access_expires_at: now + chrono::Duration::hours(1),
        user: Some(CachedUser {
            user_id: "user-123".into(),
            username: "testuser".into(),
            emails: vec![UserEmail {
                email: "test@example.com".into(),
                verified: true,
            }],
            orgs: vec![],
        }),
        created_at: now,
        last_seen_at: now,
    };
    let session_id = sessions.create(data).await.unwrap();
    format!("forage_session={}", session_id)
}
