type_test.go

  1package waypoint
  2
  3import (
  4	"os"
  5	"strings"
  6	"testing"
  7)
  8
  9func hasGraphHopperKey() bool {
 10	return os.Getenv("GRAPHHOPPER_API_KEY") != ""
 11}
 12
 13func TestToRoutingRequest_EmptyWaypoints(t *testing.T) {
 14	reqs, err := ToRoutingRequest()
 15	if err != nil {
 16		t.Fatalf("unexpected error: %v", err)
 17	}
 18	if len(reqs) != 0 {
 19		t.Errorf("expected empty map, got %d entries", len(reqs))
 20	}
 21}
 22
 23func TestToRoutingRequest_SingleWaypoint(t *testing.T) {
 24	wp := Waypoint{
 25		ID:    "wp1",
 26		Point: []float64{10.0, 20.0},
 27		From:  map[string]Route{},
 28	}
 29	reqs, err := ToRoutingRequest(wp)
 30	if err != nil {
 31		t.Fatalf("unexpected error: %v", err)
 32	}
 33	if len(reqs) != 0 {
 34		t.Errorf("expected empty map for single waypoint, got %d entries", len(reqs))
 35	}
 36}
 37
 38func TestToRoutingRequest_TwoWaypoints_NoExistingRoute(t *testing.T) {
 39	if !hasGraphHopperKey() {
 40		t.Skip("GRAPHHOPPER_API_KEY not set")
 41	}
 42
 43	wp1 := Waypoint{
 44		ID:    "wp1",
 45		Point: []float64{10.0, 20.0},
 46		From:  map[string]Route{},
 47	}
 48	wp2 := Waypoint{
 49		ID:    "wp2",
 50		Point: []float64{30.0, 40.0},
 51		From: map[string]Route{
 52			"wp1": {}, // No ID means route not calculated yet
 53		},
 54	}
 55
 56	reqs, err := ToRoutingRequest(wp1, wp2)
 57	if err != nil {
 58		t.Fatalf("unexpected error: %v", err)
 59	}
 60
 61	if len(reqs) != 1 {
 62		t.Fatalf("expected 1 request, got %d", len(reqs))
 63	}
 64	if reqs[0].WaypointID != "wp2" {
 65		t.Errorf("expected request for wp2, got %s", reqs[0].WaypointID)
 66	}
 67	if reqs[0].RouteKey != "wp1" {
 68		t.Errorf("expected route key 'wp1', got %s", reqs[0].RouteKey)
 69	}
 70}
 71
 72func TestToRoutingRequest_TwoWaypoints_ExistingRoute(t *testing.T) {
 73	wp1 := Waypoint{
 74		ID:    "wp1",
 75		Point: []float64{10.0, 20.0},
 76		From:  map[string]Route{},
 77	}
 78	wp2 := Waypoint{
 79		ID:    "wp2",
 80		Point: []float64{30.0, 40.0},
 81		From: map[string]Route{
 82			"wp1": {ID: "existing-hash", Distance: 100.0, Time: 60.0},
 83		},
 84	}
 85
 86	reqs, err := ToRoutingRequest(wp1, wp2)
 87	if err != nil {
 88		t.Fatalf("unexpected error: %v", err)
 89	}
 90
 91	if len(reqs) != 0 {
 92		t.Errorf("expected no requests when route already exists, got %d entries", len(reqs))
 93	}
 94}
 95
 96func TestToRoutingRequest_WithAdventureConstraint(t *testing.T) {
 97	if !hasGraphHopperKey() {
 98		t.Skip("GRAPHHOPPER_API_KEY not set")
 99	}
100
101	wp1 := Waypoint{
102		ID:    "wp1",
103		Point: []float64{10.0, 20.0},
104		From:  map[string]Route{},
105	}
106	wp2 := Waypoint{
107		ID:    "wp2",
108		Point: []float64{30.0, 40.0},
109		From: map[string]Route{
110			"wp1:adv(80)": {}, // High adventure value
111		},
112	}
113
114	reqs, err := ToRoutingRequest(wp1, wp2)
115	if err != nil {
116		t.Fatalf("unexpected error: %v", err)
117	}
118
119	if len(reqs) != 1 {
120		t.Fatalf("expected 1 request, got %d", len(reqs))
121	}
122	if reqs[0].WaypointID != "wp2" {
123		t.Errorf("expected request for wp2, got %s", reqs[0].WaypointID)
124	}
125	if reqs[0].RouteKey != "wp1:adv(80)" {
126		t.Errorf("expected route key 'wp1:adv(80)', got %s", reqs[0].RouteKey)
127	}
128}
129
130func TestToRoutingRequest_WithMaxSpeedConstraint(t *testing.T) {
131	if !hasGraphHopperKey() {
132		t.Skip("GRAPHHOPPER_API_KEY not set")
133	}
134
135	wp1 := Waypoint{
136		ID:    "wp1",
137		Point: []float64{10.0, 20.0},
138		From:  map[string]Route{},
139	}
140	wp2 := Waypoint{
141		ID:    "wp2",
142		Point: []float64{30.0, 40.0},
143		From: map[string]Route{
144			"wp1:max(80)": {}, // Max speed 80 kph
145		},
146	}
147
148	reqs, err := ToRoutingRequest(wp1, wp2)
149	if err != nil {
150		t.Fatalf("unexpected error: %v", err)
151	}
152
153	if len(reqs) != 1 {
154		t.Fatalf("expected 1 destination with requests, got %d", len(reqs))
155	}
156}
157
158func TestToRoutingRequest_WithMultipleConstraints(t *testing.T) {
159	if !hasGraphHopperKey() {
160		t.Skip("GRAPHHOPPER_API_KEY not set")
161	}
162
163	wp1 := Waypoint{
164		ID:    "wp1",
165		Point: []float64{10.0, 20.0},
166		From:  map[string]Route{},
167	}
168	wp2 := Waypoint{
169		ID:    "wp2",
170		Point: []float64{30.0, 40.0},
171		From: map[string]Route{
172			"wp1:adv(60):max(100)": {}, // Adventure 60, max speed 100
173		},
174	}
175
176	reqs, err := ToRoutingRequest(wp1, wp2)
177	if err != nil {
178		t.Fatalf("unexpected error: %v", err)
179	}
180
181	if len(reqs) != 1 {
182		t.Fatalf("expected 1 destination with requests, got %d", len(reqs))
183	}
184}
185
186func TestToRoutingRequest_MultipleRoutesToSameDestination(t *testing.T) {
187	if !hasGraphHopperKey() {
188		t.Skip("GRAPHHOPPER_API_KEY not set")
189	}
190
191	wp1 := Waypoint{
192		ID:    "wp1",
193		Point: []float64{10.0, 20.0},
194		From:  map[string]Route{},
195	}
196	wp2 := Waypoint{
197		ID:    "wp2",
198		Point: []float64{30.0, 40.0},
199		From:  map[string]Route{},
200	}
201	wp3 := Waypoint{
202		ID:    "wp3",
203		Point: []float64{50.0, 60.0},
204		From: map[string]Route{
205			"wp1":         {},
206			"wp2:adv(80)": {},
207			"wp2:adv(20)": {}, // Different constraint, same source
208		},
209	}
210
211	reqs, err := ToRoutingRequest(wp1, wp2, wp3)
212	if err != nil {
213		t.Fatalf("unexpected error: %v", err)
214	}
215
216	if len(reqs) != 3 {
217		t.Fatalf("expected 3 requests, got %d", len(reqs))
218	}
219	// Count requests for wp3
220	wp3Count := 0
221	for _, r := range reqs {
222		if r.WaypointID == "wp3" {
223			wp3Count++
224		}
225	}
226	if wp3Count != 3 {
227		t.Errorf("expected 3 requests for wp3, got %d", wp3Count)
228	}
229}
230
231func TestToRoutingRequest_MissingSourceWaypoint(t *testing.T) {
232	wp1 := Waypoint{
233		ID:    "wp1",
234		Point: []float64{10.0, 20.0},
235		From:  map[string]Route{},
236	}
237	wp2 := Waypoint{
238		ID:    "wp2",
239		Point: []float64{30.0, 40.0},
240		From: map[string]Route{
241			"nonexistent": {}, // Source doesn't exist
242		},
243	}
244
245	_, err := ToRoutingRequest(wp1, wp2)
246	if err == nil {
247		t.Fatal("expected error for non-existent source waypoint")
248	}
249
250	if !strings.Contains(err.Error(), "wp2") || !strings.Contains(err.Error(), "non-existent source") {
251		t.Errorf("expected error to mention waypoint ID and non-existent source, got: %v", err)
252	}
253}
254
255func TestToRoutingRequest_InvalidRouteKey(t *testing.T) {
256	wp1 := Waypoint{
257		ID:    "wp1",
258		Point: []float64{10.0, 20.0},
259		From:  map[string]Route{},
260	}
261	wp2 := Waypoint{
262		ID:    "wp2",
263		Point: []float64{30.0, 40.0},
264		From: map[string]Route{
265			"wp1:invalid": {}, // Invalid constraint format
266		},
267	}
268
269	_, err := ToRoutingRequest(wp1, wp2)
270	if err == nil {
271		t.Fatal("expected error for invalid route key")
272	}
273
274	if !strings.Contains(err.Error(), "wp2") {
275		t.Errorf("expected error to mention waypoint ID, got: %v", err)
276	}
277}
278
279func TestToRoutingRequest_InvalidAdventureValue(t *testing.T) {
280	wp1 := Waypoint{
281		ID:    "wp1",
282		Point: []float64{10.0, 20.0},
283		From:  map[string]Route{},
284	}
285	wp2 := Waypoint{
286		ID:    "wp2",
287		Point: []float64{30.0, 40.0},
288		From: map[string]Route{
289			"wp1:adv(notanumber)": {},
290		},
291	}
292
293	_, err := ToRoutingRequest(wp1, wp2)
294	if err == nil {
295		t.Fatal("expected error for invalid adventure value")
296	}
297
298	if !strings.Contains(err.Error(), "wp2") || !strings.Contains(err.Error(), "invalid adventure value") {
299		t.Errorf("expected error to mention waypoint ID and adventure value, got: %v", err)
300	}
301}
302
303func TestToRoutingRequest_InvalidMaxSpeedValue(t *testing.T) {
304	if !hasGraphHopperKey() {
305		t.Skip("GRAPHHOPPER_API_KEY not set")
306	}
307
308	wp1 := Waypoint{
309		ID:    "wp1",
310		Point: []float64{10.0, 20.0},
311		From:  map[string]Route{},
312	}
313	wp2 := Waypoint{
314		ID:    "wp2",
315		Point: []float64{30.0, 40.0},
316		From: map[string]Route{
317			"wp1:max(notanumber)": {},
318		},
319	}
320
321	_, err := ToRoutingRequest(wp1, wp2)
322	if err == nil {
323		t.Fatal("expected error for invalid maxspeed value")
324	}
325
326	if !strings.Contains(err.Error(), "wp2") || !strings.Contains(err.Error(), "invalid maxspeed value") {
327		t.Errorf("expected error to mention waypoint ID and maxspeed value, got: %v", err)
328	}
329}
330
331func TestToRoutingRequest_InvalidPointCoordinates(t *testing.T) {
332	wp1 := Waypoint{
333		ID:    "wp1",
334		Point: []float64{10.0}, // Only one coordinate - invalid
335		From:  map[string]Route{},
336	}
337	wp2 := Waypoint{
338		ID:    "wp2",
339		Point: []float64{30.0, 40.0},
340		From: map[string]Route{
341			"wp1": {},
342		},
343	}
344
345	_, err := ToRoutingRequest(wp1, wp2)
346	if err == nil {
347		t.Fatal("expected error for invalid point coordinates")
348	}
349
350	if !strings.Contains(err.Error(), "wp1") || !strings.Contains(err.Error(), "invalid point") {
351		t.Errorf("expected error to mention waypoint ID and invalid point, got: %v", err)
352	}
353}
354
355func TestToRoutingRequest_EmptyPointCoordinates(t *testing.T) {
356	wp1 := Waypoint{
357		ID:    "wp1",
358		Point: []float64{}, // Empty - invalid
359		From:  map[string]Route{},
360	}
361	wp2 := Waypoint{
362		ID:    "wp2",
363		Point: []float64{30.0, 40.0},
364		From: map[string]Route{
365			"wp1": {},
366		},
367	}
368
369	_, err := ToRoutingRequest(wp1, wp2)
370	if err == nil {
371		t.Fatal("expected error for empty point coordinates")
372	}
373}
374
375func TestToRoutingRequest_NonRoutingConstraintsIgnored(t *testing.T) {
376	if !hasGraphHopperKey() {
377		t.Skip("GRAPHHOPPER_API_KEY not set")
378	}
379
380	wp1 := Waypoint{
381		ID:    "wp1",
382		Point: []float64{10.0, 20.0},
383		From:  map[string]Route{},
384	}
385	wp2 := Waypoint{
386		ID:    "wp2",
387		Point: []float64{30.0, 40.0},
388		Cons:  []string{"dkm(200)", "bbt(120)"}, // Non-routing constraints
389		From: map[string]Route{
390			"wp1:dkm(300):bbt(180)": {}, // These should be ignored for routing
391		},
392	}
393
394	reqs, err := ToRoutingRequest(wp1, wp2)
395	if err != nil {
396		t.Fatalf("unexpected error: %v", err)
397	}
398
399	if len(reqs) != 1 {
400		t.Fatalf("expected 1 request, got %d", len(reqs))
401	}
402	// Request should be created successfully even with non-routing constraints
403	if reqs[0].WaypointID != "wp2" {
404		t.Errorf("expected request for wp2, got %s", reqs[0].WaypointID)
405	}
406}
407
408// Test ToRouteKey
409func TestToRouteKey_NoConstraints(t *testing.T) {
410	from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
411	to := Waypoint{ID: "wp2", Point: []float64{30.0, 40.0}}
412
413	key, err := ToRouteKey(from, to, false)
414	if err != nil {
415		t.Fatalf("unexpected error: %v", err)
416	}
417
418	if key != "wp1" {
419		t.Errorf("expected key 'wp1', got %s", key)
420	}
421}
422
423func TestToRouteKey_WithAdventure(t *testing.T) {
424	from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
425	to := Waypoint{
426		ID:    "wp2",
427		Point: []float64{30.0, 40.0},
428		Cons:  []string{"adv(80)"},
429	}
430
431	key, err := ToRouteKey(from, to, false)
432	if err != nil {
433		t.Fatalf("unexpected error: %v", err)
434	}
435
436	expected := "wp1:adv(80)"
437	if key != expected {
438		t.Errorf("expected key '%s', got %s", expected, key)
439	}
440}
441
442func TestToRouteKey_WithMaxSpeed(t *testing.T) {
443	from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
444	to := Waypoint{
445		ID:    "wp2",
446		Point: []float64{30.0, 40.0},
447		Cons:  []string{"max(100)"},
448	}
449
450	key, err := ToRouteKey(from, to, false)
451	if err != nil {
452		t.Fatalf("unexpected error: %v", err)
453	}
454
455	expected := "wp1:max(100)"
456	if key != expected {
457		t.Errorf("expected key '%s', got %s", expected, key)
458	}
459}
460
461func TestToRouteKey_WithWaypointLocationType(t *testing.T) {
462	from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
463	to := Waypoint{
464		ID:    "wp2",
465		Point: []float64{30.0, 40.0},
466		Cons:  []string{"wlt(v)"},
467	}
468
469	key, err := ToRouteKey(from, to, false)
470	if err != nil {
471		t.Fatalf("unexpected error: %v", err)
472	}
473
474	expected := "wp1:wlt(v)"
475	if key != expected {
476		t.Errorf("expected key '%s', got %s", expected, key)
477	}
478}
479
480func TestToRouteKey_MultipleConstraints(t *testing.T) {
481	from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
482	to := Waypoint{
483		ID:    "wp2",
484		Point: []float64{30.0, 40.0},
485		Cons:  []string{"adv(80)", "max(100)", "wlt(t)"},
486	}
487
488	key, err := ToRouteKey(from, to, false)
489	if err != nil {
490		t.Fatalf("unexpected error: %v", err)
491	}
492
493	// Should contain all three constraints
494	if !strings.Contains(key, "wp1") {
495		t.Error("key should start with source waypoint ID")
496	}
497	if !strings.Contains(key, "adv(80)") {
498		t.Error("key should contain adventure constraint")
499	}
500	if !strings.Contains(key, "max(100)") {
501		t.Error("key should contain maxspeed constraint")
502	}
503	if !strings.Contains(key, "wlt(t)") {
504		t.Error("key should contain waypoint location type constraint")
505	}
506}
507
508func TestToRouteKey_NonRoutingConstraintsIgnored(t *testing.T) {
509	from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
510	to := Waypoint{
511		ID:    "wp2",
512		Point: []float64{30.0, 40.0},
513		Cons:  []string{"dkm(200)", "bbt(120)", "dat(0800)", "abt(1800)"},
514	}
515
516	key, err := ToRouteKey(from, to, false)
517	if err != nil {
518		t.Fatalf("unexpected error: %v", err)
519	}
520
521	// Should only contain source ID, non-routing constraints should be ignored
522	if key != "wp1" {
523		t.Errorf("expected key 'wp1' (non-routing constraints ignored), got %s", key)
524	}
525}
526
527func TestToRouteKey_EmptyFromID(t *testing.T) {
528	from := Waypoint{ID: "", Point: []float64{10.0, 20.0}}
529	to := Waypoint{ID: "wp2", Point: []float64{30.0, 40.0}}
530
531	_, err := ToRouteKey(from, to, false)
532	if err == nil {
533		t.Fatal("expected error for empty from ID")
534	}
535}
536
537func TestToRouteKey_InvalidConstraint(t *testing.T) {
538	from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
539	to := Waypoint{
540		ID:    "wp2",
541		Point: []float64{30.0, 40.0},
542		Cons:  []string{"invalid"},
543	}
544
545	_, err := ToRouteKey(from, to, false)
546	if err == nil {
547		t.Fatal("expected error for invalid constraint")
548	}
549}
550
551// Test FromRouteKey
552func TestFromRouteKey_NoConstraints(t *testing.T) {
553	fromID, cons, err := FromRouteKey("wp1")
554	if err != nil {
555		t.Fatalf("unexpected error: %v", err)
556	}
557
558	if fromID != "wp1" {
559		t.Errorf("expected fromID 'wp1', got %s", fromID)
560	}
561
562	if len(cons) != 0 {
563		t.Errorf("expected 0 constraints, got %d", len(cons))
564	}
565}
566
567func TestFromRouteKey_WithConstraints(t *testing.T) {
568	fromID, cons, err := FromRouteKey("wp1:adv(80):max(100)")
569	if err != nil {
570		t.Fatalf("unexpected error: %v", err)
571	}
572
573	if fromID != "wp1" {
574		t.Errorf("expected fromID 'wp1', got %s", fromID)
575	}
576
577	if len(cons) != 2 {
578		t.Fatalf("expected 2 constraints, got %d", len(cons))
579	}
580
581	// Check first constraint
582	if cons[0].Type != Adventure || cons[0].Value != "80" {
583		t.Errorf("expected adventure constraint with value 80, got %v", cons[0])
584	}
585
586	// Check second constraint
587	if cons[1].Type != MaxSpeed || cons[1].Value != "100" {
588		t.Errorf("expected maxspeed constraint with value 100, got %v", cons[1])
589	}
590}
591
592func TestFromRouteKey_InvalidConstraint(t *testing.T) {
593	_, _, err := FromRouteKey("wp1:invalid")
594	if err == nil {
595		t.Fatal("expected error for invalid constraint format")
596	}
597}
598
599// Test ConstraintFromString
600func TestConstraintFromString_ValidConstraints(t *testing.T) {
601	tests := []struct {
602		input    string
603		expected ConstraintType
604		value    string
605	}{
606		{"adv(80)", Adventure, "80"},
607		{"max(100)", MaxSpeed, "100"},
608		{"dkm(200)", DailyKMs, "200"},
609		{"bbd(150)", BreakBeforeDist, "150"},
610		{"bbt(120)", BreakBeforeTime, "120"},
611		{"dat(0800)", DepartAfterTime, "0800"},
612		{"abt(1800)", ArriveBefore, "1800"},
613		{"wlt(v)", WaypointLocationType, "v"},
614	}
615
616	for _, test := range tests {
617		c, err := ConstraintFromString(test.input)
618		if err != nil {
619			t.Errorf("unexpected error for %s: %v", test.input, err)
620			continue
621		}
622		if c.Type != test.expected {
623			t.Errorf("expected type %v for %s, got %v", test.expected, test.input, c.Type)
624		}
625		if c.Value != test.value {
626			t.Errorf("expected value %s for %s, got %s", test.value, test.input, c.Value)
627		}
628	}
629}
630
631func TestConstraintFromString_InvalidFormat(t *testing.T) {
632	invalidInputs := []string{
633		"adv80",
634		"adv(80",
635		"unknown(value)",
636		"",
637		"adv",
638	}
639
640	for _, input := range invalidInputs {
641		_, err := ConstraintFromString(input)
642		if err == nil {
643			t.Errorf("expected error for input: %s", input)
644		}
645	}
646}
647
648// Test ParseConstraints
649func TestParseConstraints(t *testing.T) {
650	inputs := []string{
651		"adv(80)",
652		"max(100)",
653		"invalid",
654		"dkm(200)",
655	}
656
657	cons := ParseConstraints(inputs)
658
659	// Should parse 3 valid constraints, skip the invalid one
660	if len(cons) != 3 {
661		t.Errorf("expected 3 constraints (skipping invalid), got %d", len(cons))
662	}
663
664	// Verify the types
665	expectedTypes := []ConstraintType{Adventure, MaxSpeed, DailyKMs}
666	for i, expected := range expectedTypes {
667		if cons[i].Type != expected {
668			t.Errorf("expected constraint %d to be %v, got %v", i, expected, cons[i].Type)
669		}
670	}
671}
672
673func TestParseConstraints_Empty(t *testing.T) {
674	cons := ParseConstraints([]string{})
675	if len(cons) != 0 {
676		t.Errorf("expected empty constraints, got %d", len(cons))
677	}
678}
679
680// Test wpToLocation
681func TestWpToLocation_Default(t *testing.T) {
682	wp := Waypoint{
683		ID:    "wp1",
684		Point: []float64{10.5, 20.5},
685	}
686
687	loc, err := wpToLocation(wp)
688	if err != nil {
689		t.Fatalf("unexpected error: %v", err)
690	}
691
692	// PointLngLat is [2]float64 where index 0 = lon, index 1 = lat
693	if loc[0] != 10.5 || loc[1] != 20.5 {
694		t.Errorf("expected coordinates (lon=10.5, lat=20.5), got (lon=%f, lat=%f)", loc[0], loc[1])
695	}
696}
697
698// Note: TestWpToLocation_WithLocationType removed because Graphhopper doesn't support
699// location types (break, through, via, etc.) like Valhalla did. The wlt() constraint
700// is still parsed for route key purposes but doesn't affect the location structure.
701
702func TestWpToLocation_InvalidPoint(t *testing.T) {
703	tests := []struct {
704		name  string
705		point []float64
706	}{
707		{"empty", []float64{}},
708		{"one coordinate", []float64{10.0}},
709	}
710
711	for _, test := range tests {
712		t.Run(test.name, func(t *testing.T) {
713			wp := Waypoint{
714				ID:    "wp1",
715				Point: test.point,
716			}
717
718			_, err := wpToLocation(wp)
719			if err == nil {
720				t.Fatal("expected error for invalid point coordinates")
721			}
722
723			if !strings.Contains(err.Error(), "wp1") || !strings.Contains(err.Error(), "invalid point") {
724				t.Errorf("expected error to mention waypoint ID and invalid point, got: %v", err)
725			}
726		})
727	}
728}