package util

import (
	"errors"
	"net/netip"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"tailscale.com/tailcfg"
)

func TestTailscaleVersionNewerOrEqual(t *testing.T) {
	type args struct {
		minimum string
		toCheck string
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "is-equal",
			args: args{
				minimum: "1.56",
				toCheck: "1.56",
			},
			want: true,
		},
		{
			name: "is-newer-head",
			args: args{
				minimum: "1.56",
				toCheck: "head",
			},
			want: true,
		},
		{
			name: "is-newer-unstable",
			args: args{
				minimum: "1.56",
				toCheck: "unstable",
			},
			want: true,
		},
		{
			name: "is-newer-patch",
			args: args{
				minimum: "1.56.1",
				toCheck: "1.56.1",
			},
			want: true,
		},
		{
			name: "is-older-patch-same-minor",
			args: args{
				minimum: "1.56.1",
				toCheck: "1.56.0",
			},
			want: false,
		},
		{
			name: "is-older-unstable",
			args: args{
				minimum: "1.56",
				toCheck: "1.55",
			},
			want: false,
		},
		{
			name: "is-older-one-stable",
			args: args{
				minimum: "1.56",
				toCheck: "1.54",
			},
			want: false,
		},
		{
			name: "is-older-five-stable",
			args: args{
				minimum: "1.56",
				toCheck: "1.46",
			},
			want: false,
		},
		{
			name: "is-older-patch",
			args: args{
				minimum: "1.56",
				toCheck: "1.48.1",
			},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := TailscaleVersionNewerOrEqual(tt.args.minimum, tt.args.toCheck); got != tt.want {
				t.Errorf("TailscaleVersionNewerThan() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestParseLoginURLFromCLILogin(t *testing.T) {
	tests := []struct {
		name    string
		output  string
		wantURL string
		wantErr string
	}{
		{
			name: "valid https URL",
			output: `
To authenticate, visit:

        https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi

Success.`,
			wantURL: "https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi",
			wantErr: "",
		},
		{
			name: "valid http URL",
			output: `
To authenticate, visit:

        http://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi

Success.`,
			wantURL: "http://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi",
			wantErr: "",
		},
		{
			name: "no URL",
			output: `
To authenticate, visit:

Success.`,
			wantURL: "",
			wantErr: "no URL found",
		},
		{
			name: "multiple URLs",
			output: `
To authenticate, visit:

        https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi

To authenticate, visit:

        http://headscale.example.com/register/dv1l2k5FackOYl-7-V3mSd_E

Success.`,
			wantURL: "",
			wantErr: "multiple URLs found: https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi and http://headscale.example.com/register/dv1l2k5FackOYl-7-V3mSd_E",
		},
		{
			name: "invalid URL",
			output: `
To authenticate, visit:

        invalid-url

Success.`,
			wantURL: "",
			wantErr: "no URL found",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotURL, err := ParseLoginURLFromCLILogin(tt.output)
			if tt.wantErr != "" {
				if err == nil || err.Error() != tt.wantErr {
					t.Errorf("ParseLoginURLFromCLILogin() error = %v, wantErr %v", err, tt.wantErr)
				}
			} else {
				if err != nil {
					t.Errorf("ParseLoginURLFromCLILogin() error = %v, wantErr %v", err, tt.wantErr)
				}
				if gotURL.String() != tt.wantURL {
					t.Errorf("ParseLoginURLFromCLILogin() = %v, want %v", gotURL, tt.wantURL)
				}
			}
		})
	}
}

func TestParseTraceroute(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		want    Traceroute
		wantErr bool
	}{
		{
			name: "simple successful traceroute",
			input: `traceroute to 172.24.0.3 (172.24.0.3), 30 hops max, 46 byte packets
 1  ts-head-hk0urr.headscale.net (100.64.0.1)  1.135 ms  0.922 ms  0.619 ms
 2  172.24.0.3 (172.24.0.3)  0.593 ms  0.549 ms  0.522 ms`,
			want: Traceroute{
				Hostname: "172.24.0.3",
				IP:       netip.MustParseAddr("172.24.0.3"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "ts-head-hk0urr.headscale.net",
						IP:       netip.MustParseAddr("100.64.0.1"),
						Latencies: []time.Duration{
							1135 * time.Microsecond,
							922 * time.Microsecond,
							619 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "172.24.0.3",
						IP:       netip.MustParseAddr("172.24.0.3"),
						Latencies: []time.Duration{
							593 * time.Microsecond,
							549 * time.Microsecond,
							522 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "traceroute with timeouts",
			input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  router.local (192.168.1.1)  1.234 ms  1.123 ms  1.121 ms
 2  * * *
 3  isp-gateway.net (10.0.0.1)  15.678 ms  14.789 ms  15.432 ms
 4  8.8.8.8 (8.8.8.8)  20.123 ms  19.876 ms  20.345 ms`,
			want: Traceroute{
				Hostname: "8.8.8.8",
				IP:       netip.MustParseAddr("8.8.8.8"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "router.local",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							1234 * time.Microsecond,
							1123 * time.Microsecond,
							1121 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "*",
					},
					{
						Hop:      3,
						Hostname: "isp-gateway.net",
						IP:       netip.MustParseAddr("10.0.0.1"),
						Latencies: []time.Duration{
							15678 * time.Microsecond,
							14789 * time.Microsecond,
							15432 * time.Microsecond,
						},
					},
					{
						Hop:      4,
						Hostname: "8.8.8.8",
						IP:       netip.MustParseAddr("8.8.8.8"),
						Latencies: []time.Duration{
							20123 * time.Microsecond,
							19876 * time.Microsecond,
							20345 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "unsuccessful traceroute",
			input: `traceroute to 10.0.0.99 (10.0.0.99), 5 hops max, 60 byte packets
 1  router.local (192.168.1.1)  1.234 ms  1.123 ms  1.121 ms
 2  * * *
 3  * * *
 4  * * *
 5  * * *`,
			want: Traceroute{
				Hostname: "10.0.0.99",
				IP:       netip.MustParseAddr("10.0.0.99"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "router.local",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							1234 * time.Microsecond,
							1123 * time.Microsecond,
							1121 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "*",
					},
					{
						Hop:      3,
						Hostname: "*",
					},
					{
						Hop:      4,
						Hostname: "*",
					},
					{
						Hop:      5,
						Hostname: "*",
					},
				},
				Success: false,
				Err:     errors.New("traceroute did not reach target"),
			},
			wantErr: false,
		},
		{
			name:    "empty input",
			input:   "",
			want:    Traceroute{},
			wantErr: true,
		},
		{
			name:    "invalid header",
			input:   "not a valid traceroute output",
			want:    Traceroute{},
			wantErr: true,
		},
		{
			name: "windows tracert format",
			input: `Tracing route to google.com [8.8.8.8]
over a maximum of 30 hops:

  1    <1 ms    <1 ms    <1 ms  router.local [192.168.1.1]
  2     5 ms     4 ms     5 ms  10.0.0.1
  3     *        *        *     Request timed out.
  4    20 ms    19 ms    21 ms  8.8.8.8`,
			want: Traceroute{
				Hostname: "google.com",
				IP:       netip.MustParseAddr("8.8.8.8"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "router.local",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							1 * time.Millisecond,
							1 * time.Millisecond,
							1 * time.Millisecond,
						},
					},
					{
						Hop:      2,
						Hostname: "10.0.0.1",
						IP:       netip.MustParseAddr("10.0.0.1"),
						Latencies: []time.Duration{
							5 * time.Millisecond,
							4 * time.Millisecond,
							5 * time.Millisecond,
						},
					},
					{
						Hop:      3,
						Hostname: "*",
					},
					{
						Hop:      4,
						Hostname: "8.8.8.8",
						IP:       netip.MustParseAddr("8.8.8.8"),
						Latencies: []time.Duration{
							20 * time.Millisecond,
							19 * time.Millisecond,
							21 * time.Millisecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "mixed latency formats",
			input: `traceroute to 192.168.1.1 (192.168.1.1), 30 hops max, 60 byte packets
 1  gateway (192.168.1.1)  0.5 ms  *  0.4 ms`,
			want: Traceroute{
				Hostname: "192.168.1.1",
				IP:       netip.MustParseAddr("192.168.1.1"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "gateway",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							500 * time.Microsecond,
							400 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "only one latency value",
			input: `traceroute to 10.0.0.1 (10.0.0.1), 30 hops max, 60 byte packets
 1  10.0.0.1 (10.0.0.1)  1.5 ms`,
			want: Traceroute{
				Hostname: "10.0.0.1",
				IP:       netip.MustParseAddr("10.0.0.1"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "10.0.0.1",
						IP:       netip.MustParseAddr("10.0.0.1"),
						Latencies: []time.Duration{
							1500 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "backward compatibility - original format with 3 latencies",
			input: `traceroute to 172.24.0.3 (172.24.0.3), 30 hops max, 46 byte packets
 1  ts-head-hk0urr.headscale.net (100.64.0.1)  1.135 ms  0.922 ms  0.619 ms
 2  172.24.0.3 (172.24.0.3)  0.593 ms  0.549 ms  0.522 ms`,
			want: Traceroute{
				Hostname: "172.24.0.3",
				IP:       netip.MustParseAddr("172.24.0.3"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "ts-head-hk0urr.headscale.net",
						IP:       netip.MustParseAddr("100.64.0.1"),
						Latencies: []time.Duration{
							1135 * time.Microsecond,
							922 * time.Microsecond,
							619 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "172.24.0.3",
						IP:       netip.MustParseAddr("172.24.0.3"),
						Latencies: []time.Duration{
							593 * time.Microsecond,
							549 * time.Microsecond,
							522 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "two latencies only - common on packet loss",
			input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  gateway (192.168.1.1)  1.2 ms  1.1 ms`,
			want: Traceroute{
				Hostname: "8.8.8.8",
				IP:       netip.MustParseAddr("8.8.8.8"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "gateway",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							1200 * time.Microsecond,
							1100 * time.Microsecond,
						},
					},
				},
				Success: false,
				Err:     errors.New("traceroute did not reach target"),
			},
			wantErr: false,
		},
		{
			name: "hostname without parentheses - some traceroute versions",
			input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  192.168.1.1  1.2 ms  1.1 ms  1.0 ms
 2  8.8.8.8  20.1 ms  19.9 ms  20.2 ms`,
			want: Traceroute{
				Hostname: "8.8.8.8",
				IP:       netip.MustParseAddr("8.8.8.8"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "192.168.1.1",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							1200 * time.Microsecond,
							1100 * time.Microsecond,
							1000 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "8.8.8.8",
						IP:       netip.MustParseAddr("8.8.8.8"),
						Latencies: []time.Duration{
							20100 * time.Microsecond,
							19900 * time.Microsecond,
							20200 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "ipv6 traceroute",
			input: `traceroute to 2001:4860:4860::8888 (2001:4860:4860::8888), 30 hops max, 80 byte packets
 1  2001:db8::1 (2001:db8::1)  1.123 ms  1.045 ms  0.987 ms
 2  2001:4860:4860::8888 (2001:4860:4860::8888)  15.234 ms  14.876 ms  15.123 ms`,
			want: Traceroute{
				Hostname: "2001:4860:4860::8888",
				IP:       netip.MustParseAddr("2001:4860:4860::8888"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "2001:db8::1",
						IP:       netip.MustParseAddr("2001:db8::1"),
						Latencies: []time.Duration{
							1123 * time.Microsecond,
							1045 * time.Microsecond,
							987 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "2001:4860:4860::8888",
						IP:       netip.MustParseAddr("2001:4860:4860::8888"),
						Latencies: []time.Duration{
							15234 * time.Microsecond,
							14876 * time.Microsecond,
							15123 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "macos traceroute with extra spacing",
			input: `traceroute to google.com (8.8.8.8), 64 hops max, 52 byte packets
 1  router.home (192.168.1.1)   2.345 ms   1.234 ms   1.567 ms
 2  * * *
 3  isp-gw.net (10.1.1.1)   15.234 ms   14.567 ms   15.890 ms
 4  google.com (8.8.8.8)   20.123 ms   19.456 ms   20.789 ms`,
			want: Traceroute{
				Hostname: "google.com",
				IP:       netip.MustParseAddr("8.8.8.8"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "router.home",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							2345 * time.Microsecond,
							1234 * time.Microsecond,
							1567 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "*",
					},
					{
						Hop:      3,
						Hostname: "isp-gw.net",
						IP:       netip.MustParseAddr("10.1.1.1"),
						Latencies: []time.Duration{
							15234 * time.Microsecond,
							14567 * time.Microsecond,
							15890 * time.Microsecond,
						},
					},
					{
						Hop:      4,
						Hostname: "google.com",
						IP:       netip.MustParseAddr("8.8.8.8"),
						Latencies: []time.Duration{
							20123 * time.Microsecond,
							19456 * time.Microsecond,
							20789 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "busybox traceroute minimal format",
			input: `traceroute to 10.0.0.1 (10.0.0.1), 30 hops max, 38 byte packets
 1  10.0.0.1 (10.0.0.1)  1.234 ms  1.123 ms  1.456 ms`,
			want: Traceroute{
				Hostname: "10.0.0.1",
				IP:       netip.MustParseAddr("10.0.0.1"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "10.0.0.1",
						IP:       netip.MustParseAddr("10.0.0.1"),
						Latencies: []time.Duration{
							1234 * time.Microsecond,
							1123 * time.Microsecond,
							1456 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "linux traceroute with dns failure fallback to IP",
			input: `traceroute to example.com (93.184.216.34), 30 hops max, 60 byte packets
 1  192.168.1.1 (192.168.1.1)  1.234 ms  1.123 ms  1.098 ms
 2  10.0.0.1 (10.0.0.1)  5.678 ms  5.432 ms  5.321 ms
 3  93.184.216.34 (93.184.216.34)  20.123 ms  19.876 ms  20.234 ms`,
			want: Traceroute{
				Hostname: "example.com",
				IP:       netip.MustParseAddr("93.184.216.34"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "192.168.1.1",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							1234 * time.Microsecond,
							1123 * time.Microsecond,
							1098 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "10.0.0.1",
						IP:       netip.MustParseAddr("10.0.0.1"),
						Latencies: []time.Duration{
							5678 * time.Microsecond,
							5432 * time.Microsecond,
							5321 * time.Microsecond,
						},
					},
					{
						Hop:      3,
						Hostname: "93.184.216.34",
						IP:       netip.MustParseAddr("93.184.216.34"),
						Latencies: []time.Duration{
							20123 * time.Microsecond,
							19876 * time.Microsecond,
							20234 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "alpine linux traceroute with ms variations",
			input: `traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 46 byte packets
 1  gateway (192.168.0.1)  0.456ms  0.389ms  0.412ms
 2  1.1.1.1 (1.1.1.1)  8.234ms  7.987ms  8.123ms`,
			want: Traceroute{
				Hostname: "1.1.1.1",
				IP:       netip.MustParseAddr("1.1.1.1"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "gateway",
						IP:       netip.MustParseAddr("192.168.0.1"),
						Latencies: []time.Duration{
							456 * time.Microsecond,
							389 * time.Microsecond,
							412 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "1.1.1.1",
						IP:       netip.MustParseAddr("1.1.1.1"),
						Latencies: []time.Duration{
							8234 * time.Microsecond,
							7987 * time.Microsecond,
							8123 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
		{
			name: "mixed asterisk and latency values",
			input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  gateway (192.168.1.1)  *  1.234 ms  1.123 ms
 2  10.0.0.1 (10.0.0.1)  5.678 ms  *  5.432 ms
 3  8.8.8.8 (8.8.8.8)  20.123 ms  19.876 ms  *`,
			want: Traceroute{
				Hostname: "8.8.8.8",
				IP:       netip.MustParseAddr("8.8.8.8"),
				Route: []TraceroutePath{
					{
						Hop:      1,
						Hostname: "gateway",
						IP:       netip.MustParseAddr("192.168.1.1"),
						Latencies: []time.Duration{
							1234 * time.Microsecond,
							1123 * time.Microsecond,
						},
					},
					{
						Hop:      2,
						Hostname: "10.0.0.1",
						IP:       netip.MustParseAddr("10.0.0.1"),
						Latencies: []time.Duration{
							5678 * time.Microsecond,
							5432 * time.Microsecond,
						},
					},
					{
						Hop:      3,
						Hostname: "8.8.8.8",
						IP:       netip.MustParseAddr("8.8.8.8"),
						Latencies: []time.Duration{
							20123 * time.Microsecond,
							19876 * time.Microsecond,
						},
					},
				},
				Success: true,
				Err:     nil,
			},
			wantErr: false,
		},
	}

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

			if tt.wantErr {
				return
			}

			// Special handling for error field since it can't be directly compared with cmp.Diff
			gotErr := got.Err
			wantErr := tt.want.Err
			got.Err = nil
			tt.want.Err = nil

			if diff := cmp.Diff(tt.want, got, IPComparer); diff != "" {
				t.Errorf("ParseTraceroute() mismatch (-want +got):\n%s", diff)
			}

			// Now check error field separately
			if (gotErr == nil) != (wantErr == nil) {
				t.Errorf("Error field: got %v, want %v", gotErr, wantErr)
			} else if gotErr != nil && wantErr != nil && gotErr.Error() != wantErr.Error() {
				t.Errorf("Error message: got %q, want %q", gotErr.Error(), wantErr.Error())
			}
		})
	}
}

func TestEnsureHostname(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		hostinfo   *tailcfg.Hostinfo
		machineKey string
		nodeKey    string
		want       string
	}{
		{
			name: "valid_hostname",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "test-node",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "test-node",
		},
		{
			name:       "nil_hostinfo_with_machine_key",
			hostinfo:   nil,
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "node-mkey1234",
		},
		{
			name:       "nil_hostinfo_with_node_key_only",
			hostinfo:   nil,
			machineKey: "",
			nodeKey:    "nkey12345678",
			want:       "node-nkey1234",
		},
		{
			name:       "nil_hostinfo_no_keys",
			hostinfo:   nil,
			machineKey: "",
			nodeKey:    "",
			want:       "unknown-node",
		},
		{
			name: "empty_hostname_with_machine_key",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "node-mkey1234",
		},
		{
			name: "empty_hostname_with_node_key_only",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "",
			},
			machineKey: "",
			nodeKey:    "nkey12345678",
			want:       "node-nkey1234",
		},
		{
			name: "empty_hostname_no_keys",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "",
			},
			machineKey: "",
			nodeKey:    "",
			want:       "unknown-node",
		},
		{
			name: "hostname_exactly_63_chars",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "123456789012345678901234567890123456789012345678901234567890123",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "123456789012345678901234567890123456789012345678901234567890123",
		},
		{
			name: "hostname_64_chars_truncated",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "1234567890123456789012345678901234567890123456789012345678901234",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "hostname_very_long_truncated",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "test-node-with-very-long-hostname-that-exceeds-dns-label-limits-of-63-characters-and-should-be-truncated",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "hostname_with_special_chars",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "node-with-special!@#$%",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "hostname_with_unicode",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "node-ñoño-测试",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "short_machine_key",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "",
			},
			machineKey: "short",
			nodeKey:    "nkey12345678",
			want:       "node-short",
		},
		{
			name: "short_node_key",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "",
			},
			machineKey: "",
			nodeKey:    "short",
			want:       "node-short",
		},
		{
			name: "hostname_with_emoji_replaced",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "hostname-with-💩",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "hostname_only_emoji_replaced",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "🚀",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "hostname_with_multiple_emojis_replaced",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "node-🎉-🚀-test",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "uppercase_to_lowercase",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "User2-Host",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "user2-host",
		},
		{
			name: "underscore_removed",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "test_node",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "at_sign_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "Test@Host",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "chinese_chars_with_dash_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "server-北京-01",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "chinese_only_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "我的电脑",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "emoji_with_text_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "laptop-🚀",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "mixed_chinese_emoji_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "测试💻机器",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "only_emojis_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "🎉🎊",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "only_at_signs_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "@@@",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "starts_with_dash_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "-test",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "ends_with_dash_invalid",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "test-",
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
		{
			name: "very_long_hostname_truncated",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: strings.Repeat("t", 70),
			},
			machineKey: "mkey12345678",
			nodeKey:    "nkey12345678",
			want:       "invalid-",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got := EnsureHostname(tt.hostinfo, tt.machineKey, tt.nodeKey)
			// For invalid hostnames, we just check the prefix since the random part varies
			if strings.HasPrefix(tt.want, "invalid-") {
				if !strings.HasPrefix(got, "invalid-") {
					t.Errorf("EnsureHostname() = %v, want prefix %v", got, tt.want)
				}
			} else if got != tt.want {
				t.Errorf("EnsureHostname() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestEnsureHostnameWithHostinfo(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		hostinfo      *tailcfg.Hostinfo
		machineKey    string
		nodeKey       string
		wantHostname  string
		checkHostinfo func(*testing.T, *tailcfg.Hostinfo)
	}{
		{
			name: "valid_hostinfo_unchanged",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "test-node",
				OS:       "linux",
			},
			machineKey:   "mkey12345678",
			nodeKey:      "nkey12345678",
			wantHostname: "test-node",
			checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
				if hi == nil {
					t.Error("hostinfo should not be nil")
				}
				if hi.Hostname != "test-node" {
					t.Errorf("hostname = %v, want test-node", hi.Hostname)
				}
				if hi.OS != "linux" {
					t.Errorf("OS = %v, want linux", hi.OS)
				}
			},
		},
		{
			name:         "nil_hostinfo_creates_default",
			hostinfo:     nil,
			machineKey:   "mkey12345678",
			nodeKey:      "nkey12345678",
			wantHostname: "node-mkey1234",
		},
		{
			name: "empty_hostname_updated",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "",
				OS:       "darwin",
			},
			machineKey:   "mkey12345678",
			nodeKey:      "nkey12345678",
			wantHostname: "node-mkey1234",
		},
		{
			name: "long_hostname_rejected",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "test-node-with-very-long-hostname-that-exceeds-dns-label-limits-of-63-characters",
			},
			machineKey:   "mkey12345678",
			nodeKey:      "nkey12345678",
			wantHostname: "invalid-",
		},
		{
			name:         "nil_hostinfo_node_key_only",
			hostinfo:     nil,
			machineKey:   "",
			nodeKey:      "nkey12345678",
			wantHostname: "node-nkey1234",
			checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
				if hi == nil {
					t.Error("hostinfo should not be nil")
				}
				if hi.Hostname != "node-nkey1234" {
					t.Errorf("hostname = %v, want node-nkey1234", hi.Hostname)
				}
			},
		},
		{
			name:         "nil_hostinfo_no_keys",
			hostinfo:     nil,
			machineKey:   "",
			nodeKey:      "",
			wantHostname: "unknown-node",
			checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
				if hi == nil {
					t.Error("hostinfo should not be nil")
				}
				if hi.Hostname != "unknown-node" {
					t.Errorf("hostname = %v, want unknown-node", hi.Hostname)
				}
			},
		},
		{
			name: "empty_hostname_no_keys",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "",
			},
			machineKey:   "",
			nodeKey:      "",
			wantHostname: "unknown-node",
			checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
				if hi == nil {
					t.Error("hostinfo should not be nil")
				}
				if hi.Hostname != "unknown-node" {
					t.Errorf("hostname = %v, want unknown-node", hi.Hostname)
				}
			},
		},
		{
			name: "preserves_other_fields",
			hostinfo: &tailcfg.Hostinfo{
				Hostname:     "test",
				OS:           "windows",
				OSVersion:    "10.0.19044",
				DeviceModel:  "test-device",
				BackendLogID: "log123",
			},
			machineKey:   "mkey12345678",
			nodeKey:      "nkey12345678",
			wantHostname: "test",
			checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
				if hi == nil {
					t.Error("hostinfo should not be nil")
				}
				if hi.Hostname != "test" {
					t.Errorf("hostname = %v, want test", hi.Hostname)
				}
				if hi.OS != "windows" {
					t.Errorf("OS = %v, want windows", hi.OS)
				}
				if hi.OSVersion != "10.0.19044" {
					t.Errorf("OSVersion = %v, want 10.0.19044", hi.OSVersion)
				}
				if hi.DeviceModel != "test-device" {
					t.Errorf("DeviceModel = %v, want test-device", hi.DeviceModel)
				}
				if hi.BackendLogID != "log123" {
					t.Errorf("BackendLogID = %v, want log123", hi.BackendLogID)
				}
			},
		},
		{
			name: "exactly_63_chars_unchanged",
			hostinfo: &tailcfg.Hostinfo{
				Hostname: "123456789012345678901234567890123456789012345678901234567890123",
			},
			machineKey:   "mkey12345678",
			nodeKey:      "nkey12345678",
			wantHostname: "123456789012345678901234567890123456789012345678901234567890123",
			checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
				if hi == nil {
					t.Error("hostinfo should not be nil")
				}
				if len(hi.Hostname) != 63 {
					t.Errorf("hostname length = %v, want 63", len(hi.Hostname))
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			gotHostname := EnsureHostname(tt.hostinfo, tt.machineKey, tt.nodeKey)
			// For invalid hostnames, we just check the prefix since the random part varies
			if strings.HasPrefix(tt.wantHostname, "invalid-") {
				if !strings.HasPrefix(gotHostname, "invalid-") {
					t.Errorf("EnsureHostname() = %v, want prefix %v", gotHostname, tt.wantHostname)
				}
			} else if gotHostname != tt.wantHostname {
				t.Errorf("EnsureHostname() hostname = %v, want %v", gotHostname, tt.wantHostname)
			}
		})
	}
}

func TestEnsureHostname_DNSLabelLimit(t *testing.T) {
	t.Parallel()

	testCases := []string{
		"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
		"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
		"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
		"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
	}

	for i, hostname := range testCases {
		t.Run(cmp.Diff("", ""), func(t *testing.T) {
			hostinfo := &tailcfg.Hostinfo{Hostname: hostname}
			result := EnsureHostname(hostinfo, "mkey", "nkey")
			if len(result) > 63 {
				t.Errorf("test case %d: hostname length = %d, want <= 63", i, len(result))
			}
		})
	}
}

func TestEnsureHostname_Idempotent(t *testing.T) {
	t.Parallel()

	originalHostinfo := &tailcfg.Hostinfo{
		Hostname: "test-node",
		OS:       "linux",
	}

	hostname1 := EnsureHostname(originalHostinfo, "mkey", "nkey")
	hostname2 := EnsureHostname(originalHostinfo, "mkey", "nkey")

	if hostname1 != hostname2 {
		t.Errorf("hostnames not equal: %v != %v", hostname1, hostname2)
	}
}
