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