package utils
import (
"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", testParseStructuredComment)
t.Run("should return error for invalid comments", testParseInvalidComments)
t.Run("should handle different timezone formats", testParseTimezoneFormats)
t.Run("should parse all gain levels", testParseAllGainLevels)
t.Run("should handle negative temperatures", testParseNegativeTemp)
t.Run("should fallback to legacy parsing", testParseLegacyFallback)
}
func testParseStructuredComment(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 != GainMedium {
t.Errorf("Gain incorrect: got %s, want %s", result.Gain, 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)
}
}
func testParseInvalidComments(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)
}
}
}
func testParseTimezoneFormats(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 != GainHigh {
t.Errorf("Gain incorrect: got %s, want %s", result.Gain, 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)
}
}
func testParseAllGainLevels(t *testing.T) {
testCases := []struct {
gainStr string
expected GainLevel
}{
{"low", GainLow},
{"low-medium", GainLowMedium},
{"medium", GainMedium},
{"medium-high", GainMediumHigh},
{"high", 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)
}
}
}
func testParseNegativeTemp(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)
}
}
func testParseLegacyFallback(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 GainLevel
wantErr bool
}{
{"low", GainLow, false},
{"LOW", GainLow, false},
{" low ", GainLow, false},
{"low-medium", GainLowMedium, false},
{"medium", GainMedium, false},
{"medium-high", GainMediumHigh, false},
{"high", 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 != GainMedium {
t.Errorf("Gain should be normalized: got %s, want %s", result.Gain, 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)
}
})
}