//! Diesel-backed adapter for dataset persistence.
//!
//! # Ownership
//!
//! This module implements [`DatasetCatalogRepository`],
//! [`DatasetIngestRepository`], and [`DatasetReadRepository`] using SQLite
//! via Diesel. All SQL queries and Diesel model conversions live here; the
//! service layer never sees Diesel types directly.
//!
//! # Invariants
//!
//! - Tag mutations (`add_tags`, `remove_tags`, `delete_tag`, `rename_tag`,
//!   `merge_tag`) and import operations run inside `immediate_transaction` to
//!   guarantee atomicity.
//! - Import operations (`insert_imported_dataset_record`,
//!   `replace_imported_dataset_record`) preserve the archive’s `uid`,
//!   `created_at`, `favorite`, `status`, and `tags` from [`ExportedMetadata`]
//!   exactly.
//! - `replace_imported_dataset_record` clears `trashed_at` / `deleted_at` so a
//!   re-imported dataset becomes live again.
//!
//! # Extension notes
//!
//! - Adding a metadata field requires updating [`DbDatasetUpdate`],
//!   [`NewDataset`], [`dataset_record_from_models`], both import methods, and
//!   the Diesel schema in `database::schema`.
//! - Adding a new status variant requires a corresponding [`DbDatasetStatus`]
//!   mapping.

mod models;
mod types;

use diesel::{SqliteConnection, prelude::*};
use tracing::{debug, info, instrument};
use uuid::Uuid;

use self::{
    models::{Dataset, DatasetTag, DatasetUpdate as DbDatasetUpdate, NewDataset, Tag},
    types::{DbDatasetStatus, SimpleUuid},
};
use crate::{
    DEFAULT_DATASET_LIST_LIMIT,
    database::{
        core::{DatabaseError, Pool},
        schema,
    },
    dataset::{
        NormalizedTag,
        catalog::{CatalogError, DatasetCatalogRepository},
        ingest::{CreateDatasetRequest, DatasetIngestRepository, IngestError},
        model::{
            DatasetId, DatasetListQuery, DatasetMetadata, DatasetRecord, DatasetSortBy,
            DatasetStatus, DatasetUpdate, SortDirection,
        },
        portability::ExportedMetadata,
        read::{DatasetLocation, DatasetReadRepository, ReadError},
    },
};

/// Diesel-backed dataset repository.
///
/// Holds a connection pool. All public methods acquire a connection from the
/// pool, run queries, and release it. Thread-safe via `Pool`.
#[derive(Clone)]
pub(crate) struct DatasetRepository {
    pool: Pool,
}

impl DatasetRepository {
    #[must_use]
    pub(crate) fn new(pool: Pool) -> Self {
        Self { pool }
    }
}

/// Convert Diesel models into the domain [`DatasetRecord`].
///
/// Tags are passed separately because they are loaded via a grouped join.
fn dataset_record_from_models(dataset: Dataset, tags: Vec<Tag>) -> DatasetRecord {
    let metadata = DatasetMetadata {
        uid: dataset.uid.0,
        name: dataset.name,
        description: dataset.description,
        favorite: dataset.favorite,
        status: dataset.status.0,
        created_at: dataset.created_at.and_utc(),
        trashed_at: dataset.trashed_at.map(|value| value.and_utc()),
        deleted_at: dataset.deleted_at.map(|value| value.and_utc()),
        tags: tags.into_iter().map(|tag| tag.name).collect(),
    };

    DatasetRecord {
        id: dataset.id,
        metadata,
    }
}

fn dataset_location_from_model(dataset: &Dataset) -> DatasetLocation {
    DatasetLocation {
        id: dataset.id,
        uid: dataset.uid.0,
    }
}

fn get_dataset_model(
    conn: &mut SqliteConnection,
    id: DatasetId,
) -> diesel::QueryResult<Option<Dataset>> {
    match id {
        DatasetId::Id(dataset_id) => Dataset::find_by_id(conn, dataset_id),
        DatasetId::Uid(uid) => Dataset::find_by_uid(conn, uid),
    }
}

fn dataset_not_found(id: DatasetId) -> String {
    match id {
        DatasetId::Id(value) => value.to_string(),
        DatasetId::Uid(value) => value.to_string(),
    }
}

fn normalize_search(search: Option<&str>) -> Option<&str> {
    search.and_then(|value| {
        let trimmed = value.trim();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed)
        }
    })
}

fn normalize_tag_filters(tags: Option<&[String]>) -> Option<Vec<String>> {
    tags.and_then(|tags| {
        let cleaned: Vec<String> = tags
            .iter()
            .map(|tag| tag.trim())
            .filter(|tag| !tag.is_empty())
            .map(str::to_string)
            .collect();
        if cleaned.is_empty() {
            None
        } else {
            Some(cleaned)
        }
    })
}

/// Resolve dataset ids that match *any* of the given tag names.
///
/// Returns `None` when no tag filter is active (all datasets pass).
/// Returns `Some(vec![])` when filters are active but no datasets match
/// (short-circuit the caller to return an empty result).
fn resolve_tagged_dataset_ids(
    conn: &mut SqliteConnection,
    tag_filters: Option<&[String]>,
) -> Result<Option<Vec<i32>>, DatabaseError> {
    let Some(tag_filters) = tag_filters else {
        return Ok(None);
    };

    let ids = schema::datasets_tags::table
        .inner_join(schema::tags::table)
        .filter(schema::tags::name.eq_any(tag_filters))
        .select(schema::datasets_tags::dataset_id)
        .distinct()
        .load::<i32>(conn)?;

    if ids.is_empty() {
        Ok(Some(Vec::new()))
    } else {
        Ok(Some(ids))
    }
}

fn normalize_statuses(statuses: Option<&[DatasetStatus]>) -> Option<Vec<DbDatasetStatus>> {
    statuses.and_then(|statuses| {
        let mut deduped = statuses.to_vec();
        deduped.sort_unstable_by_key(|status| *status as u8);
        deduped.dedup();
        if deduped.is_empty() {
            None
        } else {
            Some(deduped.into_iter().map(DbDatasetStatus::from).collect())
        }
    })
}

fn apply_trashed_filter(
    query: schema::datasets::BoxedQuery<'_, diesel::sqlite::Sqlite>,
    trashed: Option<bool>,
) -> schema::datasets::BoxedQuery<'_, diesel::sqlite::Sqlite> {
    match trashed {
        Some(true) => query.filter(schema::datasets::trashed_at.is_not_null()),
        Some(false) => query.filter(schema::datasets::trashed_at.is_null()),
        None => query,
    }
}

fn apply_deleted_filter(
    query: schema::datasets::BoxedQuery<'_, diesel::sqlite::Sqlite>,
    deleted: Option<bool>,
) -> schema::datasets::BoxedQuery<'_, diesel::sqlite::Sqlite> {
    match deleted {
        Some(true) => query.filter(schema::datasets::deleted_at.is_not_null()),
        Some(false) => query.filter(schema::datasets::deleted_at.is_null()),
        None => query,
    }
}

/// Eagerly load tags for a batch of datasets using a grouped join.
///
/// Diesel’s `grouped_by` requires the parent records to be in the same
/// order as the query result; callers must not re-sort `all_datasets`
/// before passing them here.
fn map_datasets_with_tags(
    conn: &mut SqliteConnection,
    all_datasets: Vec<Dataset>,
) -> Result<Vec<DatasetRecord>, DatabaseError> {
    let dataset_tags = DatasetTag::belonging_to(&all_datasets)
        .inner_join(schema::tags::table)
        .select((DatasetTag::as_select(), Tag::as_select()))
        .load::<(DatasetTag, Tag)>(conn)?;

    let datasets_with_tags: Vec<(Dataset, Vec<Tag>)> = dataset_tags
        .grouped_by(&all_datasets)
        .into_iter()
        .zip(all_datasets)
        .map(|(dataset_tags, dataset)| {
            (
                dataset,
                dataset_tags.into_iter().map(|(_, tag)| tag).collect(),
            )
        })
        .collect();

    Ok(datasets_with_tags
        .into_iter()
        .map(|(dataset, tags)| dataset_record_from_models(dataset, tags))
        .collect())
}

fn list_all_dataset_records_including_deleted(
    conn: &mut SqliteConnection,
    query_options: &DatasetListQuery,
) -> Result<Vec<DatasetRecord>, DatabaseError> {
    let unbounded_query = query_options.clone().unbounded();
    list_dataset_records_with_deleted_filter(conn, &unbounded_query, None)
}

#[instrument(skip(conn, query_options))]
fn list_dataset_records(
    conn: &mut SqliteConnection,
    query_options: &DatasetListQuery,
) -> Result<Vec<DatasetRecord>, DatabaseError> {
    list_dataset_records_with_deleted_filter(conn, query_options, Some(false))
}

#[instrument(skip(conn, query_options))]
fn list_dataset_records_with_deleted_filter(
    conn: &mut SqliteConnection,
    query_options: &DatasetListQuery,
    deleted: Option<bool>,
) -> Result<Vec<DatasetRecord>, DatabaseError> {
    let search = normalize_search(query_options.search.as_deref());
    let tag_filters = normalize_tag_filters(query_options.tags.as_deref());
    let tagged_dataset_ids = resolve_tagged_dataset_ids(conn, tag_filters.as_deref())?;
    if tagged_dataset_ids.as_ref().is_some_and(Vec::is_empty) {
        return Ok(Vec::new());
    }
    let statuses = normalize_statuses(query_options.statuses.as_deref());

    let mut query = schema::datasets::table.into_boxed();
    if let Some(search) = search {
        let pattern = format!("%{search}%");
        query = query.filter(schema::datasets::name.like(pattern));
    }
    if let Some(ids) = tagged_dataset_ids {
        query = query.filter(schema::datasets::id.eq_any(ids));
    }
    if query_options.favorite_only {
        query = query.filter(schema::datasets::favorite.eq(true));
    }
    if let Some(statuses) = statuses {
        query = query.filter(schema::datasets::status.eq_any(statuses));
    }
    query = apply_trashed_filter(query, query_options.trashed);
    query = apply_deleted_filter(query, deleted);

    query = match (query_options.sort_by, query_options.sort_direction) {
        (DatasetSortBy::Id, SortDirection::Asc) => query.order(schema::datasets::id.asc()),
        (DatasetSortBy::Id, SortDirection::Desc) => query.order(schema::datasets::id.desc()),
        (DatasetSortBy::Name, SortDirection::Asc) => {
            query.order((schema::datasets::name.asc(), schema::datasets::id.desc()))
        }
        (DatasetSortBy::Name, SortDirection::Desc) => {
            query.order((schema::datasets::name.desc(), schema::datasets::id.desc()))
        }
        (DatasetSortBy::CreatedAt, SortDirection::Asc) => query.order((
            schema::datasets::created_at.asc(),
            schema::datasets::id.desc(),
        )),
        (DatasetSortBy::CreatedAt, SortDirection::Desc) => query.order((
            schema::datasets::created_at.desc(),
            schema::datasets::id.desc(),
        )),
    };

    let limit = query_options
        .limit
        .unwrap_or(DEFAULT_DATASET_LIST_LIMIT)
        .max(0);
    let offset = query_options.offset.unwrap_or(0).max(0);
    let all_datasets: Vec<Dataset> = query
        .limit(limit)
        .offset(offset)
        .select(Dataset::as_select())
        .load(conn)?;

    map_datasets_with_tags(conn, all_datasets)
}

#[instrument(skip(conn, id), fields(dataset.id = ?id))]
fn get_dataset_record(
    conn: &mut SqliteConnection,
    id: DatasetId,
) -> Result<Option<DatasetRecord>, DatabaseError> {
    let Some(dataset) = get_dataset_model(conn, id)? else {
        return Ok(None);
    };
    let tags = dataset.load_tags(conn)?;
    Ok(Some(dataset_record_from_models(dataset, tags)))
}

/// Mark all datasets currently in `Writing` status as `Aborted`.
///
/// Called at startup to recover from unclean shutdowns where ingest was
/// interrupted. Returns the number of records updated.
pub(crate) fn cleanup_writing_datasets(pool: &Pool) -> Result<usize, DatabaseError> {
    use schema::datasets::dsl::{datasets, status};

    let mut conn = pool.get()?;
    let updated_count =
        diesel::update(datasets.filter(status.eq(DbDatasetStatus::from(DatasetStatus::Writing))))
            .set(status.eq(DbDatasetStatus::from(DatasetStatus::Aborted)))
            .execute(&mut conn)?;

    if updated_count > 0 {
        info!(
            "Updated {} datasets from 'writing' to 'aborted' status",
            updated_count
        );
    }

    Ok(updated_count)
}

impl DatasetCatalogRepository for DatasetRepository {
    fn get_dataset(&self, id: DatasetId) -> Result<DatasetRecord, CatalogError> {
        let mut conn = self.pool.get()?;
        let record = get_dataset_record(&mut conn, id)?;
        record.ok_or_else(|| CatalogError::NotFound {
            id: dataset_not_found(id),
        })
    }

    fn list_datasets(
        &self,
        query_options: DatasetListQuery,
    ) -> Result<Vec<DatasetRecord>, CatalogError> {
        let mut conn = self.pool.get()?;
        Ok(list_dataset_records(&mut conn, &query_options)?)
    }

    fn list_all_datasets_including_deleted(&self) -> Result<Vec<DatasetRecord>, CatalogError> {
        let mut conn = self.pool.get()?;
        Ok(list_all_dataset_records_including_deleted(
            &mut conn,
            &DatasetListQuery::default().include_trashed(),
        )?)
    }

    fn list_deleted_datasets(&self) -> Result<Vec<DatasetRecord>, CatalogError> {
        let mut conn = self.pool.get()?;
        Ok(list_dataset_records_with_deleted_filter(
            &mut conn,
            &DatasetListQuery::default().include_trashed().unbounded(),
            Some(true),
        )?)
    }

    fn list_dataset_tags(&self) -> Result<Vec<String>, CatalogError> {
        let mut conn = self.pool.get()?;
        let tags = schema::tags::table
            .inner_join(schema::datasets_tags::table.inner_join(schema::datasets::table))
            .filter(schema::datasets::deleted_at.is_null())
            .select(schema::tags::name)
            .distinct()
            .order(schema::tags::name.asc())
            .load(&mut conn)
            .map_err(DatabaseError::from)?;
        Ok(tags)
    }

    fn update_dataset(&self, id: i32, update: DatasetUpdate) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        let db_update = DbDatasetUpdate {
            name: update.name,
            description: update.description,
            favorite: update.favorite,
            status: None,
            created_at: None,
            trashed_at: None,
            deleted_at: None,
        };
        Dataset::update_metadata(&mut conn, id, &db_update).map_err(DatabaseError::from)?;
        debug!(dataset.id = id, "Dataset metadata updated");
        Ok(())
    }

    fn add_tags(&self, id: i32, tags: &[NormalizedTag]) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        let tag_names: Vec<String> = tags.iter().map(|tag| tag.as_str().to_string()).collect();
        conn.immediate_transaction(|conn| {
            let created_tags = Tag::find_or_create_batch(conn, &tag_names)?;
            let tag_ids: Vec<i32> = created_tags.into_iter().map(|tag| tag.id).collect();
            DatasetTag::create_associations(conn, id, &tag_ids)?;
            Ok::<(), diesel::result::Error>(())
        })
        .map_err(DatabaseError::from)?;
        debug!(dataset.id = id, ?tags, "Tags added to dataset");
        Ok(())
    }

    fn remove_tags(&self, id: i32, tags: &[NormalizedTag]) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        let tag_names: Vec<String> = tags.iter().map(|tag| tag.as_str().to_string()).collect();
        conn.immediate_transaction(|conn| {
            let tag_ids_to_delete = schema::tags::table
                .filter(schema::tags::name.eq_any(tag_names))
                .select(schema::tags::id)
                .load::<i32>(conn)?;

            DatasetTag::remove_associations(conn, id, &tag_ids_to_delete)?;
            Ok::<(), diesel::result::Error>(())
        })
        .map_err(DatabaseError::from)?;
        debug!(dataset.id = id, ?tags, "Tags removed from dataset");
        Ok(())
    }

    fn mark_dataset_deleted(&self, id: i32) -> Result<DatasetRecord, CatalogError> {
        let mut conn = self.pool.get()?;
        let record =
            conn.immediate_transaction(|conn| -> Result<DatasetRecord, DatabaseError> {
                Dataset::mark_deleted(conn, id)?;
                get_dataset_record(conn, DatasetId::Id(id))?
                    .ok_or_else(|| DatabaseError::Query(diesel::result::Error::NotFound))
            })?;
        Ok(record)
    }

    fn trash_dataset(&self, id: i32) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        Dataset::trash(&mut conn, id).map_err(DatabaseError::from)?;
        Ok(())
    }

    fn restore_dataset(&self, id: i32) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        Dataset::restore(&mut conn, id).map_err(DatabaseError::from)?;
        Ok(())
    }

    fn delete_tag(&self, tag: &NormalizedTag) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        conn.immediate_transaction(|conn| Tag::delete_by_name(conn, tag.as_str()))
            .map_err(DatabaseError::from)?;
        Ok(())
    }

    fn rename_tag(
        &self,
        old_name: &NormalizedTag,
        new_name: &NormalizedTag,
    ) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        conn.immediate_transaction(|conn| Tag::rename(conn, old_name.as_str(), new_name.as_str()))
            .map_err(DatabaseError::from)?;
        Ok(())
    }

    fn merge_tag(
        &self,
        source: &NormalizedTag,
        target: &NormalizedTag,
    ) -> Result<(), CatalogError> {
        let mut conn = self.pool.get()?;
        conn.immediate_transaction(|conn| Tag::merge_into(conn, source.as_str(), target.as_str()))
            .map_err(DatabaseError::from)?;
        Ok(())
    }

    fn insert_imported_dataset_record(
        &self,
        metadata: &ExportedMetadata,
    ) -> Result<DatasetRecord, CatalogError> {
        let mut conn = self.pool.get()?;
        let (dataset, tags) = conn
            .immediate_transaction(|conn| {
                let new_dataset = NewDataset {
                    uid: SimpleUuid(metadata.uid),
                    name: &metadata.name,
                    description: &metadata.description,
                    favorite: metadata.favorite,
                    status: DbDatasetStatus::from(metadata.status),
                    created_at: metadata.created_at.naive_utc(),
                };
                let dataset: Dataset = diesel::insert_into(schema::datasets::table)
                    .values(new_dataset)
                    .returning(Dataset::as_returning())
                    .get_result(conn)?;
                let tags = if metadata.tags.is_empty() {
                    vec![]
                } else {
                    let created_tags = Tag::find_or_create_batch(conn, &metadata.tags)?;
                    let tag_ids: Vec<i32> = created_tags.iter().map(|tag| tag.id).collect();
                    DatasetTag::create_associations(conn, dataset.id, &tag_ids)?;
                    created_tags
                };
                Ok::<(Dataset, Vec<Tag>), diesel::result::Error>((dataset, tags))
            })
            .map_err(DatabaseError::from)?;
        let record = dataset_record_from_models(dataset, tags);
        debug!(dataset.id = record.id, uid = %metadata.uid, "Dataset record imported");
        Ok(record)
    }

    fn replace_imported_dataset_record(
        &self,
        id: i32,
        metadata: &ExportedMetadata,
    ) -> Result<DatasetRecord, CatalogError> {
        let mut conn = self.pool.get()?;
        let (dataset, tags) = conn
            .immediate_transaction(|conn| {
                let db_update = DbDatasetUpdate {
                    name: Some(metadata.name.clone()),
                    description: Some(metadata.description.clone()),
                    favorite: Some(metadata.favorite),
                    status: Some(DbDatasetStatus::from(metadata.status)),
                    created_at: Some(metadata.created_at.naive_utc()),
                    trashed_at: Some(None),
                    deleted_at: Some(None),
                };
                Dataset::update_metadata(conn, id, &db_update)?;

                DatasetTag::clear_associations(conn, id)?;
                let tags = if metadata.tags.is_empty() {
                    vec![]
                } else {
                    let created_tags = Tag::find_or_create_batch(conn, &metadata.tags)?;
                    let tag_ids: Vec<i32> = created_tags.iter().map(|tag| tag.id).collect();
                    DatasetTag::create_associations(conn, id, &tag_ids)?;
                    created_tags
                };
                let dataset =
                    Dataset::find_by_id(conn, id)?.ok_or(diesel::result::Error::NotFound)?;
                Ok::<(Dataset, Vec<Tag>), diesel::result::Error>((dataset, tags))
            })
            .map_err(DatabaseError::from)?;
        let record = dataset_record_from_models(dataset, tags);
        debug!(dataset.id = record.id, uid = %metadata.uid, "Dataset record replaced from import");
        Ok(record)
    }

    fn find_dataset_by_uid(&self, uid: Uuid) -> Result<Option<DatasetRecord>, CatalogError> {
        let mut conn = self.pool.get()?;
        let Some(dataset) = Dataset::find_by_uid(&mut conn, uid).map_err(DatabaseError::from)?
        else {
            return Ok(None);
        };
        let tags = dataset.load_tags(&mut conn).map_err(DatabaseError::from)?;
        Ok(Some(dataset_record_from_models(dataset, tags)))
    }
}

impl DatasetIngestRepository for DatasetRepository {
    fn create_dataset_record(
        &self,
        request: &CreateDatasetRequest,
        uid: Uuid,
    ) -> Result<DatasetRecord, IngestError> {
        let mut conn = self.pool.get()?;
        let (dataset, tags) = create_dataset_db_record(&mut conn, request, uid)?;
        Ok(dataset_record_from_models(dataset, tags))
    }

    fn update_status(&self, id: i32, status: DatasetStatus) -> Result<(), IngestError> {
        let mut conn = self.pool.get()?;
        Dataset::update_status(&mut conn, id, DbDatasetStatus::from(status))
            .map_err(DatabaseError::from)?;
        Ok(())
    }

    fn get_dataset(&self, id: DatasetId) -> Result<DatasetRecord, IngestError> {
        let mut conn = self.pool.get()?;
        let record = get_dataset_record(&mut conn, id)?;
        record.ok_or_else(|| IngestError::NotFound {
            id: dataset_not_found(id),
        })
    }
}

impl DatasetReadRepository for DatasetRepository {
    fn resolve_dataset(&self, id: DatasetId) -> Result<DatasetLocation, ReadError> {
        let mut conn = self.pool.get()?;
        let Some(dataset) = get_dataset_model(&mut conn, id).map_err(DatabaseError::from)? else {
            return Err(ReadError::NotFound {
                id: dataset_not_found(id),
            });
        };
        if dataset.deleted_at.is_some() {
            return Err(ReadError::Deleted {
                id: dataset_not_found(id),
            });
        }
        Ok(dataset_location_from_model(&dataset))
    }
}

fn create_dataset_db_record(
    conn: &mut SqliteConnection,
    request: &CreateDatasetRequest,
    uid: Uuid,
) -> Result<(Dataset, Vec<Tag>), DatabaseError> {
    conn.immediate_transaction(|conn| {
        let new_dataset = NewDataset {
            uid: SimpleUuid(uid),
            name: &request.name,
            description: &request.description,
            favorite: false,
            status: DbDatasetStatus::from(DatasetStatus::Writing),
            created_at: chrono::Utc::now().naive_utc(),
        };

        let dataset = diesel::insert_into(schema::datasets::table)
            .values(new_dataset)
            .returning(Dataset::as_returning())
            .get_result(conn)?;

        let tags = if request.tags.is_empty() {
            vec![]
        } else {
            let created_tags = Tag::find_or_create_batch(conn, &request.tags)?;
            let tag_ids: Vec<i32> = created_tags.iter().map(|tag| tag.id).collect();
            DatasetTag::create_associations(conn, dataset.id, &tag_ids)?;
            created_tags
        };

        Ok::<(Dataset, Vec<Tag>), diesel::result::Error>((dataset, tags))
    })
    .map_err(DatabaseError::from)
}

#[cfg(test)]
mod tests {
    use chrono::{Duration, Utc};
    use tempfile::TempDir;

    use super::*;
    use crate::database::core;

    struct RepoFixture {
        _tmp: TempDir,
        repo: DatasetRepository,
    }

    impl RepoFixture {
        fn new() -> Self {
            let tmp = TempDir::new().expect("temp dir");
            let db_path = tmp.path().join("fricon.sqlite3");
            let backup_path = tmp.path().join("fricon-backup.sqlite3");
            let pool = core::connect(&db_path, backup_path).expect("database");
            Self {
                _tmp: tmp,
                repo: DatasetRepository::new(pool),
            }
        }
    }

    #[test]
    fn insert_imported_dataset_record_preserves_created_at() {
        let fixture = RepoFixture::new();
        let created_at = Utc::now() - Duration::days(5);
        let metadata = ExportedMetadata {
            archive_version: 1,
            uid: Uuid::new_v4(),
            name: "imported".to_string(),
            description: "imported dataset".to_string(),
            favorite: true,
            status: DatasetStatus::Completed,
            created_at,
            tags: vec!["alpha".to_string()],
        };

        let record = fixture
            .repo
            .insert_imported_dataset_record(&metadata)
            .expect("insert imported dataset");

        assert_eq!(record.metadata.created_at, created_at);
        assert_eq!(record.metadata.tags, vec!["alpha"]);
        assert!(record.metadata.favorite);
    }

    #[test]
    fn replace_imported_dataset_record_rewrites_tags_and_revives_dataset() {
        let fixture = RepoFixture::new();
        let uid = Uuid::new_v4();
        let request = CreateDatasetRequest {
            name: "existing".to_string(),
            description: "existing dataset".to_string(),
            tags: vec!["old".to_string(), "stale".to_string()],
        };
        let existing = DatasetIngestRepository::create_dataset_record(&fixture.repo, &request, uid)
            .expect("create dataset");
        fixture
            .repo
            .trash_dataset(existing.id)
            .expect("trash dataset");
        fixture
            .repo
            .mark_dataset_deleted(existing.id)
            .expect("mark deleted");

        let created_at = Utc::now() - Duration::days(2);
        let metadata = ExportedMetadata {
            archive_version: 1,
            uid,
            name: "replacement".to_string(),
            description: "replacement dataset".to_string(),
            favorite: true,
            status: DatasetStatus::Completed,
            created_at,
            tags: vec!["fresh".to_string()],
        };

        let replaced = fixture
            .repo
            .replace_imported_dataset_record(existing.id, &metadata)
            .expect("replace imported dataset");

        assert_eq!(replaced.id, existing.id);
        assert_eq!(replaced.metadata.name, "replacement");
        assert_eq!(replaced.metadata.description, "replacement dataset");
        assert_eq!(replaced.metadata.created_at, created_at);
        assert_eq!(replaced.metadata.tags, vec!["fresh"]);
        assert!(replaced.metadata.favorite);
        assert!(replaced.metadata.trashed_at.is_none());
        assert!(replaced.metadata.deleted_at.is_none());
    }
}
