1use bhtune_core::{DcsTemplate, built_in_templates};
11use chrono::{DateTime, Utc};
12use sqlx::SqlitePool;
13
14use crate::{
15 error::DbResult,
16 models::{DcsTemplateRow, TemplateOrigin},
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SeedOutcome {
22 Inserted,
24 Updated,
27 SkippedUserOwned,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SeedResult {
38 pub name: String,
39 pub outcome: SeedOutcome,
40}
41
42pub async fn seed_templates(
54 pool: &SqlitePool,
55 templates: Vec<DcsTemplate>,
56 origin: TemplateOrigin,
57 now: DateTime<Utc>,
58) -> DbResult<Vec<SeedResult>> {
59 let mut results = Vec::new();
60
61 for template in templates {
62 let outcome = match DcsTemplateRow::get_by_name(pool, &template.name).await? {
63 None => {
64 DcsTemplateRow::insert(pool, &template, origin, now).await?;
65 SeedOutcome::Inserted
66 }
67 Some(existing) if existing.origin == origin => {
68 DcsTemplateRow::update(pool, existing.id, &template, now).await?;
69 SeedOutcome::Updated
70 }
71 Some(_) => SeedOutcome::SkippedUserOwned,
72 };
73
74 results.push(SeedResult {
75 name: template.name,
76 outcome,
77 });
78 }
79
80 Ok(results)
81}
82
83pub async fn seed_builtin_templates(
87 pool: &SqlitePool,
88 now: DateTime<Utc>,
89) -> DbResult<Vec<SeedResult>> {
90 seed_templates(pool, built_in_templates(), TemplateOrigin::Builtin, now).await
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::pool::connect_in_memory;
97
98 fn now() -> DateTime<Utc> {
99 DateTime::from_timestamp(1_700_000_000, 0).unwrap()
100 }
101
102 #[tokio::test]
103 async fn seeds_all_builtins_into_empty_database() {
104 let pool = connect_in_memory().await.unwrap();
105
106 let results = seed_builtin_templates(&pool, now()).await.unwrap();
107
108 assert_eq!(results.len(), built_in_templates().len());
109 assert!(results.iter().all(|r| r.outcome == SeedOutcome::Inserted));
110
111 let rows = DcsTemplateRow::list(&pool).await.unwrap();
112 assert_eq!(rows.len(), built_in_templates().len());
113 assert!(rows.iter().all(|r| r.origin == TemplateOrigin::Builtin));
114
115 for template in built_in_templates() {
117 let row = DcsTemplateRow::get_by_name(&pool, &template.name)
118 .await
119 .unwrap()
120 .expect("seeded template should be found by name");
121 assert_eq!(row.template, template);
122 }
123 }
124
125 #[tokio::test]
126 async fn reseeding_is_idempotent_and_does_not_duplicate() {
127 let pool = connect_in_memory().await.unwrap();
128
129 seed_builtin_templates(&pool, now()).await.unwrap();
130 let second = seed_builtin_templates(&pool, now()).await.unwrap();
131
132 assert!(second.iter().all(|r| r.outcome == SeedOutcome::Updated));
133 let rows = DcsTemplateRow::list(&pool).await.unwrap();
134 assert_eq!(rows.len(), built_in_templates().len());
135 }
136
137 #[tokio::test]
138 async fn reseeding_corrects_a_drifted_builtin_row_in_place() {
139 let pool = connect_in_memory().await.unwrap();
140 seed_builtin_templates(&pool, now()).await.unwrap();
141
142 let existing = DcsTemplateRow::get_by_name(&pool, "Yokogawa CentumVP")
145 .await
146 .unwrap()
147 .unwrap();
148 sqlx::query("UPDATE dcs_templates SET manipulated_variable_suffix = 'WRONG' WHERE id = ?")
149 .bind(existing.id)
150 .execute(&pool)
151 .await
152 .unwrap();
153
154 seed_builtin_templates(&pool, now()).await.unwrap();
155
156 let corrected = DcsTemplateRow::get_by_name(&pool, "Yokogawa CentumVP")
157 .await
158 .unwrap()
159 .unwrap();
160 assert_eq!(
161 corrected.id, existing.id,
162 "must update in place, not re-insert"
163 );
164 assert_eq!(corrected.template.manipulated_variable_suffix, "MV");
165 }
166
167 #[tokio::test]
168 async fn never_overwrites_a_user_owned_row_with_a_colliding_name() {
169 let pool = connect_in_memory().await.unwrap();
170
171 let mut custom = built_in_templates().remove(0); custom.manipulated_variable_suffix = "CUSTOM_MV".to_string();
173 let inserted = DcsTemplateRow::insert(&pool, &custom, TemplateOrigin::User, now())
174 .await
175 .unwrap();
176
177 let results = seed_builtin_templates(&pool, now()).await.unwrap();
178
179 let yokogawa_result = results
180 .iter()
181 .find(|r| r.name == "Yokogawa CentumVP")
182 .unwrap();
183 assert_eq!(yokogawa_result.outcome, SeedOutcome::SkippedUserOwned);
184
185 let still_custom = DcsTemplateRow::get(&pool, inserted.id)
186 .await
187 .unwrap()
188 .unwrap();
189 assert_eq!(
190 still_custom.template.manipulated_variable_suffix,
191 "CUSTOM_MV"
192 );
193 assert_eq!(still_custom.origin, TemplateOrigin::User);
194 }
195}