package repositories import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/testutil" ) // === UpdateTx / Version Conflict Tests === func TestTaskRepository_UpdateTx_VersionConflict(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Versioned Task") // Simulate stale version by setting wrong version task.Version = 999 task.Title = "Should Fail" tx := db.Begin() err := repo.UpdateTx(tx, task) tx.Rollback() assert.ErrorIs(t, err, ErrVersionConflict) } func TestTaskRepository_UpdateTx_Success(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Versioned Task") originalVersion := task.Version task.Title = "Updated via Tx" tx := db.Begin() err := repo.UpdateTx(tx, task) require.NoError(t, err) tx.Commit() assert.Equal(t, originalVersion+1, task.Version) found, err := repo.FindByID(task.ID) require.NoError(t, err) assert.Equal(t, "Updated via Tx", found.Title) } func TestTaskRepository_Update_VersionConflict(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Versioned Task") task.Version = 999 // stale version task.Title = "Should Fail" err := repo.Update(task) assert.ErrorIs(t, err, ErrVersionConflict) } // === Version Conflict on State Operations === func TestTaskRepository_Cancel_VersionConflict(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") err := repo.Cancel(task.ID, 999) // wrong version assert.ErrorIs(t, err, ErrVersionConflict) } func TestTaskRepository_Uncancel_VersionConflict(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") err := repo.Uncancel(task.ID, 999) assert.ErrorIs(t, err, ErrVersionConflict) } func TestTaskRepository_Archive_VersionConflict(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") err := repo.Archive(task.ID, 999) assert.ErrorIs(t, err, ErrVersionConflict) } func TestTaskRepository_Unarchive_VersionConflict(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") err := repo.Unarchive(task.ID, 999) assert.ErrorIs(t, err, ErrVersionConflict) } func TestTaskRepository_MarkInProgress(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") err := repo.MarkInProgress(task.ID, task.Version) require.NoError(t, err) found, err := repo.FindByID(task.ID) require.NoError(t, err) assert.True(t, found.InProgress) } func TestTaskRepository_MarkInProgress_VersionConflict(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") err := repo.MarkInProgress(task.ID, 999) assert.ErrorIs(t, err, ErrVersionConflict) } // === CreateCompletionTx === func TestTaskRepository_CreateCompletionTx(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") tx := db.Begin() completion := &models.TaskCompletion{ TaskID: task.ID, CompletedByID: user.ID, CompletedAt: time.Now().UTC(), Notes: "Done via tx", } err := repo.CreateCompletionTx(tx, completion) require.NoError(t, err) tx.Commit() assert.NotZero(t, completion.ID) found, err := repo.FindCompletionByID(completion.ID) require.NoError(t, err) assert.Equal(t, "Done via tx", found.Notes) } // === GetFrequencyByID === func TestTaskRepository_GetFrequencyByID(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) repo := NewTaskRepository(db) frequencies, err := repo.GetAllFrequencies() require.NoError(t, err) require.NotEmpty(t, frequencies) found, err := repo.GetFrequencyByID(frequencies[0].ID) require.NoError(t, err) assert.Equal(t, frequencies[0].Name, found.Name) } func TestTaskRepository_GetFrequencyByID_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) _, err := repo.GetFrequencyByID(9999) assert.Error(t, err) } // === FindByUser === func TestTaskRepository_FindByUser(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1") r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2") testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task A") testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task B") tasks, err := repo.FindByUser(user.ID, []uint{r1.ID, r2.ID}) require.NoError(t, err) assert.Len(t, tasks, 2) } // === CountByResidenceIDs === func TestTaskRepository_CountByResidenceIDs(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1") r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2") testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task 1") testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 2") testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 3") count, err := repo.CountByResidenceIDs([]uint{r1.ID, r2.ID}) require.NoError(t, err) assert.Equal(t, int64(3), count) } func TestTaskRepository_CountByResidenceIDs_Empty(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) count, err := repo.CountByResidenceIDs([]uint{}) require.NoError(t, err) assert.Equal(t, int64(0), count) } // === FindCompletionsByUser === func TestTaskRepository_FindCompletionsByUser(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") c := &models.TaskCompletion{ TaskID: task.ID, CompletedByID: user.ID, CompletedAt: time.Now().UTC(), } require.NoError(t, db.Create(c).Error) completions, err := repo.FindCompletionsByUser(user.ID, []uint{residence.ID}) require.NoError(t, err) assert.Len(t, completions, 1) } // === UpdateCompletion === func TestTaskRepository_UpdateCompletion(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") completion := &models.TaskCompletion{ TaskID: task.ID, CompletedByID: user.ID, CompletedAt: time.Now().UTC(), Notes: "Original", } require.NoError(t, db.Create(completion).Error) completion.Notes = "Updated notes" err := repo.UpdateCompletion(completion) require.NoError(t, err) found, err := repo.FindCompletionByID(completion.ID) require.NoError(t, err) assert.Equal(t, "Updated notes", found.Notes) } // === CompletionImage CRUD === func TestTaskRepository_CompletionImage_CRUD(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") completion := &models.TaskCompletion{ TaskID: task.ID, CompletedByID: user.ID, CompletedAt: time.Now().UTC(), } require.NoError(t, db.Create(completion).Error) // Create image img := &models.TaskCompletionImage{ CompletionID: completion.ID, ImageURL: "https://example.com/img.jpg", } err := repo.CreateCompletionImage(img) require.NoError(t, err) assert.NotZero(t, img.ID) // Find image found, err := repo.FindCompletionImageByID(img.ID) require.NoError(t, err) assert.Equal(t, "https://example.com/img.jpg", found.ImageURL) // Delete image err = repo.DeleteCompletionImage(img.ID) require.NoError(t, err) _, err = repo.FindCompletionImageByID(img.ID) assert.Error(t, err) } func TestTaskRepository_FindCompletionImageByID_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) _, err := repo.FindCompletionImageByID(9999) assert.Error(t, err) } // === GetOverdueCountByResidence === func TestTaskRepository_GetOverdueCountByResidence(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1") r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2") now := time.Now().UTC() pastDue := now.AddDate(0, 0, -5) // Overdue task in r1 t1 := &models.Task{ ResidenceID: r1.ID, CreatedByID: user.ID, Title: "Overdue in r1", DueDate: &pastDue, Version: 1, } require.NoError(t, db.Create(t1).Error) // Not overdue task in r2 (future) futureDue := now.AddDate(0, 0, 30) t2 := &models.Task{ ResidenceID: r2.ID, CreatedByID: user.ID, Title: "Future in r2", DueDate: &futureDue, Version: 1, } require.NoError(t, db.Create(t2).Error) countMap, err := repo.GetOverdueCountByResidence([]uint{r1.ID, r2.ID}, now) require.NoError(t, err) assert.Equal(t, 1, countMap[r1.ID]) assert.Equal(t, 0, countMap[r2.ID]) } func TestTaskRepository_GetOverdueCountByResidence_EmptyIDs(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) countMap, err := repo.GetOverdueCountByResidence([]uint{}, time.Now().UTC()) require.NoError(t, err) assert.Empty(t, countMap) } // === GetKanbanDataForMultipleResidences === func TestTaskRepository_GetKanbanDataForMultipleResidences(t *testing.T) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1") r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2") testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task 1") testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 2") board, err := repo.GetKanbanDataForMultipleResidences([]uint{r1.ID, r2.ID}, 30, time.Now().UTC()) require.NoError(t, err) assert.Equal(t, "all", board.ResidenceID) assert.Len(t, board.Columns, 5) // Both tasks should appear in upcoming (no due date) var upcomingCol *models.KanbanColumn for i := range board.Columns { if board.Columns[i].Name == "upcoming_tasks" { upcomingCol = &board.Columns[i] } } require.NotNil(t, upcomingCol) assert.Equal(t, 2, upcomingCol.Count) } // === DB() accessor === func TestTaskRepository_DB(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) assert.NotNil(t, repo.DB()) } // === GetBatchCompletionSummaries === func TestTaskRepository_GetBatchCompletionSummaries_Empty(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) result, err := repo.GetBatchCompletionSummaries([]uint{}, time.Now().UTC(), 10) require.NoError(t, err) assert.Empty(t, result) } func TestTaskRepository_GetBatchCompletionSummaries_WithData(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1") r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2") task1 := testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task 1") task2 := testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 2") now := time.Date(2026, 3, 15, 0, 0, 0, 0, time.UTC) c1 := models.TaskCompletion{ TaskID: task1.ID, CompletedByID: user.ID, CompletedAt: time.Date(2026, 2, 1, 12, 0, 0, 0, time.UTC), CompletedFromColumn: "completed_tasks", } require.NoError(t, db.Create(&c1).Error) c2 := models.TaskCompletion{ TaskID: task2.ID, CompletedByID: user.ID, CompletedAt: time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC), CompletedFromColumn: "overdue_tasks", } require.NoError(t, db.Create(&c2).Error) result, err := repo.GetBatchCompletionSummaries([]uint{r1.ID, r2.ID}, now, 10) require.NoError(t, err) assert.Len(t, result, 2) assert.Equal(t, 1, result[r1.ID].TotalAllTime) assert.Equal(t, 1, result[r2.ID].TotalAllTime) } // === FindCompletionByID not found === func TestTaskRepository_FindCompletionByID_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) _, err := repo.FindCompletionByID(9999) assert.Error(t, err) } // === DeleteCompletion deletes images === func TestTaskRepository_DeleteCompletion_DeletesImages(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewTaskRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") completion := &models.TaskCompletion{ TaskID: task.ID, CompletedByID: user.ID, CompletedAt: time.Now().UTC(), } require.NoError(t, db.Create(completion).Error) // Add images img := &models.TaskCompletionImage{ CompletionID: completion.ID, ImageURL: "https://example.com/img.jpg", } require.NoError(t, db.Create(img).Error) // Delete completion (should cascade to images) err := repo.DeleteCompletion(completion.ID) require.NoError(t, err) // Images should be gone var count int64 db.Model(&models.TaskCompletionImage{}).Where("completion_id = ?", completion.ID).Count(&count) assert.Equal(t, int64(0), count) }