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
55 .query_row([fighter_name], |row| row.get(0))
56 .ok();
57
58 match result {
59 Some(data) => {
60 let creed: Creed = serde_json::from_str(&data)
61 .map_err(|e| PunchError::Memory(format!("failed to deserialize creed: {e}")))?;
62 Ok(Some(creed))
63 }
64 None => Ok(None),
65 }
66 }
67
68 pub async fn load_creed_by_fighter(&self, fighter_id: &FighterId) -> PunchResult<Option<Creed>> {
70 let fighter_str = fighter_id.to_string();
71 let conn = self.conn.lock().await;
72 let mut stmt = conn
73 .prepare("SELECT creed_data FROM creeds WHERE fighter_id = ?1")
74 .map_err(|e| PunchError::Memory(format!("failed to prepare creed query: {e}")))?;
75
76 let result: Option<String> = stmt
77 .query_row([&fighter_str], |row| row.get(0))
78 .ok();
79
80 match result {
81 Some(data) => {
82 let creed: Creed = serde_json::from_str(&data)
83 .map_err(|e| PunchError::Memory(format!("failed to deserialize creed: {e}")))?;
84 Ok(Some(creed))
85 }
86 None => Ok(None),
87 }
88 }
89
90 pub async fn list_creeds(&self) -> PunchResult<Vec<Creed>> {
92 let conn = self.conn.lock().await;
93 let mut stmt = conn
94 .prepare("SELECT creed_data FROM creeds ORDER BY updated_at DESC")
95 .map_err(|e| PunchError::Memory(format!("failed to list creeds: {e}")))?;
96
97 let rows = stmt
98 .query_map([], |row| {
99 let data: String = row.get(0)?;
100 Ok(data)
101 })
102 .map_err(|e| PunchError::Memory(format!("failed to read creed rows: {e}")))?;
103
104 let mut creeds = Vec::new();
105 for row in rows {
106 let data = row.map_err(|e| PunchError::Memory(format!("failed to read creed: {e}")))?;
107 let creed: Creed = serde_json::from_str(&data)
108 .map_err(|e| PunchError::Memory(format!("failed to deserialize creed: {e}")))?;
109 creeds.push(creed);
110 }
111 Ok(creeds)
112 }
113
114 pub async fn delete_creed(&self, fighter_name: &str) -> PunchResult<()> {
116 let conn = self.conn.lock().await;
117 conn.execute(
118 "DELETE FROM creeds WHERE fighter_name = ?1",
119 [fighter_name],
120 )
121 .map_err(|e| PunchError::Memory(format!("failed to delete creed: {e}")))?;
122 debug!(fighter_name = fighter_name, "creed deleted");
123 Ok(())
124 }
125
126 pub async fn bind_creed_to_fighter(&self, fighter_name: &str, fighter_id: &FighterId) -> 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.load_creed_by_name("charlie").await.unwrap().unwrap();
195 assert_eq!(loaded.identity, "evolved");
196 assert_eq!(loaded.version, 2);
197 assert_eq!(loaded.bout_count, 5);
198 }
199
200 #[tokio::test]
201 async fn test_list_creeds() {
202 let substrate = MemorySubstrate::in_memory().unwrap();
203 substrate.save_creed(&make_creed("delta")).await.unwrap();
204 substrate.save_creed(&make_creed("echo")).await.unwrap();
205 substrate.save_creed(&make_creed("foxtrot")).await.unwrap();
206
207 let creeds = substrate.list_creeds().await.unwrap();
208 assert_eq!(creeds.len(), 3);
209
210 let names: Vec<&str> = creeds.iter().map(|c| c.fighter_name.as_str()).collect();
211 assert!(names.contains(&"delta"));
212 assert!(names.contains(&"echo"));
213 assert!(names.contains(&"foxtrot"));
214 }
215
216 #[tokio::test]
217 async fn test_delete_creed() {
218 let substrate = MemorySubstrate::in_memory().unwrap();
219 substrate.save_creed(&make_creed("golf")).await.unwrap();
220
221 assert!(substrate.load_creed_by_name("golf").await.unwrap().is_some());
223
224 substrate.delete_creed("golf").await.unwrap();
226 assert!(substrate.load_creed_by_name("golf").await.unwrap().is_none());
227 }
228
229 #[tokio::test]
230 async fn test_bind_creed_to_fighter() {
231 let substrate = MemorySubstrate::in_memory().unwrap();
232 let creed = make_creed("hotel");
233 substrate.save_creed(&creed).await.unwrap();
234
235 let loaded = substrate.load_creed_by_name("hotel").await.unwrap().unwrap();
237 assert!(loaded.fighter_id.is_none());
238
239 let fighter_id = FighterId::new();
241 substrate.bind_creed_to_fighter("hotel", &fighter_id).await.unwrap();
242
243 let loaded = substrate.load_creed_by_fighter(&fighter_id).await.unwrap();
246 assert!(loaded.is_some());
247 assert_eq!(loaded.unwrap().fighter_name, "hotel");
248 }
249
250 #[tokio::test]
251 async fn test_load_nonexistent_creed_returns_none() {
252 let substrate = MemorySubstrate::in_memory().unwrap();
253 let by_name = substrate.load_creed_by_name("nonexistent").await.unwrap();
254 assert!(by_name.is_none());
255
256 let by_id = substrate.load_creed_by_fighter(&FighterId::new()).await.unwrap();
257 assert!(by_id.is_none());
258 }
259}