package utils
import (
"skraak_mcp/db"
"strings"
"testing"
"time"
)
func TestIsAudioMoth(t *testing.T) {
t.Run("should identify AudioMoth files by artist field", func(t *testing.T) {
if !IsAudioMoth("", "AudioMoth") {
t.Error("Should identify AudioMoth by artist field")
}
if !IsAudioMoth("", "AudioMoth 123456") {
t.Error("Should identify AudioMoth with ID in artist field")
}
if IsAudioMoth("", "Other Artist") {
t.Error("Should not identify non-AudioMoth artist")
}
})
t.Run("should identify AudioMoth files by comment field", func(t *testing.T) {
if !IsAudioMoth("Recorded by AudioMoth...", "") {
t.Error("Should identify AudioMoth by comment field")
}
if IsAudioMoth("Regular recording comment", "") {
t.Error("Should not identify non-AudioMoth comment")
}
})
t.Run("should handle missing metadata", func(t *testing.T) {
if IsAudioMoth("", "") {
t.Error("Should not identify empty strings as AudioMoth")
}
})
t.Run("should be case insensitive", func(t *testing.T) {
if !IsAudioMoth("", "audiomoth") {
t.Error("Should be case insensitive")
}
if !IsAudioMoth("", "AUDIOMOTH") {
t.Error("Should be case insensitive")
}
})
}
func TestParseAudioMothComment(t *testing.T) {
t.Run("should parse a valid structured AudioMoth comment", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C."
result, err := ParseAudioMothComment(comment)
if err != nil {
t.Fatalf("Failed to parse comment: %v", err)
}
expected := time.Date(2025, 2, 24, 21, 0, 0, 0, time.FixedZone("UTC+13", 13*3600))
if !result.Timestamp.Equal(expected) {
t.Errorf("Timestamp incorrect: got %v, want %v", result.Timestamp, expected)
}
utc := result.Timestamp.UTC()
expectedUTC := time.Date(2025, 2, 24, 8, 0, 0, 0, time.UTC)
if !utc.Equal(expectedUTC) {
t.Errorf("UTC timestamp incorrect: got %v, want %v", utc, expectedUTC)
}
if result.RecorderID != "248AB50153AB0549" {
t.Errorf("RecorderID incorrect: got %s, want 248AB50153AB0549", result.RecorderID)
}
if result.Gain != db.GainMedium {
t.Errorf("Gain incorrect: got %s, want %s", result.Gain, db.GainMedium)
}
if result.BatteryV != 4.3 {
t.Errorf("BatteryV incorrect: got %f, want 4.3", result.BatteryV)
}
if result.TempC != 15.8 {
t.Errorf("TempC incorrect: got %f, want 15.8", result.TempC)
}
})
t.Run("should return error for invalid comments", func(t *testing.T) {
invalidComments := []string{
"Not an AudioMoth comment",
"Recorded at invalid time format",
"Short comment",
"",
"AudioMoth without proper format",
}
for _, comment := range invalidComments {
_, err := ParseAudioMothComment(comment)
if err == nil {
t.Errorf("Expected error for invalid comment: %s", comment)
}
}
})
t.Run("should handle different timezone formats", func(t *testing.T) {
commentUTCMinus := "Recorded at 10:30:45 15/06/2024 (UTC-5) by AudioMoth 123456789ABCDEF0 at high gain while battery was 3.9V and temperature was 22.1C."
result, err := ParseAudioMothComment(commentUTCMinus)
if err != nil {
t.Fatalf("Failed to parse comment: %v", err)
}
expected := time.Date(2024, 6, 15, 10, 30, 45, 0, time.FixedZone("UTC-5", -5*3600))
if !result.Timestamp.Equal(expected) {
t.Errorf("Timestamp incorrect: got %v, want %v", result.Timestamp, expected)
}
if result.Gain != db.GainHigh {
t.Errorf("Gain incorrect: got %s, want %s", result.Gain, db.GainHigh)
}
if result.BatteryV != 3.9 {
t.Errorf("BatteryV incorrect: got %f, want 3.9", result.BatteryV)
}
if result.TempC != 22.1 {
t.Errorf("TempC incorrect: got %f, want 22.1", result.TempC)
}
})
t.Run("should parse all gain levels", func(t *testing.T) {
testCases := []struct {
gainStr string
expected db.GainLevel
}{
{"low", db.GainLow},
{"low-medium", db.GainLowMedium},
{"medium", db.GainMedium},
{"medium-high", db.GainMediumHigh},
{"high", db.GainHigh},
}
for _, tc := range testCases {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at " + tc.gainStr + " gain while battery was 4.3V and temperature was 15.8C."
result, err := ParseAudioMothComment(comment)
if err != nil {
t.Errorf("Failed to parse comment with gain %s: %v", tc.gainStr, err)
continue
}
if result.Gain != tc.expected {
t.Errorf("Gain incorrect for %s: got %s, want %s", tc.gainStr, result.Gain, tc.expected)
}
}
})
t.Run("should handle negative temperatures", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was -5.2C."
result, err := ParseAudioMothComment(comment)
if err != nil {
t.Fatalf("Failed to parse comment: %v", err)
}
if result.TempC != -5.2 {
t.Errorf("TempC incorrect: got %f, want -5.2", result.TempC)
}
})
t.Run("should fallback to legacy parsing", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C"
result, err := ParseAudioMothComment(comment)
if err == nil {
if result.RecorderID == "" {
t.Error("RecorderID should not be empty")
}
}
})
}
func TestParseGainLevel(t *testing.T) {
testCases := []struct {
input string
expected db.GainLevel
wantErr bool
}{
{"low", db.GainLow, false},
{"LOW", db.GainLow, false},
{" low ", db.GainLow, false},
{"low-medium", db.GainLowMedium, false},
{"medium", db.GainMedium, false},
{"medium-high", db.GainMediumHigh, false},
{"high", db.GainHigh, false},
{"invalid", "", true},
{"", "", true},
{"ultra", "", true},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result, err := parseGainLevel(tc.input)
if tc.wantErr {
if err == nil {
t.Errorf("Expected error for input %q, got nil", tc.input)
}
} else {
if err != nil {
t.Errorf("Unexpected error for input %q: %v", tc.input, err)
}
if result != tc.expected {
t.Errorf("Result incorrect for %q: got %s, want %s", tc.input, result, tc.expected)
}
}
})
}
}
func TestParseAudioMothTimestamp(t *testing.T) {
t.Run("should parse standard timestamp format", func(t *testing.T) {
result, err := parseAudioMothTimestamp("21:00:00", "24/02/2025", "UTC+13")
if err != nil {
t.Fatalf("Failed to parse timestamp: %v", err)
}
expected := time.Date(2025, 2, 24, 21, 0, 0, 0, time.FixedZone("UTC+13", 13*3600))
if !result.Equal(expected) {
t.Errorf("Timestamp incorrect: got %v, want %v", result, expected)
}
})
t.Run("should parse timestamp with +HH format", func(t *testing.T) {
result, err := parseAudioMothTimestamp("10:30:45", "15/06/2024", "+13")
if err != nil {
t.Fatalf("Failed to parse timestamp: %v", err)
}
expected := time.Date(2024, 6, 15, 10, 30, 45, 0, time.FixedZone("UTC+13", 13*3600))
if !result.Equal(expected) {
t.Errorf("Timestamp incorrect: got %v, want %v", result, expected)
}
})
t.Run("should parse negative timezone offset", func(t *testing.T) {
result, err := parseAudioMothTimestamp("10:30:45", "15/06/2024", "UTC-5")
if err != nil {
t.Fatalf("Failed to parse timestamp: %v", err)
}
expected := time.Date(2024, 6, 15, 10, 30, 45, 0, time.FixedZone("UTC-5", -5*3600))
if !result.Equal(expected) {
t.Errorf("Timestamp incorrect: got %v, want %v", result, expected)
}
})
t.Run("should handle invalid time format", func(t *testing.T) {
_, err := parseAudioMothTimestamp("25:00:00", "15/06/2024", "UTC+13")
_ = err
})
t.Run("should handle invalid date format", func(t *testing.T) {
_, err := parseAudioMothTimestamp("10:30:45", "32/13/2024", "UTC+13")
_ = err
})
}
func TestStructuredVsLegacyParsing(t *testing.T) {
t.Run("should prefer structured parsing", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C."
result, err := ParseAudioMothComment(comment)
if err != nil {
t.Fatalf("Failed to parse comment: %v", err)
}
if result.RecorderID != "248AB50153AB0549" {
t.Errorf("RecorderID incorrect: got %s, want 248AB50153AB0549", result.RecorderID)
}
})
t.Run("should handle legacy format", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C."
result, err := ParseAudioMothComment(comment)
if err != nil {
t.Logf("Note: Structured parsing failed, expected legacy to handle: %v", err)
} else {
if result.RecorderID == "" {
t.Error("RecorderID should not be empty")
}
}
})
}
func TestAudioMothCommentEdgeCases(t *testing.T) {
t.Run("should handle extra whitespace", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C."
_, err := ParseAudioMothComment(comment)
if err != nil {
t.Logf("Extra whitespace causes parsing to fail (expected): %v", err)
}
})
t.Run("should handle different case in gain", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at MEDIUM gain while battery was 4.3V and temperature was 15.8C."
result, err := ParseAudioMothComment(comment)
if err == nil {
if result.Gain != db.GainMedium {
t.Errorf("Gain should be normalized: got %s, want %s", result.Gain, db.GainMedium)
}
}
})
t.Run("should handle non-hex recorder ID via legacy parser", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth GGGGGGGGGGGGGGGG at medium gain while battery was 4.3V and temperature was 15.8C."
result, err := ParseAudioMothComment(comment)
if err != nil {
t.Fatalf("Legacy parser should handle non-hex recorder ID: %v", err)
}
if result.RecorderID != "GGGGGGGGGGGGGGGG" {
t.Errorf("RecorderID incorrect: got %s, want GGGGGGGGGGGGGGGG", result.RecorderID)
}
})
t.Run("should handle recorder ID of different lengths", func(t *testing.T) {
comment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth ABCD at medium gain while battery was 4.3V and temperature was 15.8C."
result, err := ParseAudioMothComment(comment)
if err != nil {
t.Fatalf("Failed to parse comment with short ID: %v", err)
}
if !strings.Contains(result.RecorderID, "ABCD") {
t.Errorf("RecorderID should contain ABCD, got %s", result.RecorderID)
}
})
}