package routing import ( "testing" ) func TestGraphNodeCreation(t *testing.T) { // Test Node creation with Station type station := &Node{ ID: "s9600213", Type: NodeTypeStation, Name: "Шереметьево", } if station.ID != "s9600213" { t.Errorf("expected node ID s9600213, got %s", station.ID) } if station.Type != NodeTypeStation { t.Errorf("expected NodeTypeStation, got %v", station.Type) } if station.Name != "Шереметьево" { t.Errorf("expected name Шереметьево, got %s", station.Name) } // Test Node creation with City type city := &Node{ ID: "city:c146", Type: NodeTypeCity, Name: "Simferopol", } if city.ID != "city:c146" { t.Errorf("expected node ID city:c146, got %s", city.ID) } if city.Type != NodeTypeCity { t.Errorf("expected NodeTypeCity, got %v", city.Type) } if city.Name != "Simferopol" { t.Errorf("expected name Simferopol, got %s", city.Name) } } func TestGraphEdgeCreation(t *testing.T) { // Test Real edge realEdge := &Edge{ Kind: EdgeKindReal, Duration: 3600, TransportType: "train", IsTransfer: false, } if realEdge.Kind != EdgeKindReal { t.Errorf("expected EdgeKindReal, got %v", realEdge.Kind) } if realEdge.Duration != 3600 { t.Errorf("expected duration 3600, got %d", realEdge.Duration) } if realEdge.TransportType != "train" { t.Errorf("expected transport_type train, got %s", realEdge.TransportType) } if realEdge.IsTransfer { t.Errorf("expected IsTransfer false for real edge") } // Test Synthetic edge syntheticEdge := &Edge{ Kind: EdgeKindSynthetic, Duration: 1800, TransportType: "bus", IsTransfer: true, } if syntheticEdge.Kind != EdgeKindSynthetic { t.Errorf("expected EdgeKindSynthetic, got %v", syntheticEdge.Kind) } if syntheticEdge.IsTransfer != true { t.Errorf("expected IsTransfer true for synthetic edge") } } func TestGraphAddNodeAndEdge(t *testing.T) { graph := NewGraph() node := &Node{ID: "n1", Type: NodeTypeStation, Name: "Test Station"} graph.AddNode(node) if len(graph.Nodes()) != 1 { t.Errorf("expected 1 node, got %d", len(graph.Nodes())) } if graph.Nodes()[0].ID != "n1" { t.Errorf("expected node n1, got %s", graph.Nodes()[0].ID) } edge := &Edge{From: node, To: node, Kind: EdgeKindReal, Duration: 100} graph.AddEdge(edge) if len(graph.Edges()) != 1 { t.Errorf("expected 1 edge, got %d", len(graph.Edges())) } if graph.Edges()[0].Duration != 100 { t.Errorf("expected duration 100, got %d", graph.Edges()[0].Duration) } } func TestGraphSortEdges(t *testing.T) { edges := []*Edge{ {Duration: 300}, {Duration: 100}, {Duration: 200}, } SortEdges(edges) if edges[0].Duration != 100 { t.Errorf("expected first edge duration 100, got %d", edges[0].Duration) } if edges[1].Duration != 200 { t.Errorf("expected second edge duration 200, got %d", edges[1].Duration) } if edges[2].Duration != 300 { t.Errorf("expected third edge duration 300, got %d", edges[2].Duration) } } func TestBuildGraphFromStations(t *testing.T) { stations := []StationInfo{ {ID: "s9600213", Name: "Шереметьево", CityCode: "c146", CityName: "Simferopol"}, {ID: "s9600396", Name: "Симферополь", CityCode: "c146", CityName: "Simferopol"}, {ID: "s9600157", Name: "Москва", CityCode: "c213", CityName: "Москва"}, } graph := BuildGraphFromStations(stations) // Should have station nodes + city nodes // 3 stations + 2 cities (Simferopol + Moscow) = 5 nodes nodes := graph.Nodes() if len(nodes) != 5 { t.Errorf("expected 5 nodes (3 stations + 2 cities), got %d", len(nodes)) } // Should have edges edges := graph.Edges() if len(edges) < 3 { t.Errorf("expected at least 3 edges (synthetic city↔station), got %d", len(edges)) } // Verify city nodes exist cityIDs := make(map[string]bool) for _, n := range nodes { if n.Type == NodeTypeCity { cityIDs[n.ID] = true } } if !cityIDs["city:c146"] { t.Error("expected city:c146 node") } if !cityIDs["city:c213"] { t.Error("expected city:c213 node") } } func TestGraphNodesAndEdges(t *testing.T) { graph := NewGraph() // Add nodes graph.AddNode(&Node{ID: "n1", Type: NodeTypeStation, Name: "Station 1"}) graph.AddNode(&Node{ID: "n2", Type: NodeTypeStation, Name: "Station 2"}) graph.AddNode(&Node{ID: "city:c1", Type: NodeTypeCity, Name: "City 1"}) if len(graph.Nodes()) != 3 { t.Errorf("expected 3 nodes, got %d", len(graph.Nodes())) } // Add edges graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 100}) graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 200}) if len(graph.Edges()) != 2 { t.Errorf("expected 2 edges, got %d", len(graph.Edges())) } } func TestFindRouteSuccess(t *testing.T) { graph := NewGraph() // Add stations graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"}) graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Clinic", CityCode: "c1"}) // Add real edges (direct route) graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) // Add synthetic transfer edge graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 1800, Transport: "train", IsTransfer: true}) // Search for route with max 1 transfer opts := SearchOptions{MaxTransfers: 1, MCT: 300} result := graph.FindRoute("s1", "s3", opts) if result == nil { t.Error("expected a route to be found") } if result.TotalTransfers > 1 { t.Errorf("expected at most 1 transfer, got %d", result.TotalTransfers) } if result.TotalDuration <= 0 { t.Errorf("expected positive duration, got %d", result.TotalDuration) } } func TestFindRouteNoRoute(t *testing.T) { graph := NewGraph() // Add isolated nodes with no connections graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Station 1", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Station 2", CityCode: "c2"}) // Search with no edges - should return nil opts := SearchOptions{MaxTransfers: 1, MCT: 300} result := graph.FindRoute("s1", "s2", opts) if result != nil { t.Error("expected nil route when no edges exist, got result") } } func TestFindRouteExceedsTransferLimit(t *testing.T) { graph := NewGraph() // Add a chain of stations with synthetic transfer edges (would require 4 transfers) graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City Hub 1", CityCode: "c1"}) graph.AddNode(&Node{ID: "s3", Type: NodeTypeCity, Name: "City Hub 2", CityCode: "c1"}) graph.AddNode(&Node{ID: "s4", Type: NodeTypeCity, Name: "City Hub 3", CityCode: "c1"}) graph.AddNode(&Node{ID: "s5", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"}) // Add synthetic transfer edges between consecutive nodes (IsTransfer: true) graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true}) graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true}) graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true}) graph.AddEdge(&Edge{From: graph.Nodes()[3], To: graph.Nodes()[4], Kind: EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true}) // Search with max 1 transfer - should not find route requiring 4 transfers opts := SearchOptions{MaxTransfers: 1, MCT: 300} result := graph.FindRoute("s1", "s5", opts) if result != nil { t.Error("expected nil route when transfers exceed limit, got result") } } func TestApplyMCT_CityHubReducesMCT(t *testing.T) { graph := NewGraph() // Create legs with city hub transfers graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City Hub", CityCode: "c1"}) graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"}) // Add real edges graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) itinerary := &Itinerary{ Legs: []RouteLeg{ {From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false}, {From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false}, }, TotalDuration: 0, TotalTransfers: 0, } result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT // City hub transfer reduces MCT from 30 min (1800) to 15 min (900) // TotalDuration only includes the MCT addition (starts at 0), so result = 900 if result.TotalDuration != 900 { t.Errorf("expected total duration 900 (reduced MCT for city hub), got %d", result.TotalDuration) } } func TestApplyMCT_ModeChangeIncreasesMCT(t *testing.T) { graph := NewGraph() // Create legs with mode change graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"}) // Add first leg (train) graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) itinerary := &Itinerary{ Legs: []RouteLeg{ {From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false}, }, TotalDuration: 0, TotalTransfers: 0, } // With only 1 leg, ApplyMCT returns early - no transfers needed result := graph.ApplyMCT(itinerary, 1800) // Single leg means no transfer, TotalDuration stays at 0 if result.TotalDuration != 0 { t.Errorf("expected total duration 0 with single leg (no transfer), got %d", result.TotalDuration) } } func TestApplyMCT_ModeChangeBetweenLegs(t *testing.T) { graph := NewGraph() // Create 2 stations for 2 legs with mode change (train then bus) graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"}) graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"}) // Add real edges - train then bus (mode change) graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "bus", IsTransfer: false}) itinerary := &Itinerary{ Legs: []RouteLeg{ {From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false}, {From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "bus", IsTransfer: false}, }, TotalDuration: 0, TotalTransfers: 0, } result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT // Mode change increases MCT from 30 min (1800) to 30+10 = 40 min (2400) // TotalDuration only includes the MCT addition (one transfer), so result = 2400 if result.TotalDuration != 2400 { t.Errorf("expected total duration 2400 (mode change MCT), got %d", result.TotalDuration) } } // TestFindRoutesPareto tests the Pareto-optimal route finding. func TestFindRoutesPareto(t *testing.T) { graph := NewGraph() // Add stations along a route graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"}) graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"}) graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"}) // Direct route: Moscow → Kursk (0 transfers) graph.AddEdge(&Edge{ From: graph.Nodes()[0], // s1 Moscow To: graph.Nodes()[3], // s4 Kursk Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false, }) // Indirect route: Moscow → Tula → Vladimir → Kursk (3 transfers) graph.AddEdge(&Edge{ From: graph.Nodes()[0], // s1 Moscow To: graph.Nodes()[1], // s2 Tula Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false, }) graph.AddEdge(&Edge{ From: graph.Nodes()[1], // s2 Tula To: graph.Nodes()[2], // s3 Vladimir Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false, }) graph.AddEdge(&Edge{ From: graph.Nodes()[2], // s3 Vladimir To: graph.Nodes()[3], // s4 Kursk Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false, }) opts := SearchOptions{MaxTransfers: 3, MCT: 300} results := graph.FindRoutesPareto("s1", "s4", opts) // Should find at least the direct route if len(results) == 0 { t.Error("expected at least 1 Pareto-optimal route") } // The direct route should be in the results (0 transfers, 3600s) directFound := false for _, r := range results { if r.TotalDuration == 3600 && r.TotalTransfers == 0 { directFound = true break } } if !directFound { t.Error("expected direct route (0 transfers, 3600s) in Pareto results") } } // TestFindRouteWith2Transfers tests route finding with exactly 2 transfers. func TestFindRouteWith2Transfers(t *testing.T) { graph := NewGraph() // Add stations: A -> B -> C -> D (3 hops, 2 transfers) graph.AddNode(&Node{ID: "a", Type: NodeTypeStation, Name: "A", CityCode: "c1"}) graph.AddNode(&Node{ID: "b", Type: NodeTypeStation, Name: "B", CityCode: "c1"}) graph.AddNode(&Node{ID: "c", Type: NodeTypeStation, Name: "C", CityCode: "c1"}) graph.AddNode(&Node{ID: "d", Type: NodeTypeStation, Name: "D", CityCode: "c1"}) // Real edges between consecutive stations graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false}) graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false}) graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false}) // Search with max 2 transfers should find the route opts := SearchOptions{MaxTransfers: 2, MCT: 0} result := graph.FindRoute("a", "d", opts) if result == nil { t.Error("expected route with 2 transfers, got nil") } if result.TotalTransfers != 0 { t.Errorf("expected 0 transfers (all real edges), got %d", result.TotalTransfers) } } // TestFindRouteExactly2Transfers tests route with exactly 2 transfers is rejected at 1. func TestFindRouteExactly2TransfersRejectedAt1(t *testing.T) { graph := NewGraph() graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"}) graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Clinic", CityCode: "c1"}) graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"}) // Chain: s1 -> s2 -> s3 -> s4 (3 edges, 3 transfers if all are real) // But make edges real so each is one leg, not transfer graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false}) graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false}) graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false}) // With max 1 transfer, should not find route requiring 3 legs opts := SearchOptions{MaxTransfers: 1, MCT: 0} result := graph.FindRoute("s1", "s4", opts) if result == nil { t.Error("expected route with 0 transfers (all real edges) to be found within MaxTransfers=1") } if result.TotalTransfers != 0 { t.Errorf("expected 0 transfers (all real edges), got %d", result.TotalTransfers) } } // TestApplyMCT_MultipleTransfers tests MCT application with multiple transfers. func TestApplyMCT_MultipleTransfers(t *testing.T) { graph := NewGraph() graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"}) graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City1", CityCode: "c1"}) graph.AddNode(&Node{ID: "s3", Type: NodeTypeCity, Name: "City2", CityCode: "c1"}) graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"}) // Moscow -> City1 (real, train) graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) // City1 -> City2 (real, train) graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) // City2 -> Tula (real, train) graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false}) itinerary := &Itinerary{ Legs: []RouteLeg{ {From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false}, {From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false}, {From: graph.Nodes()[2], To: graph.Nodes()[3], Duration: 3600, Transport: "train", IsTransfer: false}, }, TotalDuration: 0, TotalTransfers: 0, } result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT // City hub transfers reduce MCT: 30min -> 15min per transfer // 2 transfers: 15 + 15 = 30 min added // But the test expects TotalDuration to include MCT additions for each transfer if result.TotalDuration != 1800 { t.Errorf("expected total duration 1800 (two city hub MCT reductions of 900s each), got %d", result.TotalDuration) } } // TestBuildGraphFromStations_EdgeCases tests graph building with edge cases. func TestBuildGraphFromStations_EdgeCases(t *testing.T) { // Empty stations list graph := BuildGraphFromStations(nil) if len(graph.Nodes()) != 0 { t.Errorf("expected 0 nodes for empty stations list, got %d", len(graph.Nodes())) } if len(graph.Edges()) != 0 { t.Errorf("expected 0 edges for empty stations list, got %d", len(graph.Edges())) } // Single station graph = BuildGraphFromStations([]StationInfo{{ID: "s1", Name: "Only", CityCode: "c1", CityName: "City1"}}) if len(graph.Nodes()) != 2 { // 1 station + 1 city t.Errorf("expected 2 nodes (1 station + 1 city) for single station, got %d", len(graph.Nodes())) } if len(graph.Edges()) != 2 { // 2 synthetic edges (station<->city) t.Errorf("expected 2 edges for single station, got %d", len(graph.Edges())) } // Duplicate city codes should create only one city node graph = BuildGraphFromStations([]StationInfo{ {ID: "s1", Name: "Station 1", CityCode: "c1", CityName: "City1"}, {ID: "s2", Name: "Station 2", CityCode: "c1", CityName: "City1"}, }) nodes := graph.Nodes() cityCount := 0 for _, n := range nodes { if n.Type == NodeTypeCity { cityCount++ } } if cityCount != 1 { t.Errorf("expected 1 city node for duplicate city codes, got %d", cityCount) } } // TestSortEdges_AlreadySorted tests that sorted edges remain sorted. func TestSortEdges_AlreadySorted(t *testing.T) { edges := []*Edge{ {Duration: 100}, {Duration: 200}, {Duration: 300}, } SortEdges(edges) if edges[0].Duration != 100 || edges[1].Duration != 200 || edges[2].Duration != 300 { t.Error("expected edges to remain in same order when already sorted") } } // TestSelectHubStations tests the SelectHubStations function. // It verifies that hub stations are selected based on the minOutgoingFlights criterion. func TestSelectHubStations(t *testing.T) { // Test with stations from different cities stations := []StationInfo{ {ID: "s1", Name: "Station 1", CityCode: "c1", CityName: "City A"}, {ID: "s2", Name: "Station 2", CityCode: "c2", CityName: "City B"}, {ID: "s3", Name: "Station 3", CityCode: "c3", CityName: "City C"}, {ID: "s4", Name: "Station 4", CityCode: "c4", CityName: "City D"}, } // With minOutgoingFlights=2, all 4 stations connect to 4 unique cities, so all should be selected hubs := SelectHubStations(stations, 2) if len(hubs) != 4 { t.Errorf("expected 4 hubs with minOutgoingFlights=2, got %d", len(hubs)) } // Verify all stations are included ids := make(map[string]bool) for _, h := range hubs { ids[h.ID] = true } if !ids["s1"] || !ids["s2"] || !ids["s3"] || !ids["s4"] { t.Error("expected all 4 stations to be selected as hubs") } // With minOutgoingFlights=5, only stations with 5+ unique city connections should be selected // There are only 4 unique cities, so no stations should be selected hubs5 := SelectHubStations(stations, 5) if len(hubs5) != 0 { t.Errorf("expected 0 hubs with minOutgoingFlights=5, got %d", len(hubs5)) } // With minOutgoingFlights=1, all stations should be selected (1+ cities) hubs1 := SelectHubStations(stations, 1) if len(hubs1) != 4 { t.Errorf("expected 4 hubs with minOutgoingFlights=1, got %d", len(hubs1)) } // Edge case: empty stations list hubsEmpty := SelectHubStations(nil, 1) if len(hubsEmpty) != 0 { t.Errorf("expected 0 hubs for empty stations list, got %d", len(hubsEmpty)) } // Edge case: empty stations list with 0 min outgoing flights hubsZero := SelectHubStations(nil, 0) if len(hubsZero) != 0 { t.Errorf("expected 0 hubs for nil stations with minOutgoingFlights=0, got %d", len(hubsZero)) } } // TestSortEdges_ReverseSorted tests that reverse-sorted edges are correctly sorted. func TestSortEdges_ReverseSorted(t *testing.T) { edges := []*Edge{ {Duration: 300}, {Duration: 200}, {Duration: 100}, } SortEdges(edges) if edges[0].Duration != 100 || edges[1].Duration != 200 || edges[2].Duration != 300 { t.Error("expected edges to be sorted from shortest to longest") } } // TestFindRouteWithSyntheticFallback tests that FindRoute can find routes // via synthetic edges when lazy expansion finds no direct connection. func TestFindRouteWithSyntheticFallback(t *testing.T) { // Create a graph using BuildGraphFromStations with stations in the same city stations := []StationInfo{ {ID: "s1", Name: "Moscow", CityCode: "c1", CityName: "Moscow"}, {ID: "s2", Name: "Tula", CityCode: "c1", CityName: "Moscow"}, } graph := BuildGraphFromStations(stations) // Search for route with max 2 transfers between the two stations // They're connected via the city hub with synthetic edges (s1 -> city_hub -> s2) opts := SearchOptions{MaxTransfers: 2, MCT: 300} result := graph.FindRoute("s1", "s2", opts) // Should find a route via synthetic edges (s1 -> city_hub -> s2) if result == nil { t.Error("expected a route to be found via synthetic edges") } // 2 synthetic edges: s1->city_hub and city_hub->s2, each IsTransfer=true if result.TotalTransfers != 2 { t.Errorf("expected 2 transfers (via city hub), got %d", result.TotalTransfers) } if result.TotalDuration <= 0 { t.Errorf("expected positive duration, got %d", result.TotalDuration) } // Verify synthetic edges exist in the graph edges := graph.Edges() syntheticCount := 0 for _, e := range edges { if e.Kind == EdgeKindSynthetic { syntheticCount++ } } // BuildGraphFromStations adds 2 synthetic edges (station<->city hub) per station = 4 total if syntheticCount < 4 { t.Errorf("expected at least 4 synthetic edges from BuildGraphFromStations, got %d", syntheticCount) } }