package engine import ( "strings" "testing" "time" ) func TestParseChangeData(t *testing.T) { tests := []struct { name string input string expected map[string]string }{ { "basic key-value pairs", "description: Buy groceries\nstatus: pending\npriority: H", map[string]string{"description": "Buy groceries", "status": "pending", "priority": "H"}, }, { "empty string", "", map[string]string{}, }, { "whitespace only", " \n \n ", map[string]string{}, }, { "value with colon", "description: Fix bug: crash on startup\nstatus: pending", map[string]string{"description": "Fix bug: crash on startup", "status": "pending"}, }, { "trailing newlines", "description: Test\nstatus: pending\n\n", map[string]string{"description": "Test", "status": "pending"}, }, { "no separator", "this line has no colon-space separator", map[string]string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := parseChangeData(tt.input) if len(result) != len(tt.expected) { t.Errorf("len = %d, want %d; got %v", len(result), len(tt.expected), result) return } for k, v := range tt.expected { if result[k] != v { t.Errorf("key %q = %q, want %q", k, result[k], v) } } }) } } func TestDiffFields(t *testing.T) { tests := []struct { name string prev map[string]string curr map[string]string expectChanges int expectSubstr []string // substrings that should appear in changes }{ { "no changes", map[string]string{"description": "Test", "status": "pending"}, map[string]string{"description": "Test", "status": "pending"}, 0, nil, }, { "status change", map[string]string{"status": "pending"}, map[string]string{"status": "completed"}, 1, []string{"status: pending → completed"}, }, { "field added", map[string]string{"description": "Test"}, map[string]string{"description": "Test", "priority": "H"}, 1, []string{"priority: (none) → H"}, }, { "field removed", map[string]string{"description": "Test", "priority": "H"}, map[string]string{"description": "Test"}, 1, []string{"priority: H → (none)"}, }, { "skips uuid and timestamps", map[string]string{"uuid": "abc", "created": "123", "modified": "456", "status": "pending"}, map[string]string{"uuid": "def", "created": "789", "modified": "012", "status": "completed"}, 1, []string{"status"}, }, { "multiple changes", map[string]string{"description": "Old", "status": "pending", "priority": "L"}, map[string]string{"description": "New", "status": "active", "priority": "H"}, 3, nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { changes := diffFields(tt.prev, tt.curr) if len(changes) != tt.expectChanges { t.Errorf("got %d changes, want %d: %v", len(changes), tt.expectChanges, changes) } for _, substr := range tt.expectSubstr { found := false for _, c := range changes { if strings.Contains(c, substr) { found = true break } } if !found { t.Errorf("expected change containing %q, got %v", substr, changes) } } }) } } func TestFormatFieldValue(t *testing.T) { tests := []struct { name string key string value string expected string }{ {"status passthrough", "status", "pending", "pending"}, {"description passthrough", "description", "Buy milk", "Buy milk"}, {"due as unix timestamp", "due", "1771977600", "2026-02-25"}, {"invalid timestamp", "due", "not-a-number", "not-a-number"}, {"scheduled as timestamp", "scheduled", "1771977600", "2026-02-25"}, {"start as timestamp", "start", "1771977600", "2026-02-25"}, {"end as timestamp", "end", "1771977600", "2026-02-25"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := formatFieldValue(tt.key, tt.value) if result != tt.expected { t.Errorf("formatFieldValue(%q, %q) = %q, want %q", tt.key, tt.value, result, tt.expected) } }) } } func TestFormatTaskHistory_Empty(t *testing.T) { result := FormatTaskHistory(nil) if result != "No history found.\n" { t.Errorf("expected 'No history found.\\n', got %q", result) } result = FormatTaskHistory([]HistoryEntry{}) if result != "No history found.\n" { t.Errorf("expected 'No history found.\\n', got %q", result) } } func TestFormatTaskHistory_CreateEntry(t *testing.T) { entries := []HistoryEntry{ { ID: 1, Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC), ChangeType: "create", Data: "description: Buy groceries\nstatus: pending\npriority: H\ntags: errand,shopping", }, } result := FormatTaskHistory(entries) if !strings.Contains(result, "created") { t.Error("expected 'created' in output") } if !strings.Contains(result, "Buy groceries") { t.Error("expected description in output") } if !strings.Contains(result, "priority:H") { t.Error("expected priority in output") } if !strings.Contains(result, "+errand") { t.Error("expected +errand tag in output") } if !strings.Contains(result, "+shopping") { t.Error("expected +shopping tag in output") } } func TestFormatTaskHistory_CreateWithDefaultPriority(t *testing.T) { entries := []HistoryEntry{ { ID: 1, Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC), ChangeType: "create", Data: "description: Simple task\nstatus: pending\npriority: D", }, } result := FormatTaskHistory(entries) // Default priority "D" should not be shown if strings.Contains(result, "priority") { t.Errorf("default priority should not appear in output: %s", result) } } func TestFormatTaskHistory_UpdateDiff(t *testing.T) { entries := []HistoryEntry{ { ID: 1, Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC), ChangeType: "create", Data: "description: Buy groceries\nstatus: pending\npriority: D", }, { ID: 2, Timestamp: time.Date(2026, 2, 18, 11, 0, 0, 0, time.UTC), ChangeType: "update", Data: "description: Buy groceries\nstatus: pending\npriority: H", }, } result := FormatTaskHistory(entries) if !strings.Contains(result, "modified") { t.Error("expected 'modified' in output for update with diff") } // Should show priority change if !strings.Contains(result, "priority") { t.Errorf("expected priority change in diff output: %s", result) } } func TestFormatTaskHistory_DeleteEntry(t *testing.T) { entries := []HistoryEntry{ { ID: 1, Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC), ChangeType: "create", Data: "description: Task\nstatus: pending", }, { ID: 2, Timestamp: time.Date(2026, 2, 18, 12, 0, 0, 0, time.UTC), ChangeType: "delete", Data: "", }, } result := FormatTaskHistory(entries) if !strings.Contains(result, "deleted") { t.Error("expected 'deleted' in output") } } func TestFormatTaskHistory_UpdateWithNoPrev(t *testing.T) { // Update entry without a preceding create (edge case) entries := []HistoryEntry{ { ID: 1, Timestamp: time.Date(2026, 2, 18, 10, 0, 0, 0, time.UTC), ChangeType: "update", Data: "description: Task\nstatus: completed", }, } result := FormatTaskHistory(entries) if !strings.Contains(result, "updated") { t.Errorf("expected 'updated' for update with no prev: %s", result) } } func TestFormatTaskHistory_TimestampFormat(t *testing.T) { entries := []HistoryEntry{ { ID: 1, Timestamp: time.Date(2026, 2, 18, 14, 30, 0, 0, time.UTC), ChangeType: "create", Data: "description: Test\nstatus: pending", }, } result := FormatTaskHistory(entries) if !strings.Contains(result, "2026-02-18 14:30") { t.Errorf("expected timestamp '2026-02-18 14:30' in output: %s", result) } } func TestParseUnixString(t *testing.T) { tests := []struct { name string input string wantErr bool year int }{ {"valid timestamp", "1771977600", false, 2026}, {"zero", "0", false, 1970}, {"invalid", "abc", true, 0}, {"empty", "", true, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parseUnixString(tt.input) if tt.wantErr { if err == nil { t.Error("expected error but got none") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if result.Year() != tt.year { t.Errorf("year = %d, want %d", result.Year(), tt.year) } }) } }