package handlers import ( "testing" "bookhoard/internal/database" "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" ) // Test collection rule matching with various operators func TestEvaluateRule_Equals_StringMatch(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Science Fiction", Valid: true}, } result := handler.evaluateRule(item, "genre", "equals", "Science Fiction") assert.True(t, result) } func TestEvaluateRule_Equals_NoMatch(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Fantasy", Valid: true}, } result := handler.evaluateRule(item, "genre", "equals", "Science Fiction") assert.False(t, result) } func TestEvaluateRule_NotEquals_Matches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Fantasy", Valid: true}, } result := handler.evaluateRule(item, "genre", "not_equals", "Science Fiction") assert.True(t, result) } func TestEvaluateRule_NotEquals_NoMatch(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Science Fiction", Valid: true}, } result := handler.evaluateRule(item, "genre", "not_equals", "Science Fiction") assert.False(t, result) } func TestEvaluateRule_Contains_Matches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Author: pgtype.Text{String: "Isaac Asimov", Valid: true}, } result := handler.evaluateRule(item, "author", "contains", "asimov") assert.True(t, result) } func TestEvaluateRule_Contains_NoMatch(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Author: pgtype.Text{String: "Isaac Asimov", Valid: true}, } result := handler.evaluateRule(item, "author", "contains", "clarke") assert.False(t, result) } func TestEvaluateRule_NotContains_Matches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Author: pgtype.Text{String: "Isaac Asimov", Valid: true}, } result := handler.evaluateRule(item, "author", "not_contains", "clarke") assert.True(t, result) } func TestEvaluateRule_StartsWith_Matches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Series: pgtype.Text{String: "Foundation", Valid: true}, } result := handler.evaluateRule(item, "series", "starts_with", "Foun") assert.True(t, result) } func TestEvaluateRule_StartsWith_NoMatch(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Series: pgtype.Text{String: "Foundation", Valid: true}, } result := handler.evaluateRule(item, "series", "starts_with", "Robots") assert.False(t, result) } func TestEvaluateRule_EndsWith_Matches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Series: pgtype.Text{String: "Foundation", Valid: true}, } result := handler.evaluateRule(item, "series", "ends_with", "tion") assert.True(t, result) } func TestEvaluateRule_GreaterThan_Matches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true}, } result := handler.evaluateRule(item, "copyright_year", "greater_than", "2000") assert.True(t, result) } func TestEvaluateRule_GreaterThan_NoMatch(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ CopyrightYear: pgtype.Int4{Int32: 1999, Valid: true}, } result := handler.evaluateRule(item, "copyright_year", "greater_than", "2000") assert.False(t, result) } func TestEvaluateRule_LessThan_Matches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ CopyrightYear: pgtype.Int4{Int32: 1999, Valid: true}, } result := handler.evaluateRule(item, "copyright_year", "less_than", "2000") assert.True(t, result) } func TestEvaluateRule_LessThan_NoMatch(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true}, } result := handler.evaluateRule(item, "copyright_year", "less_than", "2000") assert.False(t, result) } func TestEvaluateRule_NullField(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{Valid: false}, // NULL } result := handler.evaluateRule(item, "genre", "equals", "Science Fiction") assert.False(t, result, "Should return false for NULL fields") } func TestCompareValues_CaseInsensitive(t *testing.T) { handler := &CollectionHandler{} result := handler.compareValues("Science Fiction", "contains", "science fiction") assert.True(t, result, "Contains should be case-insensitive") result = handler.compareValues("Science Fiction", "contains", "SCIENCE FICTION") assert.True(t, result) } func TestCompareValues_EmptyString(t *testing.T) { handler := &CollectionHandler{} result := handler.compareValues("", "contains", "test") assert.False(t, result, "Empty string should not contain anything") result = handler.compareValues("test", "contains", "") assert.True(t, result, "Non-empty string contains empty string") } func TestCompareValues_NumericEdgeCases(t *testing.T) { handler := &CollectionHandler{} result := handler.compareValues("0", "greater_than", "-1") assert.True(t, result) result = handler.compareValues("9999", "less_than", "10000") assert.True(t, result) } // Test rule checking with multiple rules func TestCheckRulesAgainstBook_MatchesFirstRule(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Science Fiction", Valid: true}, } rules := []map[string]interface{}{ { "field": "genre", "operator": "equals", "value": "Science Fiction", }, { "field": "author", "operator": "contains", "value": "Asimov", }, } reason := handler.checkRulesAgainstBook(item, rules) assert.NotEmpty(t, reason, "Should match at least one rule") assert.Contains(t, reason, "genre") } func TestCheckRulesAgainstBook_MatchesSecondRule(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Fantasy", Valid: true}, Author: pgtype.Text{String: "Isaac Asimov", Valid: true}, CopyrightYear: pgtype.Int4{Int32: 2021, Valid: true}, } rules := []map[string]interface{}{ { "field": "genre", "operator": "equals", "value": "Science Fiction", }, { "field": "author", "operator": "contains", "value": "Asimov", }, } reason := handler.checkRulesAgainstBook(item, rules) assert.NotEmpty(t, reason, "Should match second rule") assert.Contains(t, reason, "author") } func TestCheckRulesAgainstBook_NoMatches(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Mystery", Valid: true}, Author: pgtype.Text{String: "Agatha Christie", Valid: true}, CopyrightYear: pgtype.Int4{Int32: 1950, Valid: true}, } rules := []map[string]interface{}{ { "field": "genre", "operator": "equals", "value": "Science Fiction", }, { "field": "author", "operator": "contains", "value": "Asimov", }, } reason := handler.checkRulesAgainstBook(item, rules) assert.Empty(t, reason, "Should not match any rules") } func TestCheckRulesAgainstBook_EmptyRules(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Science Fiction", Valid: true}, } rules := []map[string]interface{}{} reason := handler.checkRulesAgainstBook(item, rules) assert.Empty(t, reason, "Empty rules should return empty reason") } // Test complex rule scenarios func TestComplexRule_MultipleConditions(t *testing.T) { handler := &CollectionHandler{} // Book: "Foundation" by Isaac Asimov, Sci-Fi, 1951 item := database.ListMediaItemsRow{ Title: "Foundation", Author: pgtype.Text{String: "Isaac Asimov", Valid: true}, Genre: pgtype.Text{String: "Science Fiction", Valid: true}, CopyrightYear: pgtype.Int4{Int32: 1951, Valid: true}, Series: pgtype.Text{String: "Foundation", Valid: true}, } tests := []struct { name string field string operator string value string expected bool }{ { name: "Match genre exactly", field: "genre", operator: "equals", value: "Science Fiction", expected: true, }, { name: "Match author substring", field: "author", operator: "contains", value: "asimov", expected: true, }, { name: "Match year greater than", field: "copyright_year", operator: "greater_than", value: "1950", expected: true, }, { name: "Match year less than", field: "copyright_year", operator: "less_than", value: "2000", expected: true, }, { name: "Match series starts with", field: "series", operator: "starts_with", value: "Found", expected: true, }, { name: "Match series ends with", field: "series", operator: "ends_with", value: "tion", expected: true, }, { name: "No match for genre", field: "genre", operator: "equals", value: "Fantasy", expected: false, }, { name: "No match for author", field: "author", operator: "contains", value: "Tolkien", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := handler.evaluateRule(item, tt.field, tt.operator, tt.value) assert.Equal(t, tt.expected, result) }) } } // Test edge cases and error handling func TestEvaluateRule_InvalidOperator(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Science Fiction", Valid: true}, } result := handler.evaluateRule(item, "genre", "invalid_operator", "Science Fiction") assert.False(t, result, "Invalid operator should return false") } func TestEvaluateRule_NonExistentField(t *testing.T) { handler := &CollectionHandler{} item := database.ListMediaItemsRow{ Genre: pgtype.Text{String: "Science Fiction", Valid: true}, } result := handler.evaluateRule(item, "nonexistent_field", "equals", "value") assert.False(t, result, "Non-existent field should return false") } func TestCompareValues_InvalidOperator(t *testing.T) { handler := &CollectionHandler{} result := handler.compareValues("test", "invalid_op", "value") assert.False(t, result, "Invalid comparison operator should return false") } func TestCompareValues_NumericConversion(t *testing.T) { handler := &CollectionHandler{} // Valid numeric strings result := handler.compareValues("100", "greater_than", "99") assert.True(t, result) result = handler.compareValues("99", "less_than", "100") assert.True(t, result) // Invalid numeric strings (should return false) result = handler.compareValues("abc", "greater_than", "100") assert.False(t, result, "Non-numeric string should not compare as greater than") result = handler.compareValues("100", "greater_than", "xyz") assert.False(t, result, "Non-numeric value should not allow comparison") }