Skip to main content

aida_core/
import.rs

1// trace:FR-0226 | ai:claude:high
2//! Import functionality for legacy/incompatible requirements databases.
3//!
4//! This module provides:
5//! - Schema validation to detect incompatible records before import
6//! - User-configurable handling of incompatible data (skip/convert/abort)
7//! - Backup of original database before import
8//! - Summary reporting of import results
9
10use anyhow::{Context, Result};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14use std::fs;
15use std::path::Path;
16use uuid::Uuid;
17
18use crate::models::{
19    Requirement, RequirementPriority, RequirementStatus, RequirementType, RequirementsStore,
20};
21
22/// Represents an issue found during import validation
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ImportIssue {
25    /// The SPEC-ID or identifier of the problematic record
26    pub record_id: String,
27    /// Title of the record (for display)
28    pub record_title: String,
29    /// The type of issue
30    pub issue_type: ImportIssueType,
31    /// Human-readable description of the issue
32    pub description: String,
33    /// Whether this issue can be auto-converted
34    pub can_convert: bool,
35    /// Suggested conversion value (if applicable)
36    pub suggested_conversion: Option<String>,
37}
38
39/// Types of issues that can occur during import
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub enum ImportIssueType {
42    /// Unknown requirement type variant
43    UnknownType(String),
44    /// Unknown status variant
45    UnknownStatus(String),
46    /// Unknown priority variant
47    UnknownPriority(String),
48    /// Invalid relationship target (referenced SPEC-ID doesn't exist)
49    InvalidRelationshipTarget(String),
50    /// Duplicate SPEC-ID found
51    DuplicateSpecId(String),
52    /// Missing required field
53    MissingRequiredField(String),
54    /// Invalid date format
55    InvalidDateFormat(String),
56    /// Schema version mismatch
57    SchemaVersionMismatch(String),
58}
59
60impl std::fmt::Display for ImportIssueType {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            ImportIssueType::UnknownType(t) => write!(f, "Unknown type: {}", t),
64            ImportIssueType::UnknownStatus(s) => write!(f, "Unknown status: {}", s),
65            ImportIssueType::UnknownPriority(p) => write!(f, "Unknown priority: {}", p),
66            ImportIssueType::InvalidRelationshipTarget(id) => {
67                write!(f, "Invalid relationship target: {}", id)
68            }
69            ImportIssueType::DuplicateSpecId(id) => write!(f, "Duplicate SPEC-ID: {}", id),
70            ImportIssueType::MissingRequiredField(field) => {
71                write!(f, "Missing required field: {}", field)
72            }
73            ImportIssueType::InvalidDateFormat(field) => {
74                write!(f, "Invalid date in field: {}", field)
75            }
76            ImportIssueType::SchemaVersionMismatch(v) => write!(f, "Schema version: {}", v),
77        }
78    }
79}
80
81/// How to handle a specific issue during import
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
83pub enum IssueResolution {
84    /// Skip the record with this issue
85    #[default]
86    Skip,
87    /// Convert to a default value
88    ConvertToDefault,
89    /// Abort the entire import
90    Abort,
91}
92
93impl std::fmt::Display for IssueResolution {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            IssueResolution::Skip => write!(f, "Skip"),
97            IssueResolution::ConvertToDefault => write!(f, "Convert"),
98            IssueResolution::Abort => write!(f, "Abort"),
99        }
100    }
101}
102
103/// Result of validating an import file
104#[derive(Debug, Clone, Default)]
105pub struct ImportValidation {
106    /// All issues found during validation
107    pub issues: Vec<ImportIssue>,
108    /// Records that can be imported without issues
109    pub valid_record_count: usize,
110    /// Records with issues
111    pub problematic_record_count: usize,
112    /// Whether the import can proceed (no fatal issues)
113    pub can_proceed: bool,
114    /// Raw parsed store (if parsing succeeded)
115    pub parsed_store: Option<RawImportStore>,
116}
117
118impl ImportValidation {
119    /// Returns true if there are any issues
120    pub fn has_issues(&self) -> bool {
121        !self.issues.is_empty()
122    }
123
124    /// Returns issues grouped by record ID
125    pub fn issues_by_record(&self) -> HashMap<String, Vec<&ImportIssue>> {
126        let mut map: HashMap<String, Vec<&ImportIssue>> = HashMap::new();
127        for issue in &self.issues {
128            map.entry(issue.record_id.clone()).or_default().push(issue);
129        }
130        map
131    }
132
133    /// Returns issues of a specific type
134    pub fn issues_of_type(&self, issue_type: &ImportIssueType) -> Vec<&ImportIssue> {
135        self.issues
136            .iter()
137            .filter(|i| std::mem::discriminant(&i.issue_type) == std::mem::discriminant(issue_type))
138            .collect()
139    }
140
141    /// Count of unknown type issues
142    pub fn unknown_type_count(&self) -> usize {
143        self.issues
144            .iter()
145            .filter(|i| matches!(i.issue_type, ImportIssueType::UnknownType(_)))
146            .count()
147    }
148
149    /// Count of unknown status issues
150    pub fn unknown_status_count(&self) -> usize {
151        self.issues
152            .iter()
153            .filter(|i| matches!(i.issue_type, ImportIssueType::UnknownStatus(_)))
154            .count()
155    }
156
157    /// Count of invalid relationship issues
158    pub fn invalid_relationship_count(&self) -> usize {
159        self.issues
160            .iter()
161            .filter(|i| matches!(i.issue_type, ImportIssueType::InvalidRelationshipTarget(_)))
162            .count()
163    }
164}
165
166/// Summary of an import operation
167#[derive(Debug, Clone, Default)]
168pub struct ImportSummary {
169    /// Total records in source file
170    pub total_records: usize,
171    /// Records successfully imported
172    pub imported_count: usize,
173    /// Records skipped due to issues
174    pub skipped_count: usize,
175    /// Records converted (type/status changed to default)
176    pub converted_count: usize,
177    /// Relationships skipped (target not imported)
178    pub relationships_skipped: usize,
179    /// Path to backup file (if created)
180    pub backup_path: Option<String>,
181    /// Any warnings generated during import
182    pub warnings: Vec<String>,
183    /// Time taken for import
184    pub duration_ms: u64,
185}
186
187impl ImportSummary {
188    /// Returns true if the import was fully successful (no skips or conversions)
189    pub fn is_clean(&self) -> bool {
190        self.skipped_count == 0 && self.converted_count == 0 && self.relationships_skipped == 0
191    }
192}
193
194/// Configuration for an import operation
195#[derive(Debug, Clone)]
196pub struct ImportConfig {
197    /// How to handle unknown types
198    pub unknown_type_resolution: IssueResolution,
199    /// How to handle unknown statuses
200    pub unknown_status_resolution: IssueResolution,
201    /// How to handle unknown priorities
202    pub unknown_priority_resolution: IssueResolution,
203    /// Whether to create a backup before import
204    pub create_backup: bool,
205    /// Whether to merge with existing data or replace
206    pub merge_mode: ImportMergeMode,
207    /// Default type to use when converting unknown types
208    pub default_type: RequirementType,
209    /// Default status to use when converting unknown statuses
210    pub default_status: RequirementStatus,
211    /// Default priority to use when converting unknown priorities
212    pub default_priority: RequirementPriority,
213}
214
215impl Default for ImportConfig {
216    fn default() -> Self {
217        Self {
218            unknown_type_resolution: IssueResolution::Skip,
219            unknown_status_resolution: IssueResolution::ConvertToDefault,
220            unknown_priority_resolution: IssueResolution::ConvertToDefault,
221            create_backup: true,
222            merge_mode: ImportMergeMode::Replace,
223            default_type: RequirementType::Functional,
224            default_status: RequirementStatus::Draft,
225            default_priority: RequirementPriority::Medium,
226        }
227    }
228}
229
230/// How to merge imported data with existing data
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
232pub enum ImportMergeMode {
233    /// Replace existing database entirely
234    #[default]
235    Replace,
236    /// Merge with existing, keeping existing on conflict
237    MergeKeepExisting,
238    /// Merge with existing, preferring imported on conflict
239    MergePreferImported,
240}
241
242/// Raw representation of a requirement during import (before validation)
243/// Uses String for enum fields to allow detection of unknown values
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct RawRequirement {
246    #[serde(default)]
247    pub id: Option<Uuid>,
248    #[serde(default)]
249    pub spec_id: Option<String>,
250    #[serde(default)]
251    pub title: String,
252    #[serde(default)]
253    pub description: String,
254    #[serde(default)]
255    pub req_type: Option<String>,
256    #[serde(default)]
257    pub status: Option<String>,
258    #[serde(default)]
259    pub priority: Option<String>,
260    #[serde(default)]
261    pub owner: String,
262    #[serde(default)]
263    pub feature: String,
264    #[serde(default)]
265    pub tags: HashSet<String>,
266    #[serde(default)]
267    pub created_at: Option<DateTime<Utc>>,
268    #[serde(default)]
269    pub modified_at: Option<DateTime<Utc>>,
270    #[serde(default)]
271    pub relationships: Vec<RawRelationship>,
272    #[serde(default)]
273    pub comments: Vec<serde_yaml::Value>,
274    #[serde(default)]
275    pub history: Vec<serde_yaml::Value>,
276    #[serde(default)]
277    pub urls: Vec<serde_yaml::Value>,
278    #[serde(default)]
279    pub custom_fields: HashMap<String, serde_yaml::Value>,
280    #[serde(default)]
281    pub ai_evaluation: Option<serde_yaml::Value>,
282}
283
284/// Raw relationship for import
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct RawRelationship {
287    #[serde(default)]
288    pub target_id: Option<Uuid>,
289    #[serde(default)]
290    pub target_spec_id: Option<String>,
291    #[serde(default)]
292    pub rel_type: Option<String>,
293}
294
295/// Raw store structure for initial parsing
296#[derive(Debug, Clone, Serialize, Deserialize, Default)]
297pub struct RawImportStore {
298    #[serde(default)]
299    pub name: String,
300    #[serde(default)]
301    pub title: String,
302    #[serde(default)]
303    pub description: String,
304    #[serde(default)]
305    pub requirements: Vec<RawRequirement>,
306    #[serde(default)]
307    pub users: Vec<serde_yaml::Value>,
308    #[serde(default)]
309    pub teams: Vec<serde_yaml::Value>,
310    #[serde(default)]
311    pub features: Vec<serde_yaml::Value>,
312    #[serde(default)]
313    pub id_config: Option<serde_yaml::Value>,
314    #[serde(default)]
315    pub type_definitions: Vec<serde_yaml::Value>,
316    #[serde(default)]
317    pub relationship_definitions: Vec<serde_yaml::Value>,
318    #[serde(default)]
319    pub reaction_definitions: Vec<serde_yaml::Value>,
320    #[serde(default)]
321    pub ai_prompts: Option<serde_yaml::Value>,
322    #[serde(default)]
323    pub baselines: Vec<serde_yaml::Value>,
324    // Capture unknown fields
325    #[serde(flatten)]
326    pub extra_fields: HashMap<String, serde_yaml::Value>,
327}
328
329/// Known requirement type strings (for validation)
330const KNOWN_TYPES: &[&str] = &[
331    "Functional",
332    "NonFunctional",
333    "Non-Functional",
334    "System",
335    "User",
336    "ChangeRequest",
337    "Change Request",
338    "Bug",
339    "Epic",
340    "Story",
341    "Task",
342    "Spike",
343    "Sprint",
344    "Folder",
345];
346
347/// Known status strings (for validation)
348const KNOWN_STATUSES: &[&str] = &[
349    "Draft",
350    "Approved",
351    "Planned",
352    "In Progress",
353    "Completed",
354    "Rejected",
355];
356
357/// Known priority strings (for validation)
358const KNOWN_PRIORITIES: &[&str] = &["High", "Medium", "Low"];
359
360/// Parse a type string into RequirementType
361pub fn parse_requirement_type(s: &str) -> Option<RequirementType> {
362    match s {
363        "Functional" => Some(RequirementType::Functional),
364        "NonFunctional" | "Non-Functional" => Some(RequirementType::NonFunctional),
365        "System" => Some(RequirementType::System),
366        "User" => Some(RequirementType::User),
367        "ChangeRequest" | "Change Request" => Some(RequirementType::ChangeRequest),
368        "Bug" => Some(RequirementType::Bug),
369        "Epic" => Some(RequirementType::Epic),
370        "Story" => Some(RequirementType::Story),
371        "Task" => Some(RequirementType::Task),
372        "Spike" => Some(RequirementType::Spike),
373        "Sprint" => Some(RequirementType::Sprint),
374        "Folder" => Some(RequirementType::Folder),
375        _ => None,
376    }
377}
378
379/// Parse a status string into RequirementStatus
380pub fn parse_requirement_status(s: &str) -> Option<RequirementStatus> {
381    match s {
382        "Draft" => Some(RequirementStatus::Draft),
383        "Approved" => Some(RequirementStatus::Approved),
384        "Completed" => Some(RequirementStatus::Completed),
385        "Rejected" => Some(RequirementStatus::Rejected),
386        _ => None,
387    }
388}
389
390/// Parse a priority string into RequirementPriority
391pub fn parse_requirement_priority(s: &str) -> Option<RequirementPriority> {
392    match s {
393        "High" => Some(RequirementPriority::High),
394        "Medium" => Some(RequirementPriority::Medium),
395        "Low" => Some(RequirementPriority::Low),
396        _ => None,
397    }
398}
399
400/// Validate an import file and return validation results
401pub fn validate_import_file(path: &Path) -> Result<ImportValidation> {
402    let content = fs::read_to_string(path)
403        .with_context(|| format!("Failed to read import file: {:?}", path))?;
404
405    validate_import_content(&content)
406}
407
408/// Validate import content (YAML string)
409pub fn validate_import_content(content: &str) -> Result<ImportValidation> {
410    let mut validation = ImportValidation::default();
411
412    // Try to parse as raw store
413    let raw_store: RawImportStore = match serde_yaml::from_str(content) {
414        Ok(store) => store,
415        Err(e) => {
416            validation.can_proceed = false;
417            validation.issues.push(ImportIssue {
418                record_id: "PARSE_ERROR".to_string(),
419                record_title: "File Parse Error".to_string(),
420                issue_type: ImportIssueType::SchemaVersionMismatch(e.to_string()),
421                description: format!("Failed to parse YAML: {}", e),
422                can_convert: false,
423                suggested_conversion: None,
424            });
425            return Ok(validation);
426        }
427    };
428
429    // Collect all SPEC-IDs for relationship validation
430    let all_spec_ids: HashSet<String> = raw_store
431        .requirements
432        .iter()
433        .filter_map(|r| r.spec_id.clone())
434        .collect();
435
436    // Check for unknown extra fields at store level
437    if !raw_store.extra_fields.is_empty() {
438        for field_name in raw_store.extra_fields.keys() {
439            validation.issues.push(ImportIssue {
440                record_id: "STORE".to_string(),
441                record_title: "Database Schema".to_string(),
442                issue_type: ImportIssueType::SchemaVersionMismatch(format!(
443                    "Unknown field: {}",
444                    field_name
445                )),
446                description: format!("Unknown field '{}' in database schema", field_name),
447                can_convert: true,
448                suggested_conversion: Some("Field will be ignored".to_string()),
449            });
450        }
451    }
452
453    // Validate each requirement
454    let mut seen_spec_ids: HashSet<String> = HashSet::new();
455
456    for raw_req in &raw_store.requirements {
457        let record_id = raw_req
458            .spec_id
459            .clone()
460            .unwrap_or_else(|| raw_req.id.map(|id| id.to_string()).unwrap_or_default());
461        let record_title = raw_req.title.clone();
462
463        // Check for duplicate SPEC-IDs
464        if let Some(ref spec_id) = raw_req.spec_id {
465            if !seen_spec_ids.insert(spec_id.clone()) {
466                validation.issues.push(ImportIssue {
467                    record_id: record_id.clone(),
468                    record_title: record_title.clone(),
469                    issue_type: ImportIssueType::DuplicateSpecId(spec_id.clone()),
470                    description: format!("Duplicate SPEC-ID: {}", spec_id),
471                    can_convert: false,
472                    suggested_conversion: None,
473                });
474            }
475        }
476
477        // Validate requirement type
478        if let Some(ref type_str) = raw_req.req_type {
479            if parse_requirement_type(type_str).is_none() {
480                validation.issues.push(ImportIssue {
481                    record_id: record_id.clone(),
482                    record_title: record_title.clone(),
483                    issue_type: ImportIssueType::UnknownType(type_str.clone()),
484                    description: format!(
485                        "Unknown requirement type '{}'. Known types: {}",
486                        type_str,
487                        KNOWN_TYPES.join(", ")
488                    ),
489                    can_convert: true,
490                    suggested_conversion: Some("Functional".to_string()),
491                });
492                validation.problematic_record_count += 1;
493                continue;
494            }
495        }
496
497        // Validate status
498        if let Some(ref status_str) = raw_req.status {
499            if parse_requirement_status(status_str).is_none() {
500                validation.issues.push(ImportIssue {
501                    record_id: record_id.clone(),
502                    record_title: record_title.clone(),
503                    issue_type: ImportIssueType::UnknownStatus(status_str.clone()),
504                    description: format!(
505                        "Unknown status '{}'. Known statuses: {}",
506                        status_str,
507                        KNOWN_STATUSES.join(", ")
508                    ),
509                    can_convert: true,
510                    suggested_conversion: Some("Draft".to_string()),
511                });
512            }
513        }
514
515        // Validate priority
516        if let Some(ref priority_str) = raw_req.priority {
517            if parse_requirement_priority(priority_str).is_none() {
518                validation.issues.push(ImportIssue {
519                    record_id: record_id.clone(),
520                    record_title: record_title.clone(),
521                    issue_type: ImportIssueType::UnknownPriority(priority_str.clone()),
522                    description: format!(
523                        "Unknown priority '{}'. Known priorities: {}",
524                        priority_str,
525                        KNOWN_PRIORITIES.join(", ")
526                    ),
527                    can_convert: true,
528                    suggested_conversion: Some("Medium".to_string()),
529                });
530            }
531        }
532
533        // Validate relationships
534        for rel in &raw_req.relationships {
535            if let Some(ref target_spec_id) = rel.target_spec_id {
536                if !all_spec_ids.contains(target_spec_id) {
537                    validation.issues.push(ImportIssue {
538                        record_id: record_id.clone(),
539                        record_title: record_title.clone(),
540                        issue_type: ImportIssueType::InvalidRelationshipTarget(
541                            target_spec_id.clone(),
542                        ),
543                        description: format!(
544                            "Relationship references non-existent requirement: {}",
545                            target_spec_id
546                        ),
547                        can_convert: true,
548                        suggested_conversion: Some("Relationship will be skipped".to_string()),
549                    });
550                }
551            }
552        }
553
554        // Check for missing required fields
555        if raw_req.title.trim().is_empty() {
556            validation.issues.push(ImportIssue {
557                record_id: record_id.clone(),
558                record_title: "(untitled)".to_string(),
559                issue_type: ImportIssueType::MissingRequiredField("title".to_string()),
560                description: "Requirement is missing a title".to_string(),
561                can_convert: true,
562                suggested_conversion: Some("Untitled Requirement".to_string()),
563            });
564        }
565
566        validation.valid_record_count += 1;
567    }
568
569    // Update counts
570    validation.problematic_record_count = validation
571        .issues
572        .iter()
573        .map(|i| i.record_id.clone())
574        .collect::<HashSet<_>>()
575        .len();
576
577    // Determine if import can proceed
578    // Fatal issues: parse errors, duplicate SPEC-IDs
579    let has_fatal_issues = validation.issues.iter().any(|i| {
580        matches!(
581            i.issue_type,
582            ImportIssueType::SchemaVersionMismatch(_) | ImportIssueType::DuplicateSpecId(_)
583        ) && !i.can_convert
584    });
585
586    validation.can_proceed = !has_fatal_issues;
587    validation.parsed_store = Some(raw_store);
588
589    Ok(validation)
590}
591
592/// Create a backup of the current requirements file
593pub fn create_backup(source_path: &Path) -> Result<String> {
594    if !source_path.exists() {
595        return Ok(String::new());
596    }
597
598    let backup_path = source_path.with_extension("yaml.backup");
599    fs::copy(source_path, &backup_path)
600        .with_context(|| format!("Failed to create backup at {:?}", backup_path))?;
601
602    Ok(backup_path.to_string_lossy().to_string())
603}
604
605/// Execute an import operation with the given configuration
606pub fn execute_import(
607    import_content: &str,
608    target_path: &Path,
609    config: &ImportConfig,
610    validation: &ImportValidation,
611) -> Result<ImportSummary> {
612    let start_time = std::time::Instant::now();
613    let mut summary = ImportSummary::default();
614
615    // Create backup if requested
616    if config.create_backup && target_path.exists() {
617        summary.backup_path = Some(create_backup(target_path)?);
618    }
619
620    // Get the raw store from validation
621    let raw_store = validation
622        .parsed_store
623        .as_ref()
624        .context("No parsed store available - validation must be run first")?;
625
626    summary.total_records = raw_store.requirements.len();
627
628    // Build set of records to skip based on issues and resolution settings
629    let mut skip_records: HashSet<String> = HashSet::new();
630    let mut convert_records: HashSet<String> = HashSet::new();
631
632    for issue in &validation.issues {
633        let resolution = match &issue.issue_type {
634            ImportIssueType::UnknownType(_) => config.unknown_type_resolution,
635            ImportIssueType::UnknownStatus(_) => config.unknown_status_resolution,
636            ImportIssueType::UnknownPriority(_) => config.unknown_priority_resolution,
637            ImportIssueType::DuplicateSpecId(_) => IssueResolution::Skip,
638            ImportIssueType::InvalidRelationshipTarget(_) => IssueResolution::Skip, // Relationships handled separately
639            ImportIssueType::MissingRequiredField(_) => IssueResolution::ConvertToDefault,
640            ImportIssueType::InvalidDateFormat(_) => IssueResolution::ConvertToDefault,
641            ImportIssueType::SchemaVersionMismatch(_) => IssueResolution::Skip,
642        };
643
644        match resolution {
645            IssueResolution::Abort => {
646                anyhow::bail!("Import aborted due to issue: {}", issue.description);
647            }
648            IssueResolution::Skip => {
649                skip_records.insert(issue.record_id.clone());
650            }
651            IssueResolution::ConvertToDefault => {
652                convert_records.insert(issue.record_id.clone());
653            }
654        }
655    }
656
657    // Build the target store
658    let mut target_store = RequirementsStore::new();
659    target_store.name = raw_store.name.clone();
660    target_store.title = raw_store.title.clone();
661    target_store.description = raw_store.description.clone();
662
663    // Track which SPEC-IDs were imported (for relationship validation)
664    let mut imported_spec_ids: HashSet<String> = HashSet::new();
665
666    // Convert raw requirements to proper requirements
667    for raw_req in &raw_store.requirements {
668        let record_id = raw_req
669            .spec_id
670            .clone()
671            .unwrap_or_else(|| raw_req.id.map(|id| id.to_string()).unwrap_or_default());
672
673        // Skip if marked for skipping
674        if skip_records.contains(&record_id) {
675            summary.skipped_count += 1;
676            continue;
677        }
678
679        let needs_conversion = convert_records.contains(&record_id);
680
681        // Convert the requirement
682        let mut req = Requirement::new(
683            if raw_req.title.trim().is_empty() {
684                "Untitled Requirement".to_string()
685            } else {
686                raw_req.title.clone()
687            },
688            raw_req.description.clone(),
689        );
690
691        // Set ID if present
692        if let Some(id) = raw_req.id {
693            req.id = id;
694        }
695
696        // Set SPEC-ID
697        req.spec_id = raw_req.spec_id.clone();
698        if let Some(ref spec_id) = req.spec_id {
699            imported_spec_ids.insert(spec_id.clone());
700        }
701
702        // Set type (with conversion if needed)
703        if let Some(ref type_str) = raw_req.req_type {
704            req.req_type = parse_requirement_type(type_str).unwrap_or_else(|| {
705                if needs_conversion {
706                    summary.converted_count += 1;
707                }
708                config.default_type.clone()
709            });
710        }
711
712        // Set status (with conversion if needed)
713        if let Some(ref status_str) = raw_req.status {
714            req.status = parse_requirement_status(status_str).unwrap_or_else(|| {
715                if needs_conversion {
716                    summary.converted_count += 1;
717                }
718                config.default_status.clone()
719            });
720        }
721
722        // Set priority (with conversion if needed)
723        if let Some(ref priority_str) = raw_req.priority {
724            req.priority = parse_requirement_priority(priority_str).unwrap_or_else(|| {
725                if needs_conversion {
726                    summary.converted_count += 1;
727                }
728                config.default_priority.clone()
729            });
730        }
731
732        // Set other fields
733        req.owner = raw_req.owner.clone();
734        req.feature = raw_req.feature.clone();
735        req.tags = raw_req.tags.clone();
736
737        // Set timestamps
738        if let Some(created_at) = raw_req.created_at {
739            req.created_at = created_at;
740        }
741        if let Some(modified_at) = raw_req.modified_at {
742            req.modified_at = modified_at;
743        }
744
745        target_store.requirements.push(req);
746        summary.imported_count += 1;
747    }
748
749    // Second pass: add relationships (only for imported records)
750    for (idx, raw_req) in raw_store.requirements.iter().enumerate() {
751        let record_id = raw_req
752            .spec_id
753            .clone()
754            .unwrap_or_else(|| raw_req.id.map(|id| id.to_string()).unwrap_or_default());
755
756        if skip_records.contains(&record_id) {
757            continue;
758        }
759
760        // Find the corresponding requirement in target_store
761        if idx < target_store.requirements.len() {
762            for raw_rel in &raw_req.relationships {
763                // Check if target exists in imported records
764                let target_exists = raw_rel
765                    .target_spec_id
766                    .as_ref()
767                    .map(|id| imported_spec_ids.contains(id))
768                    .unwrap_or(false);
769
770                if !target_exists {
771                    summary.relationships_skipped += 1;
772                    summary.warnings.push(format!(
773                        "Skipped relationship from {} to {} (target not imported)",
774                        record_id,
775                        raw_rel.target_spec_id.as_deref().unwrap_or("unknown")
776                    ));
777                }
778                // Note: We'd add the relationship here if we had the full conversion logic
779                // For now, relationships are handled during the full store deserialization
780            }
781        }
782    }
783
784    // Save the imported store
785    let yaml = serde_yaml::to_string(&target_store)?;
786    fs::write(target_path, yaml)?;
787
788    summary.duration_ms = start_time.elapsed().as_millis() as u64;
789
790    Ok(summary)
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796
797    #[test]
798    fn test_parse_requirement_type() {
799        assert_eq!(
800            parse_requirement_type("Functional"),
801            Some(RequirementType::Functional)
802        );
803        assert_eq!(
804            parse_requirement_type("NonFunctional"),
805            Some(RequirementType::NonFunctional)
806        );
807        assert_eq!(
808            parse_requirement_type("Non-Functional"),
809            Some(RequirementType::NonFunctional)
810        );
811        assert_eq!(
812            parse_requirement_type("ChangeRequest"),
813            Some(RequirementType::ChangeRequest)
814        );
815        assert_eq!(parse_requirement_type("UnknownType"), None);
816    }
817
818    #[test]
819    fn test_parse_requirement_status() {
820        assert_eq!(
821            parse_requirement_status("Draft"),
822            Some(RequirementStatus::Draft)
823        );
824        assert_eq!(
825            parse_requirement_status("Approved"),
826            Some(RequirementStatus::Approved)
827        );
828        assert_eq!(parse_requirement_status("InProgress"), None);
829    }
830
831    #[test]
832    fn test_validate_empty_content() {
833        let content = "requirements: []";
834        let result = validate_import_content(content).unwrap();
835        assert!(result.can_proceed);
836        assert_eq!(result.valid_record_count, 0);
837    }
838
839    #[test]
840    fn test_validate_unknown_type() {
841        let content = r#"
842requirements:
843  - title: Test Requirement
844    description: Test description
845    req_type: UnknownCustomType
846    status: Draft
847"#;
848        let result = validate_import_content(content).unwrap();
849        assert!(result.has_issues());
850        assert_eq!(result.unknown_type_count(), 1);
851    }
852
853    #[test]
854    fn test_validate_invalid_relationship() {
855        let content = r#"
856requirements:
857  - spec_id: FR-0001
858    title: Test Requirement
859    description: Test
860    relationships:
861      - target_spec_id: FR-9999
862        rel_type: parent
863"#;
864        let result = validate_import_content(content).unwrap();
865        assert!(result.has_issues());
866        assert_eq!(result.invalid_relationship_count(), 1);
867    }
868}