package client

import (
	"fmt"
	"net/netip"
	"testing"
	"time"

	"github.com/netbirdio/netbird/client/internal/peer"
	"github.com/netbirdio/netbird/client/internal/routemanager/common"
	"github.com/netbirdio/netbird/client/internal/routemanager/static"
	"github.com/netbirdio/netbird/route"
)

func TestGetBestrouteFromStatuses(t *testing.T) {
	testCases := []struct {
		name            string
		statuses        map[route.ID]routerPeerStatus
		expectedRouteID route.ID
		currentRoute    route.ID
		existingRoutes  map[route.ID]*route.Route
	}{
		{
			name: "one route",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: false,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "one connected routes with relayed and direct",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: true,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "one connected routes with relayed and no direct",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: true,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "no connected peers",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnecting,
					relayed: false,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
			},
			currentRoute:    "",
			expectedRouteID: "",
		},
		{
			name: "multiple connected peers with different metrics",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: false,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: 9000,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "multiple connected peers with one relayed",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: false,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: true,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "multiple connected peers with different latencies",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					latency: 300 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "should ignore routes with latency 0",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					latency: 0 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "current route with similar score and similar but slightly worse latency should not change",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 15 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "route1",
			expectedRouteID: "route1",
		},
		{
			name: "relayed routes with latency 0 should maintain previous choice",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: true,
					latency: 0 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: true,
					latency: 0 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "route1",
			expectedRouteID: "route1",
		},
		{
			name: "p2p routes with latency 0 should maintain previous choice",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 0 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 0 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "route1",
			expectedRouteID: "route1",
		},
		{
			name: "current route with bad score should be changed to route with better score",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 200 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "route1",
			expectedRouteID: "route2",
		},
		{
			name: "current chosen route doesn't exist anymore",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 20 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "routeDoesntExistAnymore",
			expectedRouteID: "route2",
		},
		{
			name: "connected peer should be preferred over idle peer",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 100 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "idle peer should be selected when no connected peers",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnecting,
					relayed: false,
					latency: 5 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "best idle peer should be selected among multiple idle peers",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 100 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "connecting peers should not be considered for routing",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnecting,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnecting,
					relayed: false,
					latency: 5 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "",
		},
		{
			name: "mixed statuses - connected wins over idle and connecting",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusConnecting,
					relayed: false,
					latency: 5 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
				"route3": {
					status:  peer.StatusConnected,
					relayed: true,
					latency: 200 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
				"route3": {
					ID:     "route3",
					Metric: route.MaxMetric,
					Peer:   "peer3",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route3",
		},
		{
			name: "idle peer with better metric should win over idle peer with worse metric",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 50 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 50 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: 5000,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "current idle route should be maintained for similar scores",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 20 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 15 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "route1",
			expectedRouteID: "route1",
		},
		{
			name: "idle peer with zero latency should still be considered",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 0 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnecting,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "direct idle peer preferred over relayed idle peer",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: true,
					latency: 10 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 50 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "connected peer with worse metric still beats idle peer with better metric",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 50 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: 1000,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "connected peer wins even when idle peer has all advantages",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 1 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: true,
					latency: 30 * time.Minute,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: 1,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "connected peer should be preferred over idle peer",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnected,
					relayed: false,
					latency: 100 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
		{
			name: "idle peer should be selected when no connected peers",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusConnecting,
					relayed: false,
					latency: 5 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route1",
		},
		{
			name: "best idle peer should be selected among multiple idle peers",
			statuses: map[route.ID]routerPeerStatus{
				"route1": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 100 * time.Millisecond,
				},
				"route2": {
					status:  peer.StatusIdle,
					relayed: false,
					latency: 10 * time.Millisecond,
				},
			},
			existingRoutes: map[route.ID]*route.Route{
				"route1": {
					ID:     "route1",
					Metric: route.MaxMetric,
					Peer:   "peer1",
				},
				"route2": {
					ID:     "route2",
					Metric: route.MaxMetric,
					Peer:   "peer2",
				},
			},
			currentRoute:    "",
			expectedRouteID: "route2",
		},
	}

	// fill the test data with random routes
	for _, tc := range testCases {
		for i := 0; i < 50; i++ {
			dummyRoute := &route.Route{
				ID:     route.ID(fmt.Sprintf("dummy_p1_%d", i)),
				Metric: route.MinMetric,
				Peer:   fmt.Sprintf("dummy_p1_%d", i),
			}
			tc.existingRoutes[dummyRoute.ID] = dummyRoute
		}
		for i := 0; i < 50; i++ {
			dummyRoute := &route.Route{
				ID:     route.ID(fmt.Sprintf("dummy_p2_%d", i)),
				Metric: route.MinMetric,
				Peer:   fmt.Sprintf("dummy_p1_%d", i),
			}
			tc.existingRoutes[dummyRoute.ID] = dummyRoute
		}

		for i := 0; i < 50; i++ {
			id := route.ID(fmt.Sprintf("dummy_p1_%d", i))
			dummyStatus := routerPeerStatus{
				status:  peer.StatusConnecting,
				relayed: true,
				latency: 0,
			}
			tc.statuses[id] = dummyStatus
		}
		for i := 0; i < 50; i++ {
			id := route.ID(fmt.Sprintf("dummy_p2_%d", i))
			dummyStatus := routerPeerStatus{
				status:  peer.StatusConnecting,
				relayed: true,
				latency: 0,
			}
			tc.statuses[id] = dummyStatus
		}
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			currentRoute := &route.Route{
				ID: "routeDoesntExistAnymore",
			}
			if tc.currentRoute != "" {
				currentRoute = tc.existingRoutes[tc.currentRoute]
			}

			params := common.HandlerParams{
				Route: &route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")},
			}
			// create new clientNetwork
			client := &Watcher{
				handler:       static.NewRoute(params),
				routes:        tc.existingRoutes,
				currentChosen: currentRoute,
			}

			chosenRoute, _ := client.getBestRouteFromStatuses(tc.statuses)
			if chosenRoute != tc.expectedRouteID {
				t.Errorf("expected routeID %s, got %s", tc.expectedRouteID, chosenRoute)
			}
		})
	}
}
