use diesel::prelude::*;
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use time::OffsetDateTime;

use crate::config::PermissionMode;
use crate::schema::{a2ui_messages, messages, projects, sessions};

pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");

#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = projects)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Project {
    pub id: i32,
    pub name: String,
    pub path: String,
    /// Sidebar state: hides this project's session rows.
    pub collapsed: bool,
}

#[derive(Insertable)]
#[diesel(table_name = projects)]
struct NewProject<'a> {
    name: &'a str,
    path: &'a str,
}

#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(belongs_to(Project))]
#[diesel(table_name = sessions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Session {
    pub id: i32,
    pub project_id: i32,
    pub title: String,
    pub context_used: i32,
    pub context_limit: i32,
    pub last_tokens_per_second: Option<f32>,
    /// Raw column value; read it through [`Session::state`].
    state: String,
    pub compacted_summary: Option<String>,
    last_used: i64,
    /// Raw column value; read it through [`Session::permission_mode`].
    permission_mode: String,
}

impl Session {
    pub fn state(&self) -> SessionState {
        SessionState::from_id(&self.state).unwrap_or_default()
    }

    pub fn permission_mode(&self) -> PermissionMode {
        PermissionMode::from_id(&self.permission_mode).unwrap_or_default()
    }

    #[cfg(test)]
    pub fn fixture(id: i32, project_id: i32, title: &str, state: SessionState) -> Self {
        Self {
            id,
            project_id,
            title: title.to_owned(),
            context_used: 0,
            context_limit: 0,
            last_tokens_per_second: None,
            state: state.as_id().to_owned(),
            compacted_summary: None,
            last_used: 0,
            permission_mode: PermissionMode::default().as_id().to_owned(),
        }
    }
}

/// Where a session sits in the sidebar. These are lifecycle states, not flags:
/// a session is exactly one of them.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SessionState {
    #[default]
    Normal,
    Pinned,
    Archived,
}

impl SessionState {
    pub fn as_id(self) -> &'static str {
        match self {
            Self::Normal => "normal",
            Self::Pinned => "pinned",
            Self::Archived => "archived",
        }
    }

    pub fn from_id(id: &str) -> Option<Self> {
        match id {
            "normal" => Some(Self::Normal),
            "pinned" => Some(Self::Pinned),
            "archived" => Some(Self::Archived),
            _ => None,
        }
    }

    /// Sidebar grouping order: pinned, then ordinary, then archived.
    pub fn rank(self) -> u8 {
        match self {
            Self::Pinned => 0,
            Self::Normal => 1,
            Self::Archived => 2,
        }
    }
}

#[derive(Insertable)]
#[diesel(table_name = sessions)]
struct NewSession<'a> {
    project_id: i32,
    title: &'a str,
    last_used: i64,
    permission_mode: &'a str,
}

#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = messages)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct StoredMessage {
    pub id: i32,
    pub session_id: i32,
    pub user: bool,
    pub tool: bool,
    pub reasoning: Option<String>,
    pub reasoning_complete: bool,
    pub content: String,
    pub model_content: Option<String>,
    pub tool_approval_reasons: Option<String>,
    pub instruction_metadata: Option<String>,
    pub system: bool,
    pub compaction: bool,
    pub compaction_tail_start: Option<i32>,
    pub generation_duration_ms: Option<i32>,
    pub input_tokens: Option<i32>,
    pub cached_tokens: Option<i32>,
    pub output_tokens: Option<i32>,
}

#[derive(Insertable)]
#[diesel(table_name = messages)]
struct NewMessage<'a> {
    session_id: i32,
    user: bool,
    tool: bool,
    reasoning: Option<&'a str>,
    reasoning_complete: bool,
    content: &'a str,
    model_content: Option<&'a str>,
    tool_approval_reasons: Option<&'a str>,
    instruction_metadata: Option<&'a str>,
    system: bool,
    compaction: bool,
    compaction_tail_start: Option<i32>,
}

impl<'a> NewMessage<'a> {
    fn system(session_id: i32, content: &'a str, instruction_metadata: Option<&'a str>) -> Self {
        Self {
            instruction_metadata,
            ..Self::new(session_id, content, false, false, true)
        }
    }

    fn user(session_id: i32, content: &'a str, model_content: Option<&'a str>) -> Self {
        Self {
            model_content,
            ..Self::new(session_id, content, true, false, false)
        }
    }

    fn tool_result(session_id: i32, content: &'a str) -> Self {
        Self::new(session_id, content, false, true, false)
    }

    fn assistant(session_id: i32, reasoning: bool) -> Self {
        Self {
            reasoning: reasoning.then_some(""),
            reasoning_complete: !reasoning,
            ..Self::new(session_id, "", false, false, false)
        }
    }

    fn compaction(session_id: i32, content: &'a str, tail_start: Option<i32>) -> Self {
        Self {
            compaction: true,
            compaction_tail_start: tail_start,
            ..Self::system(session_id, content, None)
        }
    }

    fn new(session_id: i32, content: &'a str, user: bool, tool: bool, system: bool) -> Self {
        Self {
            session_id,
            user,
            tool,
            reasoning: None,
            reasoning_complete: true,
            content,
            model_content: None,
            tool_approval_reasons: None,
            instruction_metadata: None,
            system,
            compaction: false,
            compaction_tail_start: None,
        }
    }
}

pub struct SystemMessage {
    pub content: String,
    pub instruction_metadata: Option<String>,
}

impl SystemMessage {
    pub fn plain(content: impl Into<String>) -> Self {
        Self {
            content: content.into(),
            instruction_metadata: None,
        }
    }

    pub fn instruction(content: String, metadata: String) -> Self {
        Self {
            content,
            instruction_metadata: Some(metadata),
        }
    }
}

impl From<&str> for SystemMessage {
    fn from(content: &str) -> Self {
        Self::plain(content)
    }
}

impl From<String> for SystemMessage {
    fn from(content: String) -> Self {
        Self::plain(content)
    }
}

#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = a2ui_messages)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct StoredA2uiMessage {
    pub id: i32,
    pub session_id: i32,
    pub message_id: i32,
    pub json: String,
    pub dismissed: bool,
}

#[derive(Insertable)]
#[diesel(table_name = a2ui_messages)]
struct NewA2uiMessage<'a> {
    session_id: i32,
    message_id: i32,
    json: &'a str,
    dismissed: bool,
}

#[derive(Debug)]
pub struct ProjectWithSessions {
    pub project: Project,
    pub sessions: Vec<Session>,
}

pub struct Database {
    connection: SqliteConnection,
}

impl Database {
    pub fn open(path: &Path) -> Result<Self, String> {
        let parent = path
            .parent()
            .ok_or_else(|| "database path has no parent directory".to_owned())?;
        fs::create_dir_all(parent).map_err(|error| error.to_string())?;
        let url = path
            .to_str()
            .ok_or_else(|| "database path is not valid UTF-8".to_owned())?;
        let mut connection = SqliteConnection::establish(url).map_err(|error| error.to_string())?;
        connection
            .run_pending_migrations(MIGRATIONS)
            .map_err(|error| error.to_string())?;
        Ok(Self { connection })
    }

    pub fn load_projects(&mut self) -> Result<Vec<ProjectWithSessions>, String> {
        let project_rows = projects::table
            .order(projects::id.asc())
            .select(Project::as_select())
            .load(&mut self.connection)
            .map_err(|error| error.to_string())?;
        let session_rows = sessions::table
            .order(sessions::id.asc())
            .select(Session::as_select())
            .load::<Session>(&mut self.connection)
            .map_err(|error| error.to_string())?;
        let mut sessions_by_project: HashMap<i32, Vec<Session>> = HashMap::new();
        for session in session_rows {
            sessions_by_project
                .entry(session.project_id)
                .or_default()
                .push(session);
        }
        // Preserve sidebar groups while matching DS4's newest-used-first order.
        for sessions in sessions_by_project.values_mut() {
            sessions.sort_by(|left, right| {
                left.state()
                    .rank()
                    .cmp(&right.state().rank())
                    .then_with(|| right.last_used.cmp(&left.last_used))
                    .then_with(|| right.id.cmp(&left.id))
            });
        }

        Ok(project_rows
            .into_iter()
            .map(|project| ProjectWithSessions {
                sessions: sessions_by_project.remove(&project.id).unwrap_or_default(),
                project,
            })
            .collect())
    }

    pub fn create_project(&mut self, name: &str, path: &str) -> Result<Project, String> {
        diesel::insert_into(projects::table)
            .values(NewProject { name, path })
            .returning(Project::as_returning())
            .get_result(&mut self.connection)
            .map_err(|error| error.to_string())
    }

    pub fn set_project_collapsed(
        &mut self,
        project_id: i32,
        collapsed: bool,
    ) -> Result<(), String> {
        diesel::update(projects::table.find(project_id))
            .set(projects::collapsed.eq(collapsed))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn delete_project(&mut self, project_id: i32) -> Result<(), String> {
        self.connection
            .transaction(|connection| {
                diesel::delete(
                    a2ui_messages::table.filter(
                        a2ui_messages::session_id.eq_any(
                            sessions::table
                                .filter(sessions::project_id.eq(project_id))
                                .select(sessions::id),
                        ),
                    ),
                )
                .execute(connection)?;
                diesel::delete(
                    messages::table.filter(
                        messages::session_id.eq_any(
                            sessions::table
                                .filter(sessions::project_id.eq(project_id))
                                .select(sessions::id),
                        ),
                    ),
                )
                .execute(connection)?;
                diesel::delete(sessions::table.filter(sessions::project_id.eq(project_id)))
                    .execute(connection)?;
                diesel::delete(projects::table.find(project_id)).execute(connection)?;
                Ok(())
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }

    pub fn create_session(
        &mut self,
        project_id: i32,
        title: &str,
        permission_mode: PermissionMode,
    ) -> Result<Session, String> {
        diesel::insert_into(sessions::table)
            .values(NewSession {
                project_id,
                title,
                last_used: OffsetDateTime::now_utc().unix_timestamp(),
                permission_mode: permission_mode.as_id(),
            })
            .returning(Session::as_returning())
            .get_result(&mut self.connection)
            .map_err(|error| error.to_string())
    }

    pub fn set_session_permission_mode(
        &mut self,
        session_id: i32,
        permission_mode: PermissionMode,
    ) -> Result<(), String> {
        diesel::update(sessions::table.find(session_id))
            .set(sessions::permission_mode.eq(permission_mode.as_id()))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn touch_session(&mut self, session_id: i32) -> Result<(), String> {
        touch_session(&mut self.connection, session_id).map_err(|error| error.to_string())
    }

    pub fn rename_session(&mut self, session_id: i32, title: &str) -> Result<(), String> {
        let title = title.trim();
        if title.is_empty() {
            return Err("Session title cannot be empty.".into());
        }
        diesel::update(sessions::table.find(session_id))
            .set(sessions::title.eq(title))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn set_session_state(
        &mut self,
        session_id: i32,
        state: SessionState,
    ) -> Result<(), String> {
        diesel::update(sessions::table.find(session_id))
            .set(sessions::state.eq(state.as_id()))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn delete_session(&mut self, session_id: i32) -> Result<(), String> {
        self.connection
            .transaction(|connection| {
                diesel::delete(
                    a2ui_messages::table.filter(a2ui_messages::session_id.eq(session_id)),
                )
                .execute(connection)?;
                diesel::delete(messages::table.filter(messages::session_id.eq(session_id)))
                    .execute(connection)?;
                diesel::delete(sessions::table.find(session_id)).execute(connection)?;
                Ok(())
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }

    pub fn load_messages(&mut self, session_id: i32) -> Result<Vec<StoredMessage>, String> {
        messages::table
            .filter(messages::session_id.eq(session_id))
            .order(messages::id.asc())
            .select(StoredMessage::as_select())
            .load(&mut self.connection)
            .map_err(|error| error.to_string())
    }

    pub fn load_a2ui_messages(
        &mut self,
        session_id: i32,
    ) -> Result<Vec<StoredA2uiMessage>, String> {
        a2ui_messages::table
            .filter(a2ui_messages::session_id.eq(session_id))
            .order(a2ui_messages::id.asc())
            .select(StoredA2uiMessage::as_select())
            .load(&mut self.connection)
            .map_err(|error| error.to_string())
    }

    pub fn insert_a2ui_message(
        &mut self,
        session_id: i32,
        message_id: i32,
        json: &str,
    ) -> Result<StoredA2uiMessage, String> {
        diesel::insert_into(a2ui_messages::table)
            .values(NewA2uiMessage {
                session_id,
                message_id,
                json,
                dismissed: false,
            })
            .returning(StoredA2uiMessage::as_returning())
            .get_result(&mut self.connection)
            .map_err(|error| error.to_string())
    }

    pub fn dismiss_a2ui_surface(
        &mut self,
        session_id: i32,
        surface_id: &str,
    ) -> Result<StoredMessage, String> {
        let content = format!(
            "A2UI surface `{surface_id}` was dismissed by the user. There is no active A2UI surface. If the user asks for UI again, create a complete new surface with createSurface and a new surfaceId; do not update any earlier surface."
        );
        let json = serde_json::json!({"surfaceId": surface_id}).to_string();
        self.connection
            .transaction(|connection| {
                let message = diesel::insert_into(messages::table)
                    .values(NewMessage::system(session_id, &content, None))
                    .returning(StoredMessage::as_returning())
                    .get_result(connection)?;
                diesel::insert_into(a2ui_messages::table)
                    .values(NewA2uiMessage {
                        session_id,
                        message_id: message.id,
                        json: &json,
                        dismissed: true,
                    })
                    .execute(connection)?;
                Ok(message)
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }

    pub fn update_session_context(
        &mut self,
        session_id: i32,
        used: u32,
        limit: u32,
        tokens_per_second: Option<f32>,
    ) -> Result<(), String> {
        let used = i32::try_from(used).map_err(|_| "Used context is too large to save")?;
        let limit = i32::try_from(limit).map_err(|_| "Context limit is too large to save")?;
        if tokens_per_second.is_some_and(|speed| !speed.is_finite() || speed < 0.0) {
            return Err("Generation speed is invalid".into());
        }
        diesel::update(sessions::table.find(session_id))
            .set((
                sessions::context_used.eq(used),
                sessions::context_limit.eq(limit),
                sessions::last_tokens_per_second.eq(tokens_per_second),
            ))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn start_chat_turn(
        &mut self,
        session_id: i32,
        prompt: &str,
        model_prompt: Option<&str>,
        system_messages: &[SystemMessage],
        reasoning: bool,
    ) -> Result<Vec<StoredMessage>, String> {
        self.connection
            .transaction(|connection| {
                reactivate_archived_session(connection, session_id)?;
                touch_session(connection, session_id)?;
                let mut stored = Vec::with_capacity(system_messages.len() + 2);
                for message in system_messages {
                    stored.push(
                        diesel::insert_into(messages::table)
                            .values(NewMessage::system(
                                session_id,
                                &message.content,
                                message.instruction_metadata.as_deref(),
                            ))
                            .returning(StoredMessage::as_returning())
                            .get_result(connection)?,
                    );
                }
                let user = diesel::insert_into(messages::table)
                    .values(NewMessage::user(session_id, prompt, model_prompt))
                    .returning(StoredMessage::as_returning())
                    .get_result(connection)?;
                let assistant = diesel::insert_into(messages::table)
                    .values(NewMessage::assistant(session_id, reasoning))
                    .returning(StoredMessage::as_returning())
                    .get_result(connection)?;
                stored.extend([user, assistant]);
                Ok(stored)
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }

    pub fn record_tool_result(
        &mut self,
        session_id: i32,
        result: &str,
    ) -> Result<StoredMessage, String> {
        self.connection
            .transaction(|connection| {
                touch_session(connection, session_id)?;
                diesel::insert_into(messages::table)
                    .values(NewMessage::tool_result(session_id, result))
                    .returning(StoredMessage::as_returning())
                    .get_result(connection)
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }

    pub fn record_system_messages(
        &mut self,
        session_id: i32,
        system_messages: &[SystemMessage],
    ) -> Result<Vec<StoredMessage>, String> {
        self.connection
            .transaction(|connection| {
                touch_session(connection, session_id)?;
                system_messages
                    .iter()
                    .map(|message| {
                        diesel::insert_into(messages::table)
                            .values(NewMessage::system(
                                session_id,
                                &message.content,
                                message.instruction_metadata.as_deref(),
                            ))
                            .returning(StoredMessage::as_returning())
                            .get_result(connection)
                    })
                    .collect()
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }

    pub fn continue_durable_tool_turn(
        &mut self,
        session_id: i32,
        queued_user: Option<&str>,
        system_messages: &[SystemMessage],
        reasoning: bool,
    ) -> Result<Vec<StoredMessage>, String> {
        self.connection
            .transaction(|connection| {
                touch_session(connection, session_id)?;
                let mut stored = Vec::with_capacity(system_messages.len() + 2);
                if let Some(content) = queued_user {
                    stored.push(
                        diesel::insert_into(messages::table)
                            .values(NewMessage::user(session_id, content, None))
                            .returning(StoredMessage::as_returning())
                            .get_result(connection)?,
                    );
                }
                for message in system_messages {
                    stored.push(
                        diesel::insert_into(messages::table)
                            .values(NewMessage::system(
                                session_id,
                                &message.content,
                                message.instruction_metadata.as_deref(),
                            ))
                            .returning(StoredMessage::as_returning())
                            .get_result(connection)?,
                    );
                }
                stored.push(
                    diesel::insert_into(messages::table)
                        .values(NewMessage::assistant(session_id, reasoning))
                        .returning(StoredMessage::as_returning())
                        .get_result(connection)?,
                );
                Ok(stored)
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }

    pub fn update_message(
        &mut self,
        id: i32,
        reasoning: Option<&str>,
        reasoning_complete: bool,
        content: &str,
    ) -> Result<(), String> {
        diesel::update(messages::table.find(id))
            .set((
                messages::reasoning.eq(reasoning),
                messages::reasoning_complete.eq(reasoning_complete),
                messages::content.eq(content),
            ))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn update_message_tool_approval_reasons(
        &mut self,
        id: i32,
        reasons: &[Option<String>],
    ) -> Result<(), String> {
        let reasons = serde_json::to_string(reasons).map_err(|error| error.to_string())?;
        diesel::update(messages::table.find(id))
            .set(messages::tool_approval_reasons.eq(reasons))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn update_message_generation_stats(
        &mut self,
        id: i32,
        duration_ms: u64,
        input_tokens: u32,
        cached_tokens: u32,
        output_tokens: u32,
    ) -> Result<(), String> {
        let duration_ms =
            i32::try_from(duration_ms).map_err(|_| "Generation duration is too large to save")?;
        let input_tokens =
            i32::try_from(input_tokens).map_err(|_| "Input token count is too large to save")?;
        let cached_tokens =
            i32::try_from(cached_tokens).map_err(|_| "Cached token count is too large to save")?;
        let output_tokens =
            i32::try_from(output_tokens).map_err(|_| "Output token count is too large to save")?;
        diesel::update(messages::table.find(id))
            .set((
                messages::generation_duration_ms.eq(duration_ms),
                messages::input_tokens.eq(input_tokens),
                messages::cached_tokens.eq(cached_tokens),
                messages::output_tokens.eq(output_tokens),
            ))
            .execute(&mut self.connection)
            .map(|_| ())
            .map_err(|error| error.to_string())
    }

    pub fn record_compaction(
        &mut self,
        session_id: i32,
        summary: &str,
        tail_start: Option<i32>,
        running_jobs: Option<&str>,
        context_used: u32,
        context_limit: u32,
    ) -> Result<Vec<StoredMessage>, String> {
        let context_used = i32::try_from(context_used)
            .map_err(|_| "Compacted context is too large to save".to_owned())?;
        let context_limit = i32::try_from(context_limit)
            .map_err(|_| "Context limit is too large to save".to_owned())?;
        self.connection
            .transaction(|connection| {
                reactivate_archived_session(connection, session_id)?;
                diesel::update(sessions::table.find(session_id))
                    .set((
                        sessions::compacted_summary.eq(Some(summary)),
                        sessions::context_used.eq(context_used),
                        sessions::context_limit.eq(context_limit),
                        sessions::last_tokens_per_second.eq(None::<f32>),
                    ))
                    .execute(connection)?;
                let mut stored = Vec::with_capacity(2);
                stored.push(
                    diesel::insert_into(messages::table)
                        .values(NewMessage::compaction(session_id, summary, tail_start))
                        .returning(StoredMessage::as_returning())
                        .get_result(connection)?,
                );
                if let Some(content) = running_jobs {
                    stored.push(
                        diesel::insert_into(messages::table)
                            .values(NewMessage::tool_result(session_id, content))
                            .returning(StoredMessage::as_returning())
                            .get_result(connection)?,
                    );
                }
                Ok(stored)
            })
            .map_err(|error: diesel::result::Error| error.to_string())
    }
}

fn touch_session(
    connection: &mut SqliteConnection,
    session_id: i32,
) -> Result<(), diesel::result::Error> {
    diesel::update(sessions::table.find(session_id))
        .set(sessions::last_used.eq(OffsetDateTime::now_utc().unix_timestamp()))
        .execute(connection)
        .map(|_| ())
}

fn reactivate_archived_session(
    connection: &mut SqliteConnection,
    session_id: i32,
) -> Result<(), diesel::result::Error> {
    diesel::update(
        sessions::table
            .find(session_id)
            .filter(sessions::state.eq(SessionState::Archived.as_id())),
    )
    .set(sessions::state.eq(SessionState::Normal.as_id()))
    .execute(connection)
    .map(|_| ())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn migrations_and_crud_work() {
        let id = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("ds4-server-{id}.sqlite3"));
        let mut database = Database::open(&path).unwrap();

        let project = database.create_project("DS4", "/tmp/ds4").unwrap();
        let first = database
            .create_session(project.id, "First session", PermissionMode::Heuristic)
            .unwrap();
        let second = database
            .create_session(project.id, "Second session", PermissionMode::Ai)
            .unwrap();
        database.delete_session(first.id).unwrap();
        let loaded = database.load_projects().unwrap();
        assert_eq!(loaded[0].sessions[0].title, "Second session");
        assert_eq!(loaded[0].sessions[0].state(), SessionState::Normal);
        assert_eq!(loaded[0].sessions[0].permission_mode(), PermissionMode::Ai);
        database
            .set_session_permission_mode(second.id, PermissionMode::Heuristic)
            .unwrap();
        assert_eq!(
            database.load_projects().unwrap()[0].sessions[0].permission_mode(),
            PermissionMode::Heuristic
        );

        let ordinary = loaded[0].sessions[0].id;
        let pinned = database
            .create_session(project.id, "Pinned", PermissionMode::Heuristic)
            .unwrap();
        let archived = database
            .create_session(project.id, "Archived", PermissionMode::Heuristic)
            .unwrap();
        database
            .set_session_state(pinned.id, SessionState::Pinned)
            .unwrap();
        database
            .set_session_state(archived.id, SessionState::Archived)
            .unwrap();
        database.rename_session(pinned.id, "  Renamed  ").unwrap();
        assert!(database.rename_session(pinned.id, "   ").is_err());
        let loaded = database.load_projects().unwrap();
        assert_eq!(
            loaded[0]
                .sessions
                .iter()
                .map(|session| (session.id, session.state()))
                .collect::<Vec<_>>(),
            [
                (pinned.id, SessionState::Pinned),
                (ordinary, SessionState::Normal),
                (archived.id, SessionState::Archived),
            ]
        );
        assert_eq!(loaded[0].sessions[0].title, "Renamed");
        // Archiving a pinned session moves it out of the pinned group entirely.
        database
            .set_session_state(pinned.id, SessionState::Archived)
            .unwrap();
        let loaded = database.load_projects().unwrap();
        assert_eq!(loaded[0].sessions[0].id, ordinary);

        assert!(!loaded[0].project.collapsed);
        database.set_project_collapsed(project.id, true).unwrap();
        assert!(database.load_projects().unwrap()[0].project.collapsed);

        database.delete_project(project.id).unwrap();
        assert!(database.load_projects().unwrap().is_empty());
        drop(database);

        let mut reopened = Database::open(&path).unwrap();
        assert!(reopened.load_projects().unwrap().is_empty());
        drop(reopened);
        fs::remove_file(path).unwrap();
    }

    #[test]
    fn sessions_are_ordered_by_recent_use_within_sidebar_groups() {
        let id = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("ds4-session-order-{id}.sqlite3"));
        let mut database = Database::open(&path).unwrap();
        let project = database
            .create_project("DS4", "/tmp/ds4-session-order")
            .unwrap();
        let older = database
            .create_session(project.id, "Older", PermissionMode::Heuristic)
            .unwrap();
        let newer = database
            .create_session(project.id, "Newer", PermissionMode::Heuristic)
            .unwrap();
        let pinned = database
            .create_session(project.id, "Pinned", PermissionMode::Heuristic)
            .unwrap();
        database
            .set_session_state(pinned.id, SessionState::Pinned)
            .unwrap();
        diesel::update(sessions::table.find(older.id))
            .set(sessions::last_used.eq(100_i64))
            .execute(&mut database.connection)
            .unwrap();
        diesel::update(sessions::table.find(newer.id))
            .set(sessions::last_used.eq(200_i64))
            .execute(&mut database.connection)
            .unwrap();

        let loaded = database.load_projects().unwrap();
        assert_eq!(
            loaded[0]
                .sessions
                .iter()
                .map(|session| session.id)
                .collect::<Vec<_>>(),
            [pinned.id, newer.id, older.id]
        );

        drop(database);
        fs::remove_file(path).unwrap();
    }

    #[test]
    fn context_work_reactivates_an_archived_session() {
        let id = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("ds4-reactivate-{id}.sqlite3"));
        let mut database = Database::open(&path).unwrap();
        let project = database
            .create_project("DS4", "/tmp/ds4-reactivate")
            .unwrap();
        let session = database
            .create_session(project.id, "Archived", PermissionMode::Heuristic)
            .unwrap();
        database
            .set_session_state(session.id, SessionState::Archived)
            .unwrap();

        database
            .start_chat_turn(session.id, "Resume", None, &[], false)
            .unwrap();

        assert_eq!(
            database.load_projects().unwrap()[0].sessions[0].state(),
            SessionState::Normal
        );
        database
            .set_session_state(session.id, SessionState::Archived)
            .unwrap();
        database
            .record_compaction(session.id, "Summary", None, None, 100, 1_000)
            .unwrap();
        assert_eq!(
            database.load_projects().unwrap()[0].sessions[0].state(),
            SessionState::Normal
        );
        drop(database);
        fs::remove_file(path).unwrap();
    }

    #[test]
    fn a2ui_dismissal_persists_a_fresh_surface_boundary() {
        let id = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("ds4-a2ui-dismiss-{id}.sqlite3"));
        let mut database = Database::open(&path).unwrap();
        let project = database
            .create_project("DS4", "/tmp/ds4-a2ui-dismiss")
            .unwrap();
        let session = database
            .create_session(project.id, "A2UI", PermissionMode::Heuristic)
            .unwrap();
        let first = database
            .start_chat_turn(session.id, "First", None, &[], false)
            .unwrap()
            .pop()
            .unwrap();
        database
            .insert_a2ui_message(
                session.id,
                first.id,
                r#"{"version":"v1.0","createSurface":{"surfaceId":"first","catalogId":"https://ds4server.local/a2ui/v1_0/catalog.json"}}"#,
            )
            .unwrap();
        let dismissal = database.dismiss_a2ui_surface(session.id, "first").unwrap();
        assert!(dismissal.system);
        assert!(dismissal.content.contains("createSurface"));
        let protocol = database.load_a2ui_messages(session.id).unwrap();
        let (_, active, errors) = crate::a2ui::replay_epochs(protocol.iter().map(|message| {
            (
                message.id,
                message.message_id,
                message.dismissed,
                message.json.as_str(),
            )
        }));
        assert!(errors.is_empty());
        assert!(active.active_surface().is_none());

        let second = database
            .start_chat_turn(session.id, "Second", None, &[], false)
            .unwrap()
            .pop()
            .unwrap();
        database
            .insert_a2ui_message(
                session.id,
                second.id,
                r#"{"version":"v1.0","createSurface":{"surfaceId":"second","catalogId":"https://ds4server.local/a2ui/v1_0/catalog.json"}}"#,
            )
            .unwrap();
        drop(database);

        let mut reopened = Database::open(&path).unwrap();
        let protocol = reopened.load_a2ui_messages(session.id).unwrap();
        assert_eq!(protocol.len(), 3);
        assert!(protocol[1].dismissed);
        assert_eq!(protocol[1].message_id, dismissal.id);
        let (_, active, errors) = crate::a2ui::replay_epochs(protocol.iter().map(|message| {
            (
                message.id,
                message.message_id,
                message.dismissed,
                message.json.as_str(),
            )
        }));
        assert!(errors.is_empty());
        assert_eq!(active.active_surface().unwrap().id, "second");
        let messages = reopened.load_messages(session.id).unwrap();
        assert!(
            messages
                .iter()
                .position(|message| message.id == dismissal.id)
                .is_some_and(|index| messages[index + 1].content == "Second")
        );
        reopened.delete_project(project.id).unwrap();
        drop(reopened);
        fs::remove_file(path).unwrap();
    }

    #[test]
    fn chat_and_compaction_history_survive_reopen_and_session_deletion() {
        let id = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("ds4-chat-{id}.sqlite3"));
        let mut database = Database::open(&path).unwrap();
        let project = database.create_project("DS4", "/tmp/ds4-chat").unwrap();
        let session = database
            .create_session(project.id, "Chat", PermissionMode::Heuristic)
            .unwrap();
        let mut opening = database
            .start_chat_turn(
                session.id,
                "Question",
                Some("Question\n\nhidden metadata"),
                &[SystemMessage::instruction(
                    "Date context".into(),
                    r#"{"kind":"baseline"}"#.into(),
                )],
                true,
            )
            .unwrap();
        let assistant = opening.pop().unwrap();
        database
            .update_message(assistant.id, Some("Reasoning"), true, "Answer")
            .unwrap();
        database
            .update_message_tool_approval_reasons(
                assistant.id,
                &[Some("Reads repository metadata only.".into())],
            )
            .unwrap();
        database
            .update_message_generation_stats(assistant.id, 12_345, 1_024, 768, 256)
            .unwrap();
        database
            .insert_a2ui_message(
                session.id,
                assistant.id,
                r#"{"version":"v1.0","createSurface":{"surfaceId":"saved","catalogId":"https://ds4server.local/a2ui/v1_0/catalog.json"}}"#,
            )
            .unwrap();
        database
            .record_tool_result(session.id, "Tool result")
            .unwrap();
        assert!(
            database
                .load_messages(session.id)
                .unwrap()
                .last()
                .unwrap()
                .tool
        );
        database
            .record_system_messages(
                session.id,
                &[SystemMessage::instruction(
                    "Tool reminder".into(),
                    r#"{"kind":"change"}"#.into(),
                )],
            )
            .unwrap();
        database
            .continue_durable_tool_turn(session.id, Some("Queued correction"), &[], false)
            .unwrap();
        database
            .update_session_context(session.id, 1_234, 65_536, Some(12.5))
            .unwrap();
        drop(database);

        let mut reopened = Database::open(&path).unwrap();
        let projects = reopened.load_projects().unwrap();
        assert_eq!(projects[0].sessions[0].context_used, 1_234);
        assert_eq!(projects[0].sessions[0].context_limit, 65_536);
        assert_eq!(projects[0].sessions[0].last_tokens_per_second, Some(12.5));
        let messages = reopened.load_messages(session.id).unwrap();
        assert_eq!(messages.len(), 7);
        assert!(messages[0].system);
        assert_eq!(
            messages[0].instruction_metadata.as_deref(),
            Some(r#"{"kind":"baseline"}"#)
        );
        assert_eq!(messages[1].content, "Question");
        assert_eq!(
            messages[1].model_content.as_deref(),
            Some("Question\n\nhidden metadata")
        );
        assert_eq!(messages[2].reasoning.as_deref(), Some("Reasoning"));
        assert!(messages[2].reasoning_complete);
        assert_eq!(messages[2].content, "Answer");
        assert_eq!(
            messages[2].tool_approval_reasons.as_deref(),
            Some(r#"["Reads repository metadata only."]"#)
        );
        assert_eq!(messages[2].generation_duration_ms, Some(12_345));
        assert_eq!(messages[2].input_tokens, Some(1_024));
        assert_eq!(messages[2].cached_tokens, Some(768));
        assert_eq!(messages[2].output_tokens, Some(256));
        assert!(messages[3].tool);
        assert_eq!(messages[3].content, "Tool result");
        assert!(messages[4].system);
        assert_eq!(messages[4].content, "Tool reminder");
        assert_eq!(
            messages[4].instruction_metadata.as_deref(),
            Some(r#"{"kind":"change"}"#)
        );
        assert!(messages[5].user);
        assert_eq!(messages[5].content, "Queued correction");
        assert!(!messages[6].user);
        assert!(!messages[6].tool);
        let a2ui = reopened.load_a2ui_messages(session.id).unwrap();
        assert_eq!(a2ui.len(), 1);
        assert_eq!(a2ui[0].message_id, messages[2].id);
        assert!(a2ui[0].json.contains("createSurface"));
        let first = reopened
            .record_compaction(
                session.id,
                "First durable state.",
                Some(messages[5].id),
                Some("bash job=1 status=running"),
                321,
                32_768,
            )
            .unwrap();
        assert_eq!(first.len(), 2);
        assert!(first[0].compaction);
        assert_eq!(first[0].compaction_tail_start, Some(messages[5].id));
        drop(reopened);
        let mut reopened = Database::open(&path).unwrap();
        assert_eq!(
            reopened.load_projects().unwrap()[0].sessions[0]
                .compacted_summary
                .as_deref(),
            Some("First durable state.")
        );
        assert_eq!(
            reopened.load_projects().unwrap()[0].sessions[0].context_used,
            321
        );
        let messages = reopened.load_messages(session.id).unwrap();
        assert_eq!(messages.len(), 9);
        assert_eq!(messages[1].content, "Question");
        assert!(messages[7].compaction);
        assert_eq!(messages[7].content, "First durable state.");
        assert_eq!(messages[8].content, "bash job=1 status=running");
        reopened
            .record_tool_result(session.id, "Reloaded tool result")
            .unwrap();
        reopened
            .continue_durable_tool_turn(session.id, None, &[], false)
            .unwrap();
        let continued = reopened.load_messages(session.id).unwrap();
        let second_tail = continued[9].id;
        reopened
            .record_compaction(
                session.id,
                "Second durable state.",
                Some(second_tail),
                None,
                222,
                32_768,
            )
            .unwrap();
        reopened
            .record_tool_result(session.id, "Final tool result")
            .unwrap();
        reopened
            .continue_durable_tool_turn(session.id, None, &[], false)
            .unwrap();
        let continued = reopened.load_messages(session.id).unwrap();
        let third_tail = continued[12].id;
        reopened
            .record_compaction(
                session.id,
                "Third durable state.",
                Some(third_tail),
                None,
                111,
                32_768,
            )
            .unwrap();
        reopened
            .start_chat_turn(session.id, "After third compaction", None, &[], false)
            .unwrap();
        drop(reopened);

        let mut reopened = Database::open(&path).unwrap();
        let history = reopened.load_messages(session.id).unwrap();
        assert_eq!(history.len(), 17);
        assert_eq!(history[1].content, "Question");
        let markers = history
            .iter()
            .filter(|message| message.compaction)
            .collect::<Vec<_>>();
        assert_eq!(markers.len(), 3);
        assert_eq!(markers[0].content, "First durable state.");
        assert_eq!(markers[1].content, "Second durable state.");
        assert_eq!(markers[2].content, "Third durable state.");
        assert_eq!(markers[2].compaction_tail_start, Some(third_tail));
        assert_eq!(history[15].content, "After third compaction");
        reopened.delete_session(session.id).unwrap();
        assert!(reopened.load_messages(session.id).unwrap().is_empty());
        assert!(reopened.load_a2ui_messages(session.id).unwrap().is_empty());
        drop(reopened);
        fs::remove_file(path).unwrap();
    }
}
