// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build !plan9

package main

import (
	"context"
	"testing"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
	"tailscale.com/tstest"
)

func TestGetServicesNodePortRangeFromErr(t *testing.T) {
	tests := []struct {
		name   string
		errStr string
		want   string
	}{
		{
			name:   "valid_error_string",
			errStr: "NodePort 777777 is not in the allowed range 30000-32767",
			want:   "30000-32767",
		},
		{
			name:   "error_string_with_different_message",
			errStr: "some other error without a port range",
			want:   "",
		},
		{
			name:   "error_string_with_multiple_port_ranges",
			errStr: "range 1000-2000 and another range 3000-4000",
			want:   "",
		},
		{
			name:   "empty_error_string",
			errStr: "",
			want:   "",
		},
		{
			name:   "error_string_with_range_at_start",
			errStr: "30000-32767 is the range",
			want:   "30000-32767",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := getServicesNodePortRangeFromErr(tt.errStr); got != tt.want {
				t.Errorf("got %v, want %v", got, tt.want)
			}
		})
	}
}

func TestParseServicesNodePortRange(t *testing.T) {
	tests := []struct {
		name    string
		p       string
		want    *tsapi.PortRange
		wantErr bool
	}{
		{
			name:    "valid_range",
			p:       "30000-32767",
			want:    &tsapi.PortRange{Port: 30000, EndPort: 32767},
			wantErr: false,
		},
		{
			name:    "single_port_range",
			p:       "30000",
			want:    &tsapi.PortRange{Port: 30000, EndPort: 30000},
			wantErr: false,
		},
		{
			name:    "invalid_format_non_numeric_end",
			p:       "30000-abc",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "invalid_format_non_numeric_start",
			p:       "abc-32767",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "empty_string",
			p:       "",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "too_many_parts",
			p:       "1-2-3",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "port_too_large_start",
			p:       "65536-65537",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "port_too_large_end",
			p:       "30000-65536",
			want:    nil,
			wantErr: true,
		},
		{
			name:    "inverted_range",
			p:       "32767-30000",
			want:    nil,
			wantErr: true, // IsValid() will fail
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			portRange, err := parseServicesNodePortRange(tt.p)
			if (err != nil) != tt.wantErr {
				t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if tt.wantErr {
				return
			}

			if portRange == nil {
				t.Fatalf("got nil port range, expected %v", tt.want)
			}

			if portRange.Port != tt.want.Port || portRange.EndPort != tt.want.EndPort {
				t.Errorf("got = %v, want %v", portRange, tt.want)
			}
		})
	}
}

func TestValidateNodePortRanges(t *testing.T) {
	tests := []struct {
		name       string
		portRanges []tsapi.PortRange
		wantErr    bool
	}{
		{
			name: "valid_ranges_with_unknown_kube_range",
			portRanges: []tsapi.PortRange{
				{Port: 30003, EndPort: 30005},
				{Port: 30006, EndPort: 30007},
			},
			wantErr: false,
		},
		{
			name: "overlapping_ranges",
			portRanges: []tsapi.PortRange{
				{Port: 30000, EndPort: 30010},
				{Port: 30005, EndPort: 30015},
			},
			wantErr: true,
		},
		{
			name: "adjacent_ranges_no_overlap",
			portRanges: []tsapi.PortRange{
				{Port: 30010, EndPort: 30020},
				{Port: 30021, EndPort: 30022},
			},
			wantErr: false,
		},
		{
			name: "identical_ranges_are_overlapping",
			portRanges: []tsapi.PortRange{
				{Port: 30005, EndPort: 30010},
				{Port: 30005, EndPort: 30010},
			},
			wantErr: true,
		},
		{
			name: "range_clashes_with_existing_proxyclass",
			portRanges: []tsapi.PortRange{
				{Port: 31005, EndPort: 32070},
			},
			wantErr: true,
		},
	}

	// as part of this test, we want to create an adjacent ProxyClass in order to ensure that if it clashes with the one created in this test
	// that we get an error
	cl := tstest.NewClock(tstest.ClockOpts{})
	opc := &tsapi.ProxyClass{
		ObjectMeta: metav1.ObjectMeta{
			Name: "other-pc",
		},
		Spec: tsapi.ProxyClassSpec{
			StatefulSet: &tsapi.StatefulSet{
				Annotations: defaultProxyClassAnnotations,
			},
			StaticEndpoints: &tsapi.StaticEndpointsConfig{
				NodePort: &tsapi.NodePortConfig{
					Ports: []tsapi.PortRange{
						{Port: 31000}, {Port: 32000},
					},
					Selector: map[string]string{
						"foo/bar": "baz",
					},
				},
			},
		},
		Status: tsapi.ProxyClassStatus{
			Conditions: []metav1.Condition{{
				Type:               string(tsapi.ProxyClassReady),
				Status:             metav1.ConditionTrue,
				Reason:             reasonProxyClassValid,
				Message:            reasonProxyClassValid,
				LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
			}},
		},
	}

	fc := fake.NewClientBuilder().
		WithObjects(opc).
		WithStatusSubresource(opc).
		WithScheme(tsapi.GlobalScheme).
		Build()

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pc := &tsapi.ProxyClass{
				ObjectMeta: metav1.ObjectMeta{
					Name: "pc",
				},
				Spec: tsapi.ProxyClassSpec{
					StatefulSet: &tsapi.StatefulSet{
						Annotations: defaultProxyClassAnnotations,
					},
					StaticEndpoints: &tsapi.StaticEndpointsConfig{
						NodePort: &tsapi.NodePortConfig{
							Ports: tt.portRanges,
							Selector: map[string]string{
								"foo/bar": "baz",
							},
						},
					},
				},
				Status: tsapi.ProxyClassStatus{
					Conditions: []metav1.Condition{{
						Type:               string(tsapi.ProxyClassReady),
						Status:             metav1.ConditionTrue,
						Reason:             reasonProxyClassValid,
						Message:            reasonProxyClassValid,
						LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
					}},
				},
			}
			err := validateNodePortRanges(context.Background(), fc, &tsapi.PortRange{Port: 30000, EndPort: 32767}, pc)
			if (err != nil) != tt.wantErr {
				t.Errorf("unexpected error: %v", err)
			}
		})
	}
}

func TestGetRandomPort(t *testing.T) {
	for range 100 {
		port := getRandomPort()
		if port < tailscaledPortMin || port > tailscaledPortMax {
			t.Errorf("generated port %d which is out of range [%d, %d]", port, tailscaledPortMin, tailscaledPortMax)
		}
	}
}
