pub mod model;

use std::{path::Path, str::FromStr};

use anyhow::{anyhow, bail, Context, Result};
use client::DevServerProjectId;
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId};

use language::{LanguageName, Toolchain};
use project::WorktreeId;
use remote::ssh_session::SshProjectId;
use sqlez::{
    bindable::{Bind, Column, StaticColumnCount},
    statement::Statement,
};

use ui::px;
use util::{maybe, ResultExt};
use uuid::Uuid;

use crate::WorkspaceId;

use model::{
    GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
    SerializedSshProject, SerializedWorkspace,
};

use self::model::{DockStructure, LocalPathsOrder, SerializedWorkspaceLocation};

#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
impl sqlez::bindable::Bind for SerializedAxis {
    fn bind(
        &self,
        statement: &sqlez::statement::Statement,
        start_index: i32,
    ) -> anyhow::Result<i32> {
        match self.0 {
            gpui::Axis::Horizontal => "Horizontal",
            gpui::Axis::Vertical => "Vertical",
        }
        .bind(statement, start_index)
    }
}

impl sqlez::bindable::Column for SerializedAxis {
    fn column(
        statement: &mut sqlez::statement::Statement,
        start_index: i32,
    ) -> anyhow::Result<(Self, i32)> {
        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
            Ok((
                match axis_text.as_str() {
                    "Horizontal" => Self(Axis::Horizontal),
                    "Vertical" => Self(Axis::Vertical),
                    _ => anyhow::bail!("Stored serialized item kind is incorrect"),
                },
                next_index,
            ))
        })
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);

impl StaticColumnCount for SerializedWindowBounds {
    fn column_count() -> usize {
        5
    }
}

impl Bind for SerializedWindowBounds {
    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
        match self.0 {
            WindowBounds::Windowed(bounds) => {
                let next_index = statement.bind(&"Windowed", start_index)?;
                statement.bind(
                    &(
                        SerializedPixels(bounds.origin.x),
                        SerializedPixels(bounds.origin.y),
                        SerializedPixels(bounds.size.width),
                        SerializedPixels(bounds.size.height),
                    ),
                    next_index,
                )
            }
            WindowBounds::Maximized(bounds) => {
                let next_index = statement.bind(&"Maximized", start_index)?;
                statement.bind(
                    &(
                        SerializedPixels(bounds.origin.x),
                        SerializedPixels(bounds.origin.y),
                        SerializedPixels(bounds.size.width),
                        SerializedPixels(bounds.size.height),
                    ),
                    next_index,
                )
            }
            WindowBounds::Fullscreen(bounds) => {
                let next_index = statement.bind(&"FullScreen", start_index)?;
                statement.bind(
                    &(
                        SerializedPixels(bounds.origin.x),
                        SerializedPixels(bounds.origin.y),
                        SerializedPixels(bounds.size.width),
                        SerializedPixels(bounds.size.height),
                    ),
                    next_index,
                )
            }
        }
    }
}

impl Column for SerializedWindowBounds {
    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
        let (window_state, next_index) = String::column(statement, start_index)?;
        let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
            Column::column(statement, next_index)?;
        let bounds = Bounds {
            origin: point(px(x as f32), px(y as f32)),
            size: size(px(width as f32), px(height as f32)),
        };

        let status = match window_state.as_str() {
            "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
            "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
            "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
            _ => bail!("Window State did not have a valid string"),
        };

        Ok((status, next_index + 4))
    }
}

#[derive(Clone, Debug, PartialEq)]
struct SerializedPixels(gpui::Pixels);
impl sqlez::bindable::StaticColumnCount for SerializedPixels {}

impl sqlez::bindable::Bind for SerializedPixels {
    fn bind(
        &self,
        statement: &sqlez::statement::Statement,
        start_index: i32,
    ) -> anyhow::Result<i32> {
        let this: i32 = self.0 .0 as i32;
        this.bind(statement, start_index)
    }
}

define_connection! {
    // Current schema shape using pseudo-rust syntax:
    //
    // workspaces(
    //   workspace_id: usize, // Primary key for workspaces
    //   local_paths: Bincode<Vec<PathBuf>>,
    //   local_paths_order: Bincode<Vec<usize>>,
    //   dock_visible: bool, // Deprecated
    //   dock_anchor: DockAnchor, // Deprecated
    //   dock_pane: Option<usize>, // Deprecated
    //   left_sidebar_open: boolean,
    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
    //   window_state: String, // WindowBounds Discriminant
    //   window_x: Option<f32>, // WindowBounds::Fixed RectF x
    //   window_y: Option<f32>, // WindowBounds::Fixed RectF y
    //   window_width: Option<f32>, // WindowBounds::Fixed RectF width
    //   window_height: Option<f32>, // WindowBounds::Fixed RectF height
    //   display: Option<Uuid>, // Display id
    //   fullscreen: Option<bool>, // Is the window fullscreen?
    //   centered_layout: Option<bool>, // Is the Centered Layout mode activated?
    //   session_id: Option<String>, // Session id
    //   window_id: Option<u64>, // Window Id
    // )
    //
    // pane_groups(
    //   group_id: usize, // Primary key for pane_groups
    //   workspace_id: usize, // References workspaces table
    //   parent_group_id: Option<usize>, // None indicates that this is the root node
    //   position: Option<usize>, // None indicates that this is the root node
    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
    //   flexes: Option<Vec<f32>>, // A JSON array of floats
    // )
    //
    // panes(
    //     pane_id: usize, // Primary key for panes
    //     workspace_id: usize, // References workspaces table
    //     active: bool,
    // )
    //
    // center_panes(
    //     pane_id: usize, // Primary key for center_panes
    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
    //     position: Option<usize>, // None indicates this is the root
    // )
    //
    // CREATE TABLE items(
    //     item_id: usize, // This is the item's view id, so this is not unique
    //     workspace_id: usize, // References workspaces table
    //     pane_id: usize, // References panes table
    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
    //     active: bool, // Indicates if this item is the active one in the pane
    //     preview: bool // Indicates if this item is a preview item
    // )
    pub static ref DB: WorkspaceDb<()> =
    &[
        sql!(
        CREATE TABLE workspaces(
            workspace_id INTEGER PRIMARY KEY,
            workspace_location BLOB UNIQUE,
            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
            left_sidebar_open INTEGER, // Boolean
            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
        ) STRICT;

        CREATE TABLE pane_groups(
            group_id INTEGER PRIMARY KEY,
            workspace_id INTEGER NOT NULL,
            parent_group_id INTEGER, // NULL indicates that this is a root node
            position INTEGER, // NULL indicates that this is a root node
            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
            ON DELETE CASCADE
            ON UPDATE CASCADE,
            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
        ) STRICT;

        CREATE TABLE panes(
            pane_id INTEGER PRIMARY KEY,
            workspace_id INTEGER NOT NULL,
            active INTEGER NOT NULL, // Boolean
            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
            ON DELETE CASCADE
            ON UPDATE CASCADE
        ) STRICT;

        CREATE TABLE center_panes(
            pane_id INTEGER PRIMARY KEY,
            parent_group_id INTEGER, // NULL means that this is a root pane
            position INTEGER, // NULL means that this is a root pane
            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
            ON DELETE CASCADE,
            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
        ) STRICT;

        CREATE TABLE items(
            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
            workspace_id INTEGER NOT NULL,
            pane_id INTEGER NOT NULL,
            kind TEXT NOT NULL,
            position INTEGER NOT NULL,
            active INTEGER NOT NULL,
            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
            ON DELETE CASCADE
            ON UPDATE CASCADE,
            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
            ON DELETE CASCADE,
            PRIMARY KEY(item_id, workspace_id)
        ) STRICT;
    ),
    sql!(
        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
        ALTER TABLE workspaces ADD COLUMN window_x REAL;
        ALTER TABLE workspaces ADD COLUMN window_y REAL;
        ALTER TABLE workspaces ADD COLUMN window_width REAL;
        ALTER TABLE workspaces ADD COLUMN window_height REAL;
        ALTER TABLE workspaces ADD COLUMN display BLOB;
    ),
    // Drop foreign key constraint from workspaces.dock_pane to panes table.
    sql!(
        CREATE TABLE workspaces_2(
            workspace_id INTEGER PRIMARY KEY,
            workspace_location BLOB UNIQUE,
            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
            left_sidebar_open INTEGER, // Boolean
            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
            window_state TEXT,
            window_x REAL,
            window_y REAL,
            window_width REAL,
            window_height REAL,
            display BLOB
        ) STRICT;
        INSERT INTO workspaces_2 SELECT * FROM workspaces;
        DROP TABLE workspaces;
        ALTER TABLE workspaces_2 RENAME TO workspaces;
    ),
    // Add panels related information
    sql!(
        ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
        ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
        ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
        ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
        ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
        ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
    ),
    // Add panel zoom persistence
    sql!(
        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
    ),
    // Add pane group flex data
    sql!(
        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
    ),
    // Add fullscreen field to workspace
    // Deprecated, `WindowBounds` holds the fullscreen state now.
    // Preserving so users can downgrade Zed.
    sql!(
        ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
    ),
    // Add preview field to items
    sql!(
        ALTER TABLE items ADD COLUMN preview INTEGER; //bool
    ),
    // Add centered_layout field to workspace
    sql!(
        ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
    ),
    sql!(
        CREATE TABLE remote_projects (
            remote_project_id INTEGER NOT NULL UNIQUE,
            path TEXT,
            dev_server_name TEXT
        );
        ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
        ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
    ),
    sql!(
        DROP TABLE remote_projects;
        CREATE TABLE dev_server_projects (
            id INTEGER NOT NULL UNIQUE,
            path TEXT,
            dev_server_name TEXT
        );
        ALTER TABLE workspaces DROP COLUMN remote_project_id;
        ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
    ),
    sql!(
        ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
    ),
    sql!(
        ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
    ),
    sql!(
        ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
    ),
    sql!(
        ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
    ),
    sql!(
        CREATE TABLE ssh_projects (
            id INTEGER PRIMARY KEY,
            host TEXT NOT NULL,
            port INTEGER,
            path TEXT NOT NULL,
            user TEXT
        );
        ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
    ),
    sql!(
        ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
    ),
    sql!(
        CREATE TABLE toolchains (
            workspace_id INTEGER,
            worktree_id INTEGER,
            language_name TEXT NOT NULL,
            name TEXT NOT NULL,
            path TEXT NOT NULL,
            PRIMARY KEY (workspace_id, worktree_id, language_name)
        );
    ),
    sql!(
        ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
    ),
    ];
}

impl WorkspaceDb {
    /// Returns a serialized workspace for the given worktree_roots. If the passed array
    /// is empty, the most recent workspace is returned instead. If no workspace for the
    /// passed roots is stored, returns none.
    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
        &self,
        worktree_roots: &[P],
    ) -> Option<SerializedWorkspace> {
        // paths are sorted before db interactions to ensure that the order of the paths
        // doesn't affect the workspace selection for existing workspaces
        let local_paths = LocalPaths::new(worktree_roots);

        // Note that we re-assign the workspace_id here in case it's empty
        // and we've grabbed the most recent workspace
        let (
            workspace_id,
            local_paths,
            local_paths_order,
            window_bounds,
            display,
            centered_layout,
            docks,
            window_id,
        ): (
            WorkspaceId,
            Option<LocalPaths>,
            Option<LocalPathsOrder>,
            Option<SerializedWindowBounds>,
            Option<Uuid>,
            Option<bool>,
            DockStructure,
            Option<u64>,
        ) = self
            .select_row_bound(sql! {
                SELECT
                    workspace_id,
                    local_paths,
                    local_paths_order,
                    window_state,
                    window_x,
                    window_y,
                    window_width,
                    window_height,
                    display,
                    centered_layout,
                    left_dock_visible,
                    left_dock_active_panel,
                    left_dock_zoom,
                    right_dock_visible,
                    right_dock_active_panel,
                    right_dock_zoom,
                    bottom_dock_visible,
                    bottom_dock_active_panel,
                    bottom_dock_zoom,
                    window_id
                FROM workspaces
                WHERE local_paths = ?
            })
            .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
            .context("No workspaces found")
            .warn_on_err()
            .flatten()?;

        let local_paths = local_paths?;
        let location = match local_paths_order {
            Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
            None => {
                let order = LocalPathsOrder::default_for_paths(&local_paths);
                SerializedWorkspaceLocation::Local(local_paths, order)
            }
        };

        Some(SerializedWorkspace {
            id: workspace_id,
            location,
            center_group: self
                .get_center_pane_group(workspace_id)
                .context("Getting center group")
                .log_err()?,
            window_bounds,
            centered_layout: centered_layout.unwrap_or(false),
            display,
            docks,
            session_id: None,
            window_id,
        })
    }

    pub(crate) fn workspace_for_ssh_project(
        &self,
        ssh_project: &SerializedSshProject,
    ) -> Option<SerializedWorkspace> {
        let (workspace_id, window_bounds, display, centered_layout, docks, window_id): (
            WorkspaceId,
            Option<SerializedWindowBounds>,
            Option<Uuid>,
            Option<bool>,
            DockStructure,
            Option<u64>,
        ) = self
            .select_row_bound(sql! {
                SELECT
                    workspace_id,
                    window_state,
                    window_x,
                    window_y,
                    window_width,
                    window_height,
                    display,
                    centered_layout,
                    left_dock_visible,
                    left_dock_active_panel,
                    left_dock_zoom,
                    right_dock_visible,
                    right_dock_active_panel,
                    right_dock_zoom,
                    bottom_dock_visible,
                    bottom_dock_active_panel,
                    bottom_dock_zoom,
                    window_id
                FROM workspaces
                WHERE ssh_project_id = ?
            })
            .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0))
            .context("No workspaces found")
            .warn_on_err()
            .flatten()?;

        Some(SerializedWorkspace {
            id: workspace_id,
            location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
            center_group: self
                .get_center_pane_group(workspace_id)
                .context("Getting center group")
                .log_err()?,
            window_bounds,
            centered_layout: centered_layout.unwrap_or(false),
            display,
            docks,
            session_id: None,
            window_id,
        })
    }

    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
    /// that used this workspace previously
    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
        self.write(move |conn| {
            conn.with_savepoint("update_worktrees", || {
                // Clear out panes and pane_groups
                conn.exec_bound(sql!(
                    DELETE FROM pane_groups WHERE workspace_id = ?1;
                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
                .context("Clearing old panes")?;

                match workspace.location {
                    SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
                        conn.exec_bound(sql!(
                            DELETE FROM toolchains WHERE workspace_id = ?1;
                            DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
                        ))?((&local_paths, workspace.id))
                        .context("clearing out old locations")?;

                        // Upsert
                        let query = sql!(
                            INSERT INTO workspaces(
                                workspace_id,
                                local_paths,
                                local_paths_order,
                                left_dock_visible,
                                left_dock_active_panel,
                                left_dock_zoom,
                                right_dock_visible,
                                right_dock_active_panel,
                                right_dock_zoom,
                                bottom_dock_visible,
                                bottom_dock_active_panel,
                                bottom_dock_zoom,
                                session_id,
                                window_id,
                                timestamp
                            )
                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP)
                            ON CONFLICT DO
                            UPDATE SET
                                local_paths = ?2,
                                local_paths_order = ?3,
                                left_dock_visible = ?4,
                                left_dock_active_panel = ?5,
                                left_dock_zoom = ?6,
                                right_dock_visible = ?7,
                                right_dock_active_panel = ?8,
                                right_dock_zoom = ?9,
                                bottom_dock_visible = ?10,
                                bottom_dock_active_panel = ?11,
                                bottom_dock_zoom = ?12,
                                session_id = ?13,
                                window_id = ?14,
                                timestamp = CURRENT_TIMESTAMP
                        );
                        let mut prepared_query = conn.exec_bound(query)?;
                        let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id);

                        prepared_query(args).context("Updating workspace")?;
                    }
                    SerializedWorkspaceLocation::Ssh(ssh_project) => {
                        conn.exec_bound(sql!(
                            DELETE FROM toolchains WHERE workspace_id = ?1;
                            DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ?
                        ))?((ssh_project.id.0, workspace.id))
                        .context("clearing out old locations")?;

                        // Upsert
                        conn.exec_bound(sql!(
                            INSERT INTO workspaces(
                                workspace_id,
                                ssh_project_id,
                                left_dock_visible,
                                left_dock_active_panel,
                                left_dock_zoom,
                                right_dock_visible,
                                right_dock_active_panel,
                                right_dock_zoom,
                                bottom_dock_visible,
                                bottom_dock_active_panel,
                                bottom_dock_zoom,
                                session_id,
                                window_id,
                                timestamp
                            )
                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP)
                            ON CONFLICT DO
                            UPDATE SET
                                ssh_project_id = ?2,
                                left_dock_visible = ?3,
                                left_dock_active_panel = ?4,
                                left_dock_zoom = ?5,
                                right_dock_visible = ?6,
                                right_dock_active_panel = ?7,
                                right_dock_zoom = ?8,
                                bottom_dock_visible = ?9,
                                bottom_dock_active_panel = ?10,
                                bottom_dock_zoom = ?11,
                                session_id = ?12,
                                window_id = ?13,
                                timestamp = CURRENT_TIMESTAMP
                        ))?((
                            workspace.id,
                            ssh_project.id.0,
                            workspace.docks,
                            workspace.session_id,
                            workspace.window_id
                        ))
                        .context("Updating workspace")?;
                    }
                }

                // Save center pane group
                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
                    .context("save pane group in save workspace")?;

                Ok(())
            })
            .log_err();
        })
        .await;
    }

    pub(crate) async fn get_or_create_ssh_project(
        &self,
        host: String,
        port: Option<u16>,
        paths: Vec<String>,
        user: Option<String>,
    ) -> Result<SerializedSshProject> {
        let paths = serde_json::to_string(&paths)?;
        if let Some(project) = self
            .get_ssh_project(host.clone(), port, paths.clone(), user.clone())
            .await?
        {
            Ok(project)
        } else {
            self.insert_ssh_project(host, port, paths, user)
                .await?
                .ok_or_else(|| anyhow!("failed to insert ssh project"))
        }
    }

    query! {
        async fn get_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
            SELECT id, host, port, paths, user
            FROM ssh_projects
            WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ?
            LIMIT 1
        }
    }

    query! {
        async fn insert_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
            INSERT INTO ssh_projects(
                host,
                port,
                paths,
                user
            ) VALUES (?1, ?2, ?3, ?4)
            RETURNING id, host, port, paths, user
        }
    }

    query! {
        pub async fn next_id() -> Result<WorkspaceId> {
            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
        }
    }

    query! {
        fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>)>> {
            SELECT workspace_id, local_paths, local_paths_order, ssh_project_id
            FROM workspaces
            WHERE local_paths IS NOT NULL
                OR ssh_project_id IS NOT NULL
            ORDER BY timestamp DESC
        }
    }

    query! {
        fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
            SELECT local_paths, local_paths_order, window_id, ssh_project_id
            FROM workspaces
            WHERE session_id = ?1 AND dev_server_project_id IS NULL
            ORDER BY timestamp DESC
        }
    }

    query! {
        fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
            SELECT id, host, port, paths, user
            FROM ssh_projects
        }
    }

    query! {
        fn ssh_project(id: u64) -> Result<SerializedSshProject> {
            SELECT id, host, port, paths, user
            FROM ssh_projects
            WHERE id = ?
        }
    }

    pub(crate) fn last_window(
        &self,
    ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
        let mut prepared_query =
            self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
                SELECT
                display,
                window_state, window_x, window_y, window_width, window_height
                FROM workspaces
                WHERE local_paths
                IS NOT NULL
                ORDER BY timestamp DESC
                LIMIT 1
            ))?;
        let result = prepared_query()?;
        Ok(result.into_iter().next().unwrap_or((None, None)))
    }

    query! {
        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
            DELETE FROM toolchains WHERE workspace_id = ?1;
            DELETE FROM workspaces
            WHERE workspace_id IS ?
        }
    }

    pub async fn delete_workspace_by_dev_server_project_id(
        &self,
        id: DevServerProjectId,
    ) -> Result<()> {
        self.write(move |conn| {
            conn.exec_bound(sql!(
                DELETE FROM dev_server_projects WHERE id = ?
            ))?(id.0)?;
            conn.exec_bound(sql!(
                DELETE FROM toolchains WHERE workspace_id = ?1;
                DELETE FROM workspaces
                WHERE dev_server_project_id IS ?
            ))?(id.0)
        })
        .await
    }

    // Returns the recent locations which are still valid on disk and deletes ones which no longer
    // exist.
    pub async fn recent_workspaces_on_disk(
        &self,
    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
        let mut result = Vec::new();
        let mut delete_tasks = Vec::new();
        let ssh_projects = self.ssh_projects()?;

        for (id, location, order, ssh_project_id) in self.recent_workspaces()? {
            if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
                if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
                    result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
                } else {
                    delete_tasks.push(self.delete_workspace_by_id(id));
                }
                continue;
            }

            if location.paths().iter().all(|path| path.exists())
                && location.paths().iter().any(|path| path.is_dir())
            {
                result.push((id, SerializedWorkspaceLocation::Local(location, order)));
            } else {
                delete_tasks.push(self.delete_workspace_by_id(id));
            }
        }

        futures::future::join_all(delete_tasks).await;
        Ok(result)
    }

    pub async fn last_workspace(&self) -> Result<Option<SerializedWorkspaceLocation>> {
        Ok(self
            .recent_workspaces_on_disk()
            .await?
            .into_iter()
            .next()
            .map(|(_, location)| location))
    }

    // Returns the locations of the workspaces that were still opened when the last
    // session was closed (i.e. when Zed was quit).
    // If `last_session_window_order` is provided, the returned locations are ordered
    // according to that.
    pub fn last_session_workspace_locations(
        &self,
        last_session_id: &str,
        last_session_window_stack: Option<Vec<WindowId>>,
    ) -> Result<Vec<SerializedWorkspaceLocation>> {
        let mut workspaces = Vec::new();

        for (location, order, window_id, ssh_project_id) in
            self.session_workspaces(last_session_id.to_owned())?
        {
            if let Some(ssh_project_id) = ssh_project_id {
                let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?);
                workspaces.push((location, window_id.map(WindowId::from)));
            } else if location.paths().iter().all(|path| path.exists())
                && location.paths().iter().any(|path| path.is_dir())
            {
                let location = SerializedWorkspaceLocation::Local(location, order);
                workspaces.push((location, window_id.map(WindowId::from)));
            }
        }

        if let Some(stack) = last_session_window_stack {
            workspaces.sort_by_key(|(_, window_id)| {
                window_id
                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
                    .unwrap_or(usize::MAX)
            });
        }

        Ok(workspaces
            .into_iter()
            .map(|(paths, _)| paths)
            .collect::<Vec<_>>())
    }

    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
        Ok(self
            .get_pane_group(workspace_id, None)?
            .into_iter()
            .next()
            .unwrap_or_else(|| {
                SerializedPaneGroup::Pane(SerializedPane {
                    active: true,
                    children: vec![],
                    pinned_count: 0,
                })
            }))
    }

    fn get_pane_group(
        &self,
        workspace_id: WorkspaceId,
        group_id: Option<GroupId>,
    ) -> Result<Vec<SerializedPaneGroup>> {
        type GroupKey = (Option<GroupId>, WorkspaceId);
        type GroupOrPane = (
            Option<GroupId>,
            Option<SerializedAxis>,
            Option<PaneId>,
            Option<bool>,
            Option<usize>,
            Option<String>,
        );
        self.select_bound::<GroupKey, GroupOrPane>(sql!(
            SELECT group_id, axis, pane_id, active, pinned_count, flexes
                FROM (SELECT
                        group_id,
                        axis,
                        NULL as pane_id,
                        NULL as active,
                        NULL as pinned_count,
                        position,
                        parent_group_id,
                        workspace_id,
                        flexes
                      FROM pane_groups
                    UNION
                      SELECT
                        NULL,
                        NULL,
                        center_panes.pane_id,
                        panes.active as active,
                        pinned_count,
                        position,
                        parent_group_id,
                        panes.workspace_id as workspace_id,
                        NULL
                      FROM center_panes
                      JOIN panes ON center_panes.pane_id = panes.pane_id)
                WHERE parent_group_id IS ? AND workspace_id = ?
                ORDER BY position
        ))?((group_id, workspace_id))?
        .into_iter()
        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
            if let Some((group_id, axis)) = group_id.zip(axis) {
                let flexes = flexes
                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
                    .transpose()?;

                Ok(SerializedPaneGroup::Group {
                    axis,
                    children: self.get_pane_group(workspace_id, Some(group_id))?,
                    flexes,
                })
            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
                    self.get_items(pane_id)?,
                    active,
                    pinned_count,
                )))
            } else {
                bail!("Pane Group Child was neither a pane group or a pane");
            }
        })
        // Filter out panes and pane groups which don't have any children or items
        .filter(|pane_group| match pane_group {
            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
            _ => true,
        })
        .collect::<Result<_>>()
    }

    fn save_pane_group(
        conn: &Connection,
        workspace_id: WorkspaceId,
        pane_group: &SerializedPaneGroup,
        parent: Option<(GroupId, usize)>,
    ) -> Result<()> {
        match pane_group {
            SerializedPaneGroup::Group {
                axis,
                children,
                flexes,
            } => {
                let (parent_id, position) = parent.unzip();

                let flex_string = flexes
                    .as_ref()
                    .map(|flexes| serde_json::json!(flexes).to_string());

                let group_id = conn.select_row_bound::<_, i64>(sql!(
                    INSERT INTO pane_groups(
                        workspace_id,
                        parent_group_id,
                        position,
                        axis,
                        flexes
                    )
                    VALUES (?, ?, ?, ?, ?)
                    RETURNING group_id
                ))?((
                    workspace_id,
                    parent_id,
                    position,
                    *axis,
                    flex_string,
                ))?
                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;

                for (position, group) in children.iter().enumerate() {
                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
                }

                Ok(())
            }
            SerializedPaneGroup::Pane(pane) => {
                Self::save_pane(conn, workspace_id, pane, parent)?;
                Ok(())
            }
        }
    }

    fn save_pane(
        conn: &Connection,
        workspace_id: WorkspaceId,
        pane: &SerializedPane,
        parent: Option<(GroupId, usize)>,
    ) -> Result<PaneId> {
        let pane_id = conn.select_row_bound::<_, i64>(sql!(
            INSERT INTO panes(workspace_id, active, pinned_count)
            VALUES (?, ?, ?)
            RETURNING pane_id
        ))?((workspace_id, pane.active, pane.pinned_count))?
        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;

        let (parent_id, order) = parent.unzip();
        conn.exec_bound(sql!(
            INSERT INTO center_panes(pane_id, parent_group_id, position)
            VALUES (?, ?, ?)
        ))?((pane_id, parent_id, order))?;

        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;

        Ok(pane_id)
    }

    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
        self.select_bound(sql!(
            SELECT kind, item_id, active, preview FROM items
            WHERE pane_id = ?
                ORDER BY position
        ))?(pane_id)
    }

    fn save_items(
        conn: &Connection,
        workspace_id: WorkspaceId,
        pane_id: PaneId,
        items: &[SerializedItem],
    ) -> Result<()> {
        let mut insert = conn.exec_bound(sql!(
            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
        )).context("Preparing insertion")?;
        for (position, item) in items.iter().enumerate() {
            insert((workspace_id, pane_id, position, item))?;
        }

        Ok(())
    }

    query! {
        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
            UPDATE workspaces
            SET timestamp = CURRENT_TIMESTAMP
            WHERE workspace_id = ?
        }
    }

    query! {
        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
            UPDATE workspaces
            SET window_state = ?2,
                window_x = ?3,
                window_y = ?4,
                window_width = ?5,
                window_height = ?6,
                display = ?7
            WHERE workspace_id = ?1
        }
    }

    query! {
        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
            UPDATE workspaces
            SET centered_layout = ?2
            WHERE workspace_id = ?1
        }
    }

    pub async fn toolchain(
        &self,
        workspace_id: WorkspaceId,
        worktree_id: WorktreeId,
        language_name: LanguageName,
    ) -> Result<Option<Toolchain>> {
        self.write(move |this| {
            let mut select = this
                .select_bound(sql!(
                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ?
                ))
                .context("Preparing insertion")?;

            let toolchain: Vec<(String, String, String)> =
                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize()))?;

            Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
                name: name.into(),
                path: path.into(),
                language_name,
                as_json: serde_json::Value::from_str(&raw_json).ok()?
            })))
        })
        .await
    }

    pub(crate) async fn toolchains(
        &self,
        workspace_id: WorkspaceId,
    ) -> Result<Vec<(Toolchain, WorktreeId)>> {
        self.write(move |this| {
            let mut select = this
                .select_bound(sql!(
                    SELECT name, path, worktree_id, language_name, raw_json FROM toolchains WHERE workspace_id = ?
                ))
                .context("Preparing insertion")?;

            let toolchain: Vec<(String, String, u64, String, String)> =
                select(workspace_id)?;

            Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, language_name, raw_json)| Some((Toolchain {
                name: name.into(),
                path: path.into(),
                language_name: LanguageName::new(&language_name),
                as_json: serde_json::Value::from_str(&raw_json).ok()?
            }, WorktreeId::from_proto(worktree_id)))).collect())
        })
        .await
    }
    pub async fn set_toolchain(
        &self,
        workspace_id: WorkspaceId,
        worktree_id: WorktreeId,
        toolchain: Toolchain,
    ) -> Result<()> {
        self.write(move |conn| {
            let mut insert = conn
                .exec_bound(sql!(
                    INSERT INTO toolchains(workspace_id, worktree_id, language_name, name, path) VALUES (?, ?, ?, ?,  ?)
                    ON CONFLICT DO
                    UPDATE SET
                        name = ?4,
                        path = ?5

                ))
                .context("Preparing insertion")?;

            insert((
                workspace_id,
                worktree_id.to_usize(),
                toolchain.language_name.as_ref(),
                toolchain.name.as_ref(),
                toolchain.path.as_ref(),
            ))?;

            Ok(())
        }).await
    }
}

#[cfg(test)]
mod tests {
    use std::thread;
    use std::time::Duration;

    use super::*;
    use crate::persistence::model::SerializedWorkspace;
    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
    use db::open_test_db;
    use gpui;

    #[gpui::test]
    async fn test_next_id_stability() {
        env_logger::try_init().ok();

        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);

        db.write(|conn| {
            conn.migrate(
                "test_table",
                &[sql!(
                    CREATE TABLE test_table(
                        text TEXT,
                        workspace_id INTEGER,
                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
                        ON DELETE CASCADE
                    ) STRICT;
                )],
            )
            .unwrap();
        })
        .await;

        let id = db.next_id().await.unwrap();
        // Assert the empty row got inserted
        assert_eq!(
            Some(id),
            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
            ))
            .unwrap()(id)
            .unwrap()
        );

        db.write(move |conn| {
            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
                .unwrap()(("test-text-1", id))
            .unwrap()
        })
        .await;

        let test_text_1 = db
            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
            .unwrap()(1)
        .unwrap()
        .unwrap();
        assert_eq!(test_text_1, "test-text-1");
    }

    #[gpui::test]
    async fn test_workspace_id_stability() {
        env_logger::try_init().ok();

        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);

        db.write(|conn| {
            conn.migrate(
                "test_table",
                &[sql!(
                        CREATE TABLE test_table(
                            text TEXT,
                            workspace_id INTEGER,
                            FOREIGN KEY(workspace_id)
                                REFERENCES workspaces(workspace_id)
                            ON DELETE CASCADE
                        ) STRICT;)],
            )
        })
        .await
        .unwrap();

        let mut workspace_1 = SerializedWorkspace {
            id: WorkspaceId(1),
            location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: None,
        };

        let workspace_2 = SerializedWorkspace {
            id: WorkspaceId(2),
            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: None,
        };

        db.save_workspace(workspace_1.clone()).await;

        db.write(|conn| {
            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
                .unwrap()(("test-text-1", 1))
            .unwrap();
        })
        .await;

        db.save_workspace(workspace_2.clone()).await;

        db.write(|conn| {
            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
                .unwrap()(("test-text-2", 2))
            .unwrap();
        })
        .await;

        workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
        db.save_workspace(workspace_1.clone()).await;
        db.save_workspace(workspace_1).await;
        db.save_workspace(workspace_2).await;

        let test_text_2 = db
            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
            .unwrap()(2)
        .unwrap()
        .unwrap();
        assert_eq!(test_text_2, "test-text-2");

        let test_text_1 = db
            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
            .unwrap()(1)
        .unwrap()
        .unwrap();
        assert_eq!(test_text_1, "test-text-1");
    }

    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
        SerializedPaneGroup::Group {
            axis: SerializedAxis(axis),
            flexes: None,
            children,
        }
    }

    #[gpui::test]
    async fn test_full_workspace_serialization() {
        env_logger::try_init().ok();

        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);

        //  -----------------
        //  | 1,2   | 5,6   |
        //  | - - - |       |
        //  | 3,4   |       |
        //  -----------------
        let center_group = group(
            Axis::Horizontal,
            vec![
                group(
                    Axis::Vertical,
                    vec![
                        SerializedPaneGroup::Pane(SerializedPane::new(
                            vec![
                                SerializedItem::new("Terminal", 5, false, false),
                                SerializedItem::new("Terminal", 6, true, false),
                            ],
                            false,
                            0,
                        )),
                        SerializedPaneGroup::Pane(SerializedPane::new(
                            vec![
                                SerializedItem::new("Terminal", 7, true, false),
                                SerializedItem::new("Terminal", 8, false, false),
                            ],
                            false,
                            0,
                        )),
                    ],
                ),
                SerializedPaneGroup::Pane(SerializedPane::new(
                    vec![
                        SerializedItem::new("Terminal", 9, false, false),
                        SerializedItem::new("Terminal", 10, true, false),
                    ],
                    false,
                    0,
                )),
            ],
        );

        let workspace = SerializedWorkspace {
            id: WorkspaceId(5),
            location: SerializedWorkspaceLocation::Local(
                LocalPaths::new(["/tmp", "/tmp2"]),
                LocalPathsOrder::new([1, 0]),
            ),
            center_group,
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: Some(999),
        };

        db.save_workspace(workspace.clone()).await;

        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
        assert_eq!(workspace, round_trip_workspace.unwrap());

        // Test guaranteed duplicate IDs
        db.save_workspace(workspace.clone()).await;
        db.save_workspace(workspace.clone()).await;

        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
        assert_eq!(workspace, round_trip_workspace.unwrap());
    }

    #[gpui::test]
    async fn test_workspace_assignment() {
        env_logger::try_init().ok();

        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);

        let workspace_1 = SerializedWorkspace {
            id: WorkspaceId(1),
            location: SerializedWorkspaceLocation::Local(
                LocalPaths::new(["/tmp", "/tmp2"]),
                LocalPathsOrder::new([0, 1]),
            ),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: Some(1),
        };

        let mut workspace_2 = SerializedWorkspace {
            id: WorkspaceId(2),
            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: Some(2),
        };

        db.save_workspace(workspace_1.clone()).await;
        db.save_workspace(workspace_2.clone()).await;

        // Test that paths are treated as a set
        assert_eq!(
            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
            workspace_1
        );
        assert_eq!(
            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
            workspace_1
        );

        // Make sure that other keys work
        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);

        // Test 'mutate' case of updating a pre-existing id
        workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);

        db.save_workspace(workspace_2.clone()).await;
        assert_eq!(
            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
            workspace_2
        );

        // Test other mechanism for mutating
        let mut workspace_3 = SerializedWorkspace {
            id: WorkspaceId(3),
            location: SerializedWorkspaceLocation::Local(
                LocalPaths::new(["/tmp", "/tmp2"]),
                LocalPathsOrder::new([1, 0]),
            ),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: Some(3),
        };

        db.save_workspace(workspace_3.clone()).await;
        assert_eq!(
            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
            workspace_3
        );

        // Make sure that updating paths differently also works
        workspace_3.location =
            SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
        db.save_workspace(workspace_3.clone()).await;
        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
        assert_eq!(
            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
                .unwrap(),
            workspace_3
        );
    }

    #[gpui::test]
    async fn test_session_workspaces() {
        env_logger::try_init().ok();

        let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);

        let workspace_1 = SerializedWorkspace {
            id: WorkspaceId(1),
            location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: Some("session-id-1".to_owned()),
            window_id: Some(10),
        };

        let workspace_2 = SerializedWorkspace {
            id: WorkspaceId(2),
            location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: Some("session-id-1".to_owned()),
            window_id: Some(20),
        };

        let workspace_3 = SerializedWorkspace {
            id: WorkspaceId(3),
            location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: Some("session-id-2".to_owned()),
            window_id: Some(30),
        };

        let workspace_4 = SerializedWorkspace {
            id: WorkspaceId(4),
            location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: None,
        };

        let ssh_project = db
            .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
            .await
            .unwrap();

        let workspace_5 = SerializedWorkspace {
            id: WorkspaceId(5),
            location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: Some("session-id-2".to_owned()),
            window_id: Some(50),
        };

        let workspace_6 = SerializedWorkspace {
            id: WorkspaceId(6),
            location: SerializedWorkspaceLocation::Local(
                LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
                LocalPathsOrder::new([2, 1, 0]),
            ),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: Some("session-id-3".to_owned()),
            window_id: Some(60),
        };

        db.save_workspace(workspace_1.clone()).await;
        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
        db.save_workspace(workspace_2.clone()).await;
        db.save_workspace(workspace_3.clone()).await;
        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
        db.save_workspace(workspace_4.clone()).await;
        db.save_workspace(workspace_5.clone()).await;
        db.save_workspace(workspace_6.clone()).await;

        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
        assert_eq!(locations.len(), 2);
        assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"]));
        assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
        assert_eq!(locations[0].2, Some(20));
        assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"]));
        assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
        assert_eq!(locations[1].2, Some(10));

        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
        assert_eq!(locations.len(), 2);
        let empty_paths: Vec<&str> = Vec::new();
        assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter()));
        assert_eq!(locations[0].1, LocalPathsOrder::new([]));
        assert_eq!(locations[0].2, Some(50));
        assert_eq!(locations[0].3, Some(ssh_project.id.0));
        assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"]));
        assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
        assert_eq!(locations[1].2, Some(30));

        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
        assert_eq!(locations.len(), 1);
        assert_eq!(
            locations[0].0,
            LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
        );
        assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
        assert_eq!(locations[0].2, Some(60));
    }

    fn default_workspace<P: AsRef<Path>>(
        workspace_id: &[P],
        center_group: &SerializedPaneGroup,
    ) -> SerializedWorkspace {
        SerializedWorkspace {
            id: WorkspaceId(4),
            location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
            center_group: center_group.clone(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: None,
            window_id: None,
        }
    }

    #[gpui::test]
    async fn test_last_session_workspace_locations() {
        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();

        let db =
            WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);

        let workspaces = [
            (1, vec![dir1.path()], vec![0], 9),
            (2, vec![dir2.path()], vec![0], 5),
            (3, vec![dir3.path()], vec![0], 8),
            (4, vec![dir4.path()], vec![0], 2),
            (
                5,
                vec![dir1.path(), dir2.path(), dir3.path()],
                vec![0, 1, 2],
                3,
            ),
            (
                6,
                vec![dir2.path(), dir3.path(), dir4.path()],
                vec![2, 1, 0],
                4,
            ),
        ]
        .into_iter()
        .map(|(id, locations, order, window_id)| SerializedWorkspace {
            id: WorkspaceId(id),
            location: SerializedWorkspaceLocation::Local(
                LocalPaths::new(locations),
                LocalPathsOrder::new(order),
            ),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: Some("one-session".to_owned()),
            window_id: Some(window_id),
        })
        .collect::<Vec<_>>();

        for workspace in workspaces.iter() {
            db.save_workspace(workspace.clone()).await;
        }

        let stack = Some(Vec::from([
            WindowId::from(2), // Top
            WindowId::from(8),
            WindowId::from(5),
            WindowId::from(9),
            WindowId::from(3),
            WindowId::from(4), // Bottom
        ]));

        let have = db
            .last_session_workspace_locations("one-session", stack)
            .unwrap();
        assert_eq!(have.len(), 6);
        assert_eq!(
            have[0],
            SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
        );
        assert_eq!(
            have[1],
            SerializedWorkspaceLocation::from_local_paths([dir3.path()])
        );
        assert_eq!(
            have[2],
            SerializedWorkspaceLocation::from_local_paths([dir2.path()])
        );
        assert_eq!(
            have[3],
            SerializedWorkspaceLocation::from_local_paths([dir1.path()])
        );
        assert_eq!(
            have[4],
            SerializedWorkspaceLocation::Local(
                LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
                LocalPathsOrder::new([0, 1, 2]),
            ),
        );
        assert_eq!(
            have[5],
            SerializedWorkspaceLocation::Local(
                LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
                LocalPathsOrder::new([2, 1, 0]),
            ),
        );
    }

    #[gpui::test]
    async fn test_last_session_workspace_locations_ssh_projects() {
        let db = WorkspaceDb(
            open_test_db("test_serializing_workspaces_last_session_workspaces_ssh_projects").await,
        );

        let ssh_projects = [
            ("host-1", "my-user-1"),
            ("host-2", "my-user-2"),
            ("host-3", "my-user-3"),
            ("host-4", "my-user-4"),
        ]
        .into_iter()
        .map(|(host, user)| async {
            db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
                .await
                .unwrap()
        })
        .collect::<Vec<_>>();

        let ssh_projects = futures::future::join_all(ssh_projects).await;

        let workspaces = [
            (1, ssh_projects[0].clone(), 9),
            (2, ssh_projects[1].clone(), 5),
            (3, ssh_projects[2].clone(), 8),
            (4, ssh_projects[3].clone(), 2),
        ]
        .into_iter()
        .map(|(id, ssh_project, window_id)| SerializedWorkspace {
            id: WorkspaceId(id),
            location: SerializedWorkspaceLocation::Ssh(ssh_project),
            center_group: Default::default(),
            window_bounds: Default::default(),
            display: Default::default(),
            docks: Default::default(),
            centered_layout: false,
            session_id: Some("one-session".to_owned()),
            window_id: Some(window_id),
        })
        .collect::<Vec<_>>();

        for workspace in workspaces.iter() {
            db.save_workspace(workspace.clone()).await;
        }

        let stack = Some(Vec::from([
            WindowId::from(2), // Top
            WindowId::from(8),
            WindowId::from(5),
            WindowId::from(9), // Bottom
        ]));

        let have = db
            .last_session_workspace_locations("one-session", stack)
            .unwrap();
        assert_eq!(have.len(), 4);
        assert_eq!(
            have[0],
            SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
        );
        assert_eq!(
            have[1],
            SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
        );
        assert_eq!(
            have[2],
            SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
        );
        assert_eq!(
            have[3],
            SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
        );
    }

    #[gpui::test]
    async fn test_get_or_create_ssh_project() {
        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);

        let (host, port, paths, user) = (
            "example.com".to_string(),
            Some(22_u16),
            vec!["/home/user".to_string(), "/etc/nginx".to_string()],
            Some("user".to_string()),
        );

        let project = db
            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
            .await
            .unwrap();

        assert_eq!(project.host, host);
        assert_eq!(project.paths, paths);
        assert_eq!(project.user, user);

        // Test that calling the function again with the same parameters returns the same project
        let same_project = db
            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
            .await
            .unwrap();

        assert_eq!(project.id, same_project.id);

        // Test with different parameters
        let (host2, paths2, user2) = (
            "otherexample.com".to_string(),
            vec!["/home/otheruser".to_string()],
            Some("otheruser".to_string()),
        );

        let different_project = db
            .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
            .await
            .unwrap();

        assert_ne!(project.id, different_project.id);
        assert_eq!(different_project.host, host2);
        assert_eq!(different_project.paths, paths2);
        assert_eq!(different_project.user, user2);
    }

    #[gpui::test]
    async fn test_get_or_create_ssh_project_with_null_user() {
        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);

        let (host, port, paths, user) = (
            "example.com".to_string(),
            None,
            vec!["/home/user".to_string()],
            None,
        );

        let project = db
            .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
            .await
            .unwrap();

        assert_eq!(project.host, host);
        assert_eq!(project.paths, paths);
        assert_eq!(project.user, None);

        // Test that calling the function again with the same parameters returns the same project
        let same_project = db
            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
            .await
            .unwrap();

        assert_eq!(project.id, same_project.id);
    }

    #[gpui::test]
    async fn test_get_ssh_projects() {
        let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);

        let projects = vec![
            (
                "example.com".to_string(),
                None,
                vec!["/home/user".to_string()],
                None,
            ),
            (
                "anotherexample.com".to_string(),
                Some(123_u16),
                vec!["/home/user2".to_string()],
                Some("user2".to_string()),
            ),
            (
                "yetanother.com".to_string(),
                Some(345_u16),
                vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
                None,
            ),
        ];

        for (host, port, paths, user) in projects.iter() {
            let project = db
                .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
                .await
                .unwrap();

            assert_eq!(&project.host, host);
            assert_eq!(&project.port, port);
            assert_eq!(&project.paths, paths);
            assert_eq!(&project.user, user);
        }

        let stored_projects = db.ssh_projects().unwrap();
        assert_eq!(stored_projects.len(), projects.len());
    }

    #[gpui::test]
    async fn test_simple_split() {
        env_logger::try_init().ok();

        let db = WorkspaceDb(open_test_db("simple_split").await);

        //  -----------------
        //  | 1,2   | 5,6   |
        //  | - - - |       |
        //  | 3,4   |       |
        //  -----------------
        let center_pane = group(
            Axis::Horizontal,
            vec![
                group(
                    Axis::Vertical,
                    vec![
                        SerializedPaneGroup::Pane(SerializedPane::new(
                            vec![
                                SerializedItem::new("Terminal", 1, false, false),
                                SerializedItem::new("Terminal", 2, true, false),
                            ],
                            false,
                            0,
                        )),
                        SerializedPaneGroup::Pane(SerializedPane::new(
                            vec![
                                SerializedItem::new("Terminal", 4, false, false),
                                SerializedItem::new("Terminal", 3, true, false),
                            ],
                            true,
                            0,
                        )),
                    ],
                ),
                SerializedPaneGroup::Pane(SerializedPane::new(
                    vec![
                        SerializedItem::new("Terminal", 5, true, false),
                        SerializedItem::new("Terminal", 6, false, false),
                    ],
                    false,
                    0,
                )),
            ],
        );

        let workspace = default_workspace(&["/tmp"], &center_pane);

        db.save_workspace(workspace.clone()).await;

        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();

        assert_eq!(workspace.center_group, new_workspace.center_group);
    }

    #[gpui::test]
    async fn test_cleanup_panes() {
        env_logger::try_init().ok();

        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);

        let center_pane = group(
            Axis::Horizontal,
            vec![
                group(
                    Axis::Vertical,
                    vec![
                        SerializedPaneGroup::Pane(SerializedPane::new(
                            vec![
                                SerializedItem::new("Terminal", 1, false, false),
                                SerializedItem::new("Terminal", 2, true, false),
                            ],
                            false,
                            0,
                        )),
                        SerializedPaneGroup::Pane(SerializedPane::new(
                            vec![
                                SerializedItem::new("Terminal", 4, false, false),
                                SerializedItem::new("Terminal", 3, true, false),
                            ],
                            true,
                            0,
                        )),
                    ],
                ),
                SerializedPaneGroup::Pane(SerializedPane::new(
                    vec![
                        SerializedItem::new("Terminal", 5, false, false),
                        SerializedItem::new("Terminal", 6, true, false),
                    ],
                    false,
                    0,
                )),
            ],
        );

        let id = &["/tmp"];

        let mut workspace = default_workspace(id, &center_pane);

        db.save_workspace(workspace.clone()).await;

        workspace.center_group = group(
            Axis::Vertical,
            vec![
                SerializedPaneGroup::Pane(SerializedPane::new(
                    vec![
                        SerializedItem::new("Terminal", 1, false, false),
                        SerializedItem::new("Terminal", 2, true, false),
                    ],
                    false,
                    0,
                )),
                SerializedPaneGroup::Pane(SerializedPane::new(
                    vec![
                        SerializedItem::new("Terminal", 4, true, false),
                        SerializedItem::new("Terminal", 3, false, false),
                    ],
                    true,
                    0,
                )),
            ],
        );

        db.save_workspace(workspace.clone()).await;

        let new_workspace = db.workspace_for_roots(id).unwrap();

        assert_eq!(workspace.center_group, new_workspace.center_group);
    }
}
