1use punch_types::{Creed, FighterId, PunchError, PunchResult};
7use tracing::debug;
8
9use crate::MemorySubstrate;
10
11impl MemorySubstrate {
12 pub async fn save_creed(&self, creed: &Creed) -> PunchResult<()> {
14 let creed_data = serde_json::to_string(creed)
15 .map_err(|e| PunchError::Memory(format!("failed to serialize creed: {e}")))?;
16 let fighter_id_str = creed.fighter_id.map(|id| id.to_string());
17 let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
18
19 let conn = self.conn.lock().await;
20 conn.execute(
21 "INSERT INTO creeds (id, fighter_name, fighter_id, creed_data, version, bout_count, message_count, created_at, updated_at)
22 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8)
23 ON CONFLICT(fighter_name) DO UPDATE SET
24 fighter_id = excluded.fighter_id,
25 creed_data = excluded.creed_data,
26 version = excluded.version,
27 bout_count = excluded.bout_count,
28 message_count = excluded.message_count,
29 updated_at = excluded.updated_at",
30 rusqlite::params![
31 creed.id.to_string(),
32 creed.fighter_name,
33 fighter_id_str,
34 creed_data,
35 creed.version,
36 creed.bout_count,
37 creed.message_count,
38 now,
39 ],
40 )
41 .map_err(|e| PunchError::Memory(format!("failed to save creed: {e}")))?;
42
43 debug!(fighter_name = %creed.fighter_name, version = creed.version, "creed saved");
44 Ok(())
45 }
46
47 pub async fn load_creed_by_name(&self, fighter_name: &str) -> PunchResult<Option<Creed>> {
49 let conn = self.conn.lock().await;
50 let mut stmt = conn
51 .prepare("SELECT creed_data FROM creeds WHERE fighter_name = ?1")
52 .map_err(|e| PunchError::Memory(format!("failed to prepare creed query: {e}")))?;
53
54 let result: Option<String> = stmt.query_row([fighter_name], |row| row.get(0)).ok();
55
56 match result {
57 Some(data) => {
58 let creed: Creed = serde_json::from_str(&data)
59 .map_err(|e| PunchError::Memory(format!("failed to deserialize creed: {e}")))?;
60 Ok(Some(creed))
61 }
62 None => Ok(None),
63 }
64 }
65
66 pub async fn load_creed_by_fighter(
68 &self,
69 fighter_id: &FighterId,
70 ) -> PunchResult<Option<Creed>> {
71 let fighter_str = fighter_id.to_string();
72 let conn = self.conn.lock().await;
73 let mut stmt = conn
74 .prepare("SELECT creed_data FROM creeds WHERE fighter_id = ?1")
75 .map_err(|e| PunchError::Memory(format!("failed to prepare creed query: {e}")))?;
76
77 let result: Option<String> = stmt.query_row([&fighter_str], |row| row.get(0)).ok();
78
79 match result {
80 Some(data) => {
81 let creed: Creed = serde_json::from_str(&data)
82 .map_err(|e| PunchError::Memory(format!("failed to deserialize creed: {e}")))?;
83 Ok(Some(creed))
84 }
85 None => Ok(None),
86 }
87 }
88
89 pub async fn list_creeds(&self) -> PunchResult<Vec<Creed>> {
91 let conn = self.conn.lock().await;
92 let mut stmt = conn
93 .prepare("SELECT creed_data FROM creeds ORDER BY updated_at DESC")
94 .map_err(|e| PunchError::Memory(format!("failed to list creeds: {e}")))?;
95
96 let rows = stmt
97 .query_map([], |row| {
98 let data: String = row.get(0)?;
99 Ok(data)
100 })
101 .map_err(|e| PunchError::Memory(format!("failed to read creed rows: {e}")))?;
102
103 let mut creeds = Vec::new();
104 for row in rows {
105 let data = row.map_err(|e| PunchError::Memory(format!("failed to read creed: {e}")))?;
106 let creed: Creed = serde_json::from_str(&data)
107 .map_err(|e| PunchError::Memory(format!("failed to deserialize creed: {e}")))?;
108 creeds.push(creed);
109 }
110 Ok(creeds)
111 }
112
113 pub async fn delete_creed(&self, fighter_name: &str) -> PunchResult<()> {
115 let conn = self.conn.lock().await;
116 conn.execute("DELETE FROM creeds WHERE fighter_name = ?1", [fighter_name])
117 .map_err(|e| PunchError::Memory(format!("failed to delete creed: {e}")))?;
118 debug!(fighter_name = fighter_name, "creed deleted");
119 Ok(())
120 }
121
122 pub async fn bind_creed_to_fighter(
124 &self,
125 fighter_name: &str,
126 fighter_id: &FighterId,
127 ) -> PunchResult<()> {
128 let fighter_str = fighter_id.to_string();
129 let conn = self.conn.lock().await;
130 conn.execute(
131 "UPDATE creeds SET fighter_id = ?1, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE fighter_name = ?2",
132 rusqlite::params![fighter_str, fighter_name],
133 )
134 .map_err(|e| PunchError::Memory(format!("failed to bind creed: {e}")))?;
135 debug!(fighter_name = fighter_name, fighter_id = %fighter_id, "creed bound to fighter");
136 Ok(())
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use punch_types::{Creed, FighterId};
143
144 use crate::MemorySubstrate;
145
146 fn make_creed(name: &str) -> Creed {
147 Creed::new(name)
148 }
149
150 #[tokio::test]
151 async fn test_save_and_load_creed_by_name() {
152 let substrate = MemorySubstrate::in_memory().unwrap();
153 let creed = make_creed("atlas");
154
155 substrate.save_creed(&creed).await.unwrap();
156 let loaded = substrate.load_creed_by_name("atlas").await.unwrap();
157 assert!(loaded.is_some());
158 let loaded = loaded.unwrap();
159 assert_eq!(loaded.fighter_name, "atlas");
160 assert_eq!(loaded.id, creed.id);
161 assert_eq!(loaded.version, 1);
162 assert_eq!(loaded.bout_count, 0);
163 assert_eq!(loaded.message_count, 0);
164 }
165
166 #[tokio::test]
167 async fn test_load_creed_by_fighter() {
168 let substrate = MemorySubstrate::in_memory().unwrap();
169 let fighter_id = FighterId::new();
170 let mut creed = make_creed("bravo");
171 creed.fighter_id = Some(fighter_id);
172
173 substrate.save_creed(&creed).await.unwrap();
174 let loaded = substrate.load_creed_by_fighter(&fighter_id).await.unwrap();
175 assert!(loaded.is_some());
176 let loaded = loaded.unwrap();
177 assert_eq!(loaded.fighter_name, "bravo");
178 assert_eq!(loaded.fighter_id, Some(fighter_id));
179 }
180
181 #[tokio::test]
182 async fn test_save_creed_upsert() {
183 let substrate = MemorySubstrate::in_memory().unwrap();
184 let mut creed = make_creed("charlie");
185 creed.identity = "original".into();
186 substrate.save_creed(&creed).await.unwrap();
187
188 creed.identity = "evolved".into();
190 creed.version = 2;
191 creed.bout_count = 5;
192 substrate.save_creed(&creed).await.unwrap();
193
194 let loaded = substrate
195 .load_creed_by_name("charlie")
196 .await
197 .unwrap()
198 .unwrap();
199 assert_eq!(loaded.identity, "evolved");
200 assert_eq!(loaded.version, 2);
201 assert_eq!(loaded.bout_count, 5);
202 }
203
204 #[tokio::test]
205 async fn test_list_creeds() {
206 let substrate = MemorySubstrate::in_memory().unwrap();
207 substrate.save_creed(&make_creed("delta")).await.unwrap();
208 substrate.save_creed(&make_creed("echo")).await.unwrap();
209 substrate.save_creed(&make_creed("foxtrot")).await.unwrap();
210
211 let creeds = substrate.list_creeds().await.unwrap();
212 assert_eq!(creeds.len(), 3);
213
214 let names: Vec<&str> = creeds.iter().map(|c| c.fighter_name.as_str()).collect();
215 assert!(names.contains(&"delta"));
216 assert!(names.contains(&"echo"));
217 assert!(names.contains(&"foxtrot"));
218 }
219
220 #[tokio::test]
221 async fn test_delete_creed() {
222 let substrate = MemorySubstrate::in_memory().unwrap();
223 substrate.save_creed(&make_creed("golf")).await.unwrap();
224
225 assert!(
227 substrate
228 .load_creed_by_name("golf")
229 .await
230 .unwrap()
231 .is_some()
232 );
233
234 substrate.delete_creed("golf").await.unwrap();
236 assert!(
237 substrate
238 .load_creed_by_name("golf")
239 .await
240 .unwrap()
241 .is_none()
242 );
243 }
244
245 #[tokio::test]
246 async fn test_bind_creed_to_fighter() {
247 let substrate = MemorySubstrate::in_memory().unwrap();
248 let creed = make_creed("hotel");
249 substrate.save_creed(&creed).await.unwrap();
250
251 let loaded = substrate
253 .load_creed_by_name("hotel")
254 .await
255 .unwrap()
256 .unwrap();
257 assert!(loaded.fighter_id.is_none());
258
259 let fighter_id = FighterId::new();
261 substrate
262 .bind_creed_to_fighter("hotel", &fighter_id)
263 .await
264 .unwrap();
265
266 let loaded = substrate.load_creed_by_fighter(&fighter_id).await.unwrap();
269 assert!(loaded.is_some());
270 assert_eq!(loaded.unwrap().fighter_name, "hotel");
271 }
272
273 #[tokio::test]
274 async fn test_load_nonexistent_creed_returns_none() {
275 let substrate = MemorySubstrate::in_memory().unwrap();
276 let by_name = substrate.load_creed_by_name("nonexistent").await.unwrap();
277 assert!(by_name.is_none());
278
279 let by_id = substrate
280 .load_creed_by_fighter(&FighterId::new())
281 .await
282 .unwrap();
283 assert!(by_id.is_none());
284 }
285}