// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package model_test

import (
	"encoding/json"
	"reflect"
	"testing"
	"time"

	structpb "google.golang.org/protobuf/types/known/structpb"

	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/serviceregistry/memory"
	"istio.io/istio/pilot/pkg/serviceregistry/mock"
	"istio.io/istio/pkg/config/host"
	"istio.io/istio/pkg/test/util/assert"
	"istio.io/istio/pkg/util/protomarshal"
)

func TestServiceNode(t *testing.T) {
	cases := []struct {
		in  *model.Proxy
		out string
	}{
		{
			in:  &mock.HelloProxyV0,
			out: "sidecar~10.1.1.0~v0.default~default.svc.cluster.local",
		},
		{
			in: &model.Proxy{
				Type:         model.Router,
				ID:           "random",
				IPAddresses:  []string{"10.3.3.3"},
				DNSDomain:    "local",
				Metadata:     &model.NodeMetadata{},
				IstioVersion: model.MaxIstioVersion,
			},
			out: "router~10.3.3.3~random~local",
		},
		{
			in: &model.Proxy{
				Type:        model.SidecarProxy,
				ID:          "random",
				IPAddresses: []string{"10.3.3.3", "10.4.4.4", "10.5.5.5", "10.6.6.6"},
				DNSDomain:   "local",
				Metadata: &model.NodeMetadata{
					InstanceIPs: []string{"10.3.3.3", "10.4.4.4", "10.5.5.5", "10.6.6.6"},
				},
				IstioVersion: model.MaxIstioVersion,
			},
			out: "sidecar~10.3.3.3~random~local",
		},
	}

	for _, node := range cases {
		in, err := model.ParseServiceNodeWithMetadata(node.out, node.in.Metadata)
		if err != nil {
			t.Errorf("ParseServiceNode(%q) => Got error %v", node.out, err)
		}
		if !reflect.DeepEqual(in, node.in) {
			t.Errorf("ParseServiceNode(%q) => Got %#v, want %#v", node.out, in, node.in)
		}
	}
}

func TestParseMetadata(t *testing.T) {
	cases := []struct {
		name     string
		metadata map[string]any
		out      *model.Proxy
	}{
		{
			name: "Basic Case",
			out: &model.Proxy{
				Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
				Metadata: &model.NodeMetadata{Raw: map[string]any{}},
			},
		},
		{
			name:     "Capture Arbitrary Metadata",
			metadata: map[string]any{"foo": "bar"},
			out: &model.Proxy{
				Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
				Metadata: &model.NodeMetadata{
					Raw: map[string]any{
						"foo": "bar",
					},
				},
			},
		},
		{
			name: "Capture Labels",
			metadata: map[string]any{
				"LABELS": map[string]string{
					"foo": "bar",
				},
			},
			out: &model.Proxy{
				Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
				Metadata: &model.NodeMetadata{
					Raw: map[string]any{
						"LABELS": map[string]any{"foo": "bar"},
					},
					Labels: map[string]string{"foo": "bar"},
				},
			},
		},
		{
			name: "Capture Pod Ports",
			metadata: map[string]any{
				"POD_PORTS": `[{"name":"http","containerPort":8080,"protocol":"TCP"},{"name":"grpc","containerPort":8079,"protocol":"TCP"}]`,
			},
			out: &model.Proxy{
				Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
				Metadata: &model.NodeMetadata{
					Raw: map[string]any{
						"POD_PORTS": `[{"name":"http","containerPort":8080,"protocol":"TCP"},{"name":"grpc","containerPort":8079,"protocol":"TCP"}]`,
					},
					PodPorts: []model.PodPort{
						{Name: "http", ContainerPort: 8080, Protocol: "TCP"},
						{Name: "grpc", ContainerPort: 8079, Protocol: "TCP"},
					},
				},
			},
		},
	}

	nodeID := "sidecar~1.1.1.1~id~domain"

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			meta, err := mapToStruct(tt.metadata)
			if err != nil {
				t.Fatalf("failed to setup metadata: %v", err)
			}
			parsed, err := model.ParseMetadata(meta)
			if err != nil {
				t.Fatalf("failed to parse service node: %v", err)
			}
			node, err := model.ParseServiceNodeWithMetadata(nodeID, parsed)
			if err != nil {
				t.Fatalf("failed to parse service node: %v", err)
			}
			if !reflect.DeepEqual(*tt.out.Metadata, *node.Metadata) {
				t.Errorf("Got \n%v, want \n%v", *node.Metadata, *tt.out.Metadata)
			}
		})
	}
}

func mapToStruct(msg map[string]any) (*structpb.Struct, error) {
	if msg == nil {
		return &structpb.Struct{}, nil
	}
	b, err := json.Marshal(msg)
	if err != nil {
		return nil, err
	}

	pbs := &structpb.Struct{}
	if err := protomarshal.Unmarshal(b, pbs); err != nil {
		return nil, err
	}

	return pbs, nil
}

func TestGetOrDefault(t *testing.T) {
	assert.Equal(t, "a", model.GetOrDefault("a", "b"))
	assert.Equal(t, "b", model.GetOrDefault("", "b"))
}

func TestProxyVersion_Compare(t *testing.T) {
	type fields struct {
		Major int
		Minor int
		Patch int
	}
	type args struct {
		inv *model.IstioVersion
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		want   int
	}{
		{
			name:   "greater major",
			fields: fields{Major: 2, Minor: 1, Patch: 1},
			args:   args{&model.IstioVersion{Major: 1, Minor: 2, Patch: 1}},
			want:   1,
		},
		{
			name:   "equal at minor",
			fields: fields{Major: 2, Minor: 1, Patch: 1},
			args:   args{&model.IstioVersion{Major: 2, Minor: 1, Patch: -1}},
			want:   0,
		},
		{
			name:   "less at patch",
			fields: fields{Major: 2, Minor: 1, Patch: 0},
			args:   args{&model.IstioVersion{Major: 2, Minor: 1, Patch: 1}},
			want:   -1,
		},
		{
			name:   "ignore minor",
			fields: fields{Major: 2, Minor: 1, Patch: 11},
			args:   args{&model.IstioVersion{Major: 2, Minor: -1, Patch: 1}},
			want:   0,
		},
		{
			name:   "ignore patch",
			fields: fields{Major: 2, Minor: 1, Patch: 11},
			args:   args{&model.IstioVersion{Major: 2, Minor: 1, Patch: -1}},
			want:   0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pversion := &model.IstioVersion{
				Major: tt.fields.Major,
				Minor: tt.fields.Minor,
				Patch: tt.fields.Patch,
			}
			if got := pversion.Compare(tt.args.inv); got != tt.want {
				t.Errorf("ProxyVersion.Compare() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_parseIstioVersion(t *testing.T) {
	type args struct {
		ver string
	}
	tests := []struct {
		name string
		args args
		want *model.IstioVersion
	}{
		{
			name: "major.minor.patch",
			args: args{ver: "1.2.3"},
			want: &model.IstioVersion{Major: 1, Minor: 2, Patch: 3},
		},
		{
			name: "major.minor",
			args: args{ver: "1.2"},
			want: &model.IstioVersion{Major: 1, Minor: 2, Patch: 65535},
		},
		{
			name: "dev",
			args: args{ver: "1.5-alpha.f70faea2aa817eeec0b08f6cc3b5078e5dcf3beb"},
			want: &model.IstioVersion{Major: 1, Minor: 5, Patch: 65535},
		},
		{
			name: "release-major.minor-date",
			args: args{ver: "release-1.2-123214234"},
			want: &model.IstioVersion{Major: 1, Minor: 2, Patch: 65535},
		},
		{
			name: "master-date",
			args: args{ver: "master-123214234"},
			want: model.MaxIstioVersion,
		},
		{
			name: "master-sha",
			args: args{ver: "master-0b94e017f5b6c7c4598a4da42ea9d45eeb099e5f"},
			want: model.MaxIstioVersion,
		},
		{
			name: "junk-major.minor.patch",
			args: args{ver: "junk-1.2.3214234"},
			want: model.MaxIstioVersion,
		},
		{
			name: "junk-garbage",
			args: args{ver: "junk-garbage"},
			want: model.MaxIstioVersion,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := model.ParseIstioVersion(tt.args.ver); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("parseIstioVersion() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestSetServiceInstances(t *testing.T) {
	tnow := time.Now()
	instances := []model.ServiceTarget{
		{
			Service: &model.Service{
				CreationTime: tnow.Add(1 * time.Second),
				Hostname:     host.Name("test1.com"),
			},
		},
		{
			Service: &model.Service{
				CreationTime: tnow,
				Hostname:     host.Name("test3.com"),
			},
		},
		{
			Service: &model.Service{
				CreationTime: tnow,
				Hostname:     host.Name("test2.com"),
			},
		},
	}

	serviceDiscovery := memory.NewServiceDiscovery()
	serviceDiscovery.WantGetProxyServiceTargets = instances

	env := &model.Environment{
		ServiceDiscovery: serviceDiscovery,
	}

	proxy := &model.Proxy{}
	proxy.SetServiceTargets(env)

	assert.Equal(t, len(proxy.ServiceTargets), 3)
	assert.Equal(t, proxy.ServiceTargets[0].Service.Hostname, "test2.com")
	assert.Equal(t, proxy.ServiceTargets[1].Service.Hostname, "test3.com")
	assert.Equal(t, proxy.ServiceTargets[2].Service.Hostname, "test1.com")
}

func TestGlobalUnicastIP(t *testing.T) {
	cases := []struct {
		name   string
		in     []string
		expect string
	}{
		{
			name:   "single IPv4 (k8s)",
			in:     []string{"10.0.4.16"},
			expect: "10.0.4.16",
		},
		{
			name:   "single IPv6 (k8s)",
			in:     []string{"fc00:f853:ccd:e793::1"},
			expect: "fc00:f853:ccd:e793::1",
		},
		{
			name:   "multi IPv4 [1st] (VM)",
			in:     []string{"10.128.0.51", "fc00:f853:ccd:e793::1", "172.17.0.1", "fe80::42:35ff:fec1:7436", "fe80::345d:33ff:fe54:5c8e"},
			expect: "10.128.0.51",
		},
		{
			name:   "multi IPv6 [1st] (VM)",
			in:     []string{"fc00:f853:ccd:e793::1", "172.17.0.1", "fe80::42:35ff:fec1:7436", "10.128.0.51", "fe80::345d:33ff:fe54:5c8e"},
			expect: "fc00:f853:ccd:e793::1",
		},
		{
			name:   "multi IPv4 [2nd] (VM)",
			in:     []string{"127.0.0.1", "10.128.0.51", "fc00:f853:ccd:e793::1", "172.17.0.1", "fe80::42:35ff:fec1:7436", "fe80::345d:33ff:fe54:5c8e"},
			expect: "10.128.0.51",
		},
		{
			name:   "multi IPv6 [2nd] (VM)",
			in:     []string{"fe80::42:35ff:fec1:7436", "fc00:f853:ccd:e793::1", "172.17.0.1", "10.128.0.51", "fe80::345d:33ff:fe54:5c8e"},
			expect: "fc00:f853:ccd:e793::1",
		},
	}
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			var node model.Proxy
			node.IPAddresses = tt.in
			node.DiscoverIPMode()
			if got := node.GlobalUnicastIP; got != tt.expect {
				t.Errorf("GlobalUnicastIP = %v, want %v", got, tt.expect)
			}
		})
	}
}
