package http

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/netip"
	"testing"

	"github.com/netbirdio/netbird/management/server/http/api"
	"github.com/netbirdio/netbird/management/server/status"
	"github.com/netbirdio/netbird/route"

	"github.com/gorilla/mux"
	"github.com/magiconair/properties/assert"

	"github.com/netbirdio/netbird/management/server"
	"github.com/netbirdio/netbird/management/server/jwtclaims"
	"github.com/netbirdio/netbird/management/server/mock_server"
)

const (
	existingRouteID         = "existingRouteID"
	existingRouteID2        = "existingRouteID2" // for peer_groups test
	notFoundRouteID         = "notFoundRouteID"
	existingPeerIP1         = "100.64.0.100"
	existingPeerIP2         = "100.64.0.101"
	notFoundPeerID          = "nonExistingPeer"
	existingPeerKey         = "existingPeerKey"
	nonLinuxExistingPeerKey = "darwinExistingPeerKey"
	testAccountID           = "test_id"
	existingGroupID         = "testGroup"
	notFoundGroupID         = "nonExistingGroup"
)

var emptyString = ""
var existingPeerID = "peer-id"
var nonLinuxExistingPeerID = "darwin-peer-id"

var baseExistingRoute = &route.Route{
	ID:          existingRouteID,
	Description: "base route",
	NetID:       "awesomeNet",
	Network:     netip.MustParsePrefix("192.168.0.0/24"),
	NetworkType: route.IPv4Network,
	Metric:      9999,
	Masquerade:  false,
	Enabled:     true,
	Groups:      []string{existingGroupID},
}

var testingAccount = &server.Account{
	Id:     testAccountID,
	Domain: "hotmail.com",
	Peers: map[string]*server.Peer{
		existingPeerID: {
			Key: existingPeerKey,
			IP:  netip.MustParseAddr(existingPeerIP1).AsSlice(),
			ID:  existingPeerID,
			Meta: server.PeerSystemMeta{
				GoOS: "linux",
			},
		},
		nonLinuxExistingPeerID: {
			Key: nonLinuxExistingPeerID,
			IP:  netip.MustParseAddr(existingPeerIP2).AsSlice(),
			ID:  nonLinuxExistingPeerID,
			Meta: server.PeerSystemMeta{
				GoOS: "darwin",
			},
		},
	},
	Users: map[string]*server.User{
		"test_user": server.NewAdminUser("test_user"),
	},
}

func initRoutesTestData() *RoutesHandler {
	return &RoutesHandler{
		accountManager: &mock_server.MockAccountManager{
			GetRouteFunc: func(_, routeID, _ string) (*route.Route, error) {
				if routeID == existingRouteID {
					return baseExistingRoute, nil
				}
				if routeID == existingRouteID2 {
					route := baseExistingRoute.Copy()
					route.PeerGroups = []string{existingGroupID}
					return route, nil
				}
				return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID)
			},
			CreateRouteFunc: func(accountID, network, peerID string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, _ string) (*route.Route, error) {
				if peerID == notFoundPeerID {
					return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
				}
				if len(peerGroups) > 0 && peerGroups[0] == notFoundGroupID {
					return nil, status.Errorf(status.InvalidArgument, "peer groups with ID %s not found", peerGroups[0])
				}
				networkType, p, _ := route.ParseNetwork(network)
				return &route.Route{
					ID:          existingRouteID,
					NetID:       netID,
					Peer:        peerID,
					PeerGroups:  peerGroups,
					Network:     p,
					NetworkType: networkType,
					Description: description,
					Masquerade:  masquerade,
					Enabled:     enabled,
					Groups:      groups,
				}, nil
			},
			SaveRouteFunc: func(_, _ string, r *route.Route) error {
				if r.Peer == notFoundPeerID {
					return status.Errorf(status.InvalidArgument, "peer with ID %s not found", r.Peer)
				}
				return nil
			},
			DeleteRouteFunc: func(_ string, routeID string, _ string) error {
				if routeID != existingRouteID {
					return status.Errorf(status.NotFound, "Peer with ID %s not found", routeID)
				}
				return nil
			},
			GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
				return testingAccount, testingAccount.Users["test_user"], nil
			},
		},
		claimsExtractor: jwtclaims.NewClaimsExtractor(
			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
				return jwtclaims.AuthorizationClaims{
					UserId:    "test_user",
					Domain:    "hotmail.com",
					AccountId: testAccountID,
				}
			}),
		),
	}
}

func TestRoutesHandlers(t *testing.T) {
	baseExistingRouteWithPeerGroups := baseExistingRoute.Copy()
	baseExistingRouteWithPeerGroups.PeerGroups = []string{existingGroupID}

	tt := []struct {
		name           string
		expectedStatus int
		expectedBody   bool
		expectedRoute  *api.Route
		requestType    string
		requestPath    string
		requestBody    io.Reader
	}{
		{
			name:           "Get Existing Route",
			requestType:    http.MethodGet,
			requestPath:    "/api/routes/" + existingRouteID,
			expectedStatus: http.StatusOK,
			expectedBody:   true,
			expectedRoute:  toRouteResponse(baseExistingRoute),
		},
		{
			name:           "Get Not Existing Route",
			requestType:    http.MethodGet,
			requestPath:    "/api/routes/" + notFoundRouteID,
			expectedStatus: http.StatusNotFound,
		},
		{
			name:           "Get Existing Route with Peer Groups",
			requestType:    http.MethodGet,
			requestPath:    "/api/routes/" + existingRouteID2,
			expectedStatus: http.StatusOK,
			expectedBody:   true,
			expectedRoute:  toRouteResponse(baseExistingRouteWithPeerGroups),
		},
		{
			name:           "Delete Existing Route",
			requestType:    http.MethodDelete,
			requestPath:    "/api/routes/" + existingRouteID,
			expectedStatus: http.StatusOK,
			expectedBody:   false,
		},
		{
			name:           "Delete Not Existing Route",
			requestType:    http.MethodDelete,
			requestPath:    "/api/routes/" + notFoundRouteID,
			expectedStatus: http.StatusNotFound,
		},
		{
			name:        "POST OK",
			requestType: http.MethodPost,
			requestPath: "/api/routes",
			requestBody: bytes.NewBuffer(
				[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID))),
			expectedStatus: http.StatusOK,
			expectedBody:   true,
			expectedRoute: &api.Route{
				Id:          existingRouteID,
				Description: "Post",
				NetworkId:   "awesomeNet",
				Network:     "192.168.0.0/16",
				Peer:        &existingPeerID,
				NetworkType: route.IPv4NetworkString,
				Masquerade:  false,
				Enabled:     false,
				Groups:      []string{existingGroupID},
			},
		},
		{
			name:           "POST Non Linux Peer",
			requestType:    http.MethodPost,
			requestPath:    "/api/routes",
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", nonLinuxExistingPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:           "POST Not Found Peer",
			requestType:    http.MethodPost,
			requestPath:    "/api/routes",
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", notFoundPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:           "POST Invalid Network Identifier",
			requestType:    http.MethodPost,
			requestPath:    "/api/routes",
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:           "POST Invalid Network",
			requestType:    http.MethodPost,
			requestPath:    "/api/routes",
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:        "POST UnprocessableEntity when both peer and peer_groups are provided",
			requestType: http.MethodPost,
			requestPath: "/api/routes",
			requestBody: bytes.NewBuffer(
				[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer\":\"%s\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingPeerID, existingGroupID, existingGroupID))),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:        "POST UnprocessableEntity when no peer and peer_groups are provided",
			requestType: http.MethodPost,
			requestPath: "/api/routes",
			requestBody: bytes.NewBuffer(
				[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"groups\":[\"%s\"]}", existingPeerID))),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:           "PUT OK",
			requestType:    http.MethodPut,
			requestPath:    "/api/routes/" + existingRouteID,
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)),
			expectedStatus: http.StatusOK,
			expectedBody:   true,
			expectedRoute: &api.Route{
				Id:          existingRouteID,
				Description: "Post",
				NetworkId:   "awesomeNet",
				Network:     "192.168.0.0/16",
				Peer:        &existingPeerID,
				NetworkType: route.IPv4NetworkString,
				Masquerade:  false,
				Enabled:     false,
				Groups:      []string{existingGroupID},
			},
		},
		{
			name:           "PUT OK when peer_groups provided",
			requestType:    http.MethodPut,
			requestPath:    "/api/routes/" + existingRouteID,
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingGroupID, existingGroupID)),
			expectedStatus: http.StatusOK,
			expectedBody:   true,
			expectedRoute: &api.Route{
				Id:          existingRouteID,
				Description: "Post",
				NetworkId:   "awesomeNet",
				Network:     "192.168.0.0/16",
				Peer:        &emptyString,
				PeerGroups:  &[]string{existingGroupID},
				NetworkType: route.IPv4NetworkString,
				Masquerade:  false,
				Enabled:     false,
				Groups:      []string{existingGroupID},
			},
		},
		{
			name:           "PUT Not Found Route",
			requestType:    http.MethodPut,
			requestPath:    "/api/routes/" + notFoundRouteID,
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)),
			expectedStatus: http.StatusNotFound,
			expectedBody:   false,
		},
		{
			name:           "PUT Not Found Peer",
			requestType:    http.MethodPut,
			requestPath:    "/api/routes/" + existingRouteID,
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", notFoundPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:           "PUT Non Linux Peer",
			requestType:    http.MethodPut,
			requestPath:    "/api/routes/" + existingRouteID,
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", nonLinuxExistingPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:           "PUT Invalid Network Identifier",
			requestType:    http.MethodPut,
			requestPath:    "/api/routes/" + existingRouteID,
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:           "PUT Invalid Network",
			requestType:    http.MethodPut,
			requestPath:    "/api/routes/" + existingRouteID,
			requestBody:    bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:        "PUT UnprocessableEntity when both peer and peer_groups are provided",
			requestType: http.MethodPut,
			requestPath: "/api/routes/" + existingRouteID,
			requestBody: bytes.NewBuffer(
				[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer\":\"%s\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingPeerID, existingGroupID, existingGroupID))),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
		{
			name:        "PUT UnprocessableEntity when no peer and peer_groups are provided",
			requestType: http.MethodPut,
			requestPath: "/api/routes/" + existingRouteID,
			requestBody: bytes.NewBuffer(
				[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"groups\":[\"%s\"]}", existingPeerID))),
			expectedStatus: http.StatusUnprocessableEntity,
			expectedBody:   false,
		},
	}

	p := initRoutesTestData()

	for _, tc := range tt {
		t.Run(tc.name, func(t *testing.T) {
			recorder := httptest.NewRecorder()
			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)

			router := mux.NewRouter()
			router.HandleFunc("/api/routes/{routeId}", p.GetRoute).Methods("GET")
			router.HandleFunc("/api/routes/{routeId}", p.DeleteRoute).Methods("DELETE")
			router.HandleFunc("/api/routes", p.CreateRoute).Methods("POST")
			router.HandleFunc("/api/routes/{routeId}", p.UpdateRoute).Methods("PUT")
			router.ServeHTTP(recorder, req)

			res := recorder.Result()
			defer res.Body.Close()

			content, err := io.ReadAll(res.Body)
			if err != nil {
				t.Fatalf("I don't know what I expected; %v", err)
			}

			if status := recorder.Code; status != tc.expectedStatus {
				t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
					status, tc.expectedStatus, string(content))
				return
			}

			if !tc.expectedBody {
				return
			}

			got := &api.Route{}
			if err = json.Unmarshal(content, &got); err != nil {
				t.Fatalf("Sent content is not in correct json format; %v", err)
			}
			assert.Equal(t, got, tc.expectedRoute)
		})
	}
}
