package main import ( "github.com/tkrajina/gpxgo/gpx" "math" "testing" ) func TestParseMultiStops(t *testing.T) { tests := []struct { name string input string expected []MultiStop }{ { name: "Empty input", input: "", expected: []MultiStop{}, }, { name: "Single stop", input: "59.3293,18.0686:1", expected: []MultiStop{ {Lat: 59.3293, Lon: 18.0686, Nights: 1}, }, }, { name: "Multiple stops", input: "59.3293,18.0686:1;60.1282,18.6435:2", expected: []MultiStop{ {Lat: 59.3293, Lon: 18.0686, Nights: 1}, {Lat: 60.1282, Lon: 18.6435, Nights: 2}, }, }, { name: "Invalid format", input: "invalid", expected: []MultiStop{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := parseMultiStops(tt.input) if len(result) != len(tt.expected) { t.Errorf("Expected %d stops, got %d", len(tt.expected), len(result)) return } for i, stop := range result { if stop.Lat != tt.expected[i].Lat || stop.Lon != tt.expected[i].Lon || stop.Nights != tt.expected[i].Nights { t.Errorf("Stop %d mismatch: expected %+v, got %+v", i, tt.expected[i], stop) } } }) } } func TestHaversine(t *testing.T) { tests := []struct { name string lat1 float64 lon1 float64 lat2 float64 lon2 float64 expected float64 }{ { name: "Same point", lat1: 59.3293, lon1: 18.0686, lat2: 59.3293, lon2: 18.0686, expected: 0, }, { name: "Stockholm to Uppsala", lat1: 59.3293, lon1: 18.0686, lat2: 59.8586, lon2: 17.6389, expected: 63.63, // Approximate distance in km }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := haversine(tt.lat1, tt.lon1, tt.lat2, tt.lon2) // Allow for small floating point differences if math.Abs(result-tt.expected) > 0.5 { t.Errorf("Expected distance of %.2f km, got %.2f km", tt.expected, result) } }) } } func TestNormalizeCutIndexes(t *testing.T) { tests := []struct { name string cutIndexes []int days int totalPoints int expected []int }{ { name: "No cuts", cutIndexes: []int{}, days: 5, totalPoints: 100, expected: []int{}, }, { name: "Fewer cuts than days", cutIndexes: []int{25, 50, 75}, days: 5, totalPoints: 100, expected: []int{25, 50, 75}, }, { name: "More cuts than days", cutIndexes: []int{20, 40, 60, 80, 99}, days: 3, totalPoints: 100, expected: []int{20, 40, 99}, }, { name: "Duplicate cuts (rest days)", cutIndexes: []int{25, 25, 50, 75}, days: 5, totalPoints: 100, expected: []int{25, 25, 50, 75}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := normalizeCutIndexes(tt.cutIndexes, tt.days, tt.totalPoints) if len(result) != len(tt.expected) { t.Errorf("Expected %d cuts, got %d", len(tt.expected), len(result)) return } for i, cut := range result { if cut != tt.expected[i] { t.Errorf("Cut %d mismatch: expected %d, got %d", i, tt.expected[i], cut) } } }) } } func TestCreateSegment(t *testing.T) { // Create test points points := []TrackPoint{ {Index: 0, Distance: 0, Elevation: 0}, {Index: 1, Distance: 10, Elevation: 100}, {Index: 2, Distance: 20, Elevation: 200}, {Index: 3, Distance: 30, Elevation: 300}, {Index: 4, Distance: 40, Elevation: 400}, {Index: 5, Distance: 50, Elevation: 500}, } tests := []struct { name string start int end int cutIndexes []int segmentIndex int expectedLen int }{ { name: "Normal segment", start: 0, end: 3, cutIndexes: []int{3, 5}, segmentIndex: 0, expectedLen: 3, // Points 0, 1, 2 }, { name: "Forced stop (end <= start)", start: 3, end: 3, cutIndexes: []int{3, 5}, segmentIndex: 0, expectedLen: 2, // Points 3, 4 (halfway to the next cut) }, { name: "Last segment", start: 3, end: 5, cutIndexes: []int{3, 5}, segmentIndex: 1, expectedLen: 2, // Points 3, 4 }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := createSegment(points, tt.start, tt.end, tt.cutIndexes, tt.segmentIndex) if len(result) != tt.expectedLen { t.Errorf("Expected segment length %d, got %d", tt.expectedLen, len(result)) } }) } } func TestCreateDaySegment(t *testing.T) { // Create test points points := []TrackPoint{ {Index: 0, Distance: 0, Elevation: 0}, {Index: 1, Distance: 10, Elevation: 100}, {Index: 2, Distance: 20, Elevation: 200}, } dayNumber := 1 elevFactor := 4.0 forestRadius := 3 resupplyRadius := 500 // This test will not query for forest or resupply points // since that would require network access. We're just testing the // calculation of distance, elevation, and effort. segment := createDaySegment(points, dayNumber, elevFactor, forestRadius, resupplyRadius, false) if segment.DayNumber != dayNumber { t.Errorf("Expected day number %d, got %d", dayNumber, segment.DayNumber) } expectedDist := 20.0 // Last point distance - first point distance if math.Abs(segment.Distance-expectedDist) > 0.1 { t.Errorf("Expected distance %.1f, got %.1f", expectedDist, segment.Distance) } expectedElev := 200.0 // Last point elevation - first point elevation if math.Abs(segment.Elevation-expectedElev) > 0.1 { t.Errorf("Expected elevation %.1f, got %.1f", expectedElev, segment.Elevation) } expectedEffort := expectedDist + (expectedElev / 100.0 * elevFactor) if math.Abs(segment.Effort-expectedEffort) > 0.1 { t.Errorf("Expected effort %.1f, got %.1f", expectedEffort, segment.Effort) } } func TestCalculateCutIndexes(t *testing.T) { // Create test route data points := []TrackPoint{ {Index: 0, Distance: 0, Elevation: 0}, {Index: 1, Distance: 25, Elevation: 250}, // Effort: 25 + (250/100*4) = 35 {Index: 2, Distance: 50, Elevation: 500}, // Effort: 50 + (500/100*4) = 70 {Index: 3, Distance: 75, Elevation: 750}, // Effort: 75 + (750/100*4) = 105 {Index: 4, Distance: 100, Elevation: 1000}, // Effort: 100 + (1000/100*4) = 140 } routeData := RouteData{ Points: points, TotalDist: 100, TotalElev: 1000, TotalEffort: 140, } tests := []struct { name string days int elevFactor float64 multiStops []MultiStop expected []int }{ { name: "2 days, no stops", days: 2, elevFactor: 4.0, multiStops: []MultiStop{}, expected: []int{2, 4}, // Cut at index 2 (effort 70) and 4 (end) }, { name: "3 days, no stops", days: 3, elevFactor: 4.0, multiStops: []MultiStop{}, expected: []int{1, 3, 4}, // Cut at index 1, 3, and 4 (end) }, { name: "3 days, with one multi-stop", days: 3, elevFactor: 4.0, multiStops: []MultiStop{ {Lat: 0, Lon: 0, Nights: 1}, // This will match point 0 }, expected: []int{0, 1, 4}, // Cut at index 0 (forced stop), 1, and 4 (end) }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := calculateCutIndexes(routeData, tt.days, tt.elevFactor, tt.multiStops) if len(result) != len(tt.expected) { t.Errorf("Expected %d cuts, got %d", len(tt.expected), len(result)) return } for i, cut := range result { if cut != tt.expected[i] { t.Errorf("Cut %d mismatch: expected %d, got %d", i, tt.expected[i], cut) } } }) } } // New tests for refactored functions func TestFindForcedStopIndexes(t *testing.T) { // Create test points with latitude and longitude points := []TrackPoint{ {Index: 0, Point: gpx.GPXPoint{Point: gpx.Point{Latitude: 59.3293, Longitude: 18.0686}}}, {Index: 1, Point: gpx.GPXPoint{Point: gpx.Point{Latitude: 59.8586, Longitude: 17.6389}}}, {Index: 2, Point: gpx.GPXPoint{Point: gpx.Point{Latitude: 60.1282, Longitude: 18.6435}}}, } tests := []struct { name string points []TrackPoint multiStops []MultiStop expected []int }{ { name: "No stops", points: points, multiStops: []MultiStop{}, expected: []int{}, }, { name: "One stop matching first point", points: points, multiStops: []MultiStop{ {Lat: 59.3293, Lon: 18.0686, Nights: 1}, // Matches point 0 }, expected: []int{0}, }, { name: "One stop with multiple nights", points: points, multiStops: []MultiStop{ {Lat: 59.8586, Lon: 17.6389, Nights: 2}, // Matches point 1 }, expected: []int{1, 1}, // Duplicated for 2 nights }, { name: "Multiple stops", points: points, multiStops: []MultiStop{ {Lat: 59.3293, Lon: 18.0686, Nights: 1}, // Matches point 0 {Lat: 60.1282, Lon: 18.6435, Nights: 1}, // Matches point 2 }, expected: []int{0, 2}, }, { name: "Stop too far from any point", points: points, multiStops: []MultiStop{ {Lat: 55.0, Lon: 15.0, Nights: 1}, // Far from all points }, expected: []int{}, // No match within 2km }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := findForcedStopIndexes(tt.points, tt.multiStops) if len(result) != len(tt.expected) { t.Errorf("Expected %d forced stops, got %d", len(tt.expected), len(result)) return } for i, idx := range result { if idx != tt.expected[i] { t.Errorf("Forced stop %d mismatch: expected %d, got %d", i, tt.expected[i], idx) } } }) } } func TestHandleTooManyForcedStops(t *testing.T) { tests := []struct { name string forcedStopIndexes []int days int lastPointIndex int totalPoints int expected []int }{ { name: "Fewer forced stops than days", forcedStopIndexes: []int{25, 50}, days: 4, lastPointIndex: 99, totalPoints: 100, expected: []int{25, 50, 99}, }, { name: "More forced stops than days", forcedStopIndexes: []int{20, 40, 60, 80}, days: 3, lastPointIndex: 99, totalPoints: 100, expected: []int{20, 40, 99}, // Only first (days-1) forced stops are kept }, { name: "Equal forced stops and days", forcedStopIndexes: []int{33, 66}, days: 3, lastPointIndex: 99, totalPoints: 100, expected: []int{33, 66, 99}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := handleTooManyForcedStops(tt.forcedStopIndexes, tt.days, tt.lastPointIndex, tt.totalPoints) if len(result) != len(tt.expected) { t.Errorf("Expected %d cuts, got %d", len(tt.expected), len(result)) return } for i, cut := range result { if cut != tt.expected[i] { t.Errorf("Cut %d mismatch: expected %d, got %d", i, tt.expected[i], cut) } } }) } } func TestCreateSegmentsBetweenStops(t *testing.T) { // Create test points points := []TrackPoint{ {Index: 0, Distance: 0, Elevation: 0}, {Index: 1, Distance: 10, Elevation: 100}, {Index: 2, Distance: 20, Elevation: 200}, {Index: 3, Distance: 30, Elevation: 300}, {Index: 4, Distance: 40, Elevation: 400}, } tests := []struct { name string points []TrackPoint allStops []int elevFactor float64 expected []struct { start, end int effort float64 } }{ { name: "No stops", points: points, allStops: []int{}, elevFactor: 4.0, expected: []struct { start, end int effort float64 }{}, }, { name: "One segment", points: points, allStops: []int{0, 4}, elevFactor: 4.0, expected: []struct { start, end int effort float64 }{ {0, 4, 56.0}, // Effort: 40 + (400/100*4) = 56 }, }, { name: "Multiple segments", points: points, allStops: []int{0, 2, 4}, elevFactor: 4.0, expected: []struct { start, end int effort float64 }{ {0, 2, 28.0}, // Effort: 20 + (200/100*4) = 28 {2, 4, 42.0}, // Effort: (30-20) + ((300-200)/100*4) + (40-30) + ((400-300)/100*4) = 42 }, }, { name: "Duplicate stops (rest days)", points: points, allStops: []int{0, 2, 2, 4}, elevFactor: 4.0, expected: []struct { start, end int effort float64 }{ {0, 2, 28.0}, // Effort: 20 + (200/100*4) = 28 {2, 4, 42.0}, // Effort: (30-20) + ((300-200)/100*4) + (40-30) + ((400-300)/100*4) = 42 }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := createSegmentsBetweenStops(tt.points, tt.allStops, tt.elevFactor) if len(result) != len(tt.expected) { t.Errorf("Expected %d segments, got %d", len(tt.expected), len(result)) return } for i, seg := range result { if seg.start != tt.expected[i].start || seg.end != tt.expected[i].end { t.Errorf("Segment %d bounds mismatch: expected (%d,%d), got (%d,%d)", i, tt.expected[i].start, tt.expected[i].end, seg.start, seg.end) } if math.Abs(seg.effort-tt.expected[i].effort) > 0.1 { t.Errorf("Segment %d effort mismatch: expected %.1f, got %.1f", i, tt.expected[i].effort, seg.effort) } } }) } } func TestHandleTooManyCuts(t *testing.T) { tests := []struct { name string cutIndexes []int forcedStopIndexes []int days int lastPointIndex int expectedLen int }{ { name: "Fewer cuts than days", cutIndexes: []int{25, 50}, forcedStopIndexes: []int{}, days: 4, lastPointIndex: 99, expectedLen: 4, // Will add cuts to reach days }, { name: "More cuts than days", cutIndexes: []int{20, 40, 60, 80}, forcedStopIndexes: []int{}, days: 3, lastPointIndex: 99, expectedLen: 3, // Will reduce to days }, { name: "With forced stops", cutIndexes: []int{20, 40, 60, 80}, forcedStopIndexes: []int{20, 60}, days: 3, lastPointIndex: 99, expectedLen: 3, // Will keep forced stops and last point }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := handleTooManyCuts(tt.cutIndexes, tt.forcedStopIndexes, tt.days, tt.lastPointIndex) if len(result) != tt.expectedLen { t.Errorf("Expected %d cuts, got %d", tt.expectedLen, len(result)) } // Check that forced stops are included for _, forcedIdx := range tt.forcedStopIndexes { found := false for _, resultIdx := range result { if resultIdx == forcedIdx { found = true break } } if !found { t.Errorf("Forced stop %d not found in result", forcedIdx) } } // Check that last point is included lastPointFound := false for _, resultIdx := range result { if resultIdx == tt.lastPointIndex { lastPointFound = true break } } if !lastPointFound { t.Errorf("Last point %d not found in result", tt.lastPointIndex) } }) } }