package scanner

import (
	"sort"
	"testing"

	"github.com/package-url/packageurl-go"
	"github.com/samber/lo"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
	cmd "github.com/aquasecurity/trivy/pkg/commands/artifact"
	ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/flag"
	"github.com/aquasecurity/trivy/pkg/purl"
	"github.com/aquasecurity/trivy/pkg/sbom/core"
	"github.com/aquasecurity/trivy/pkg/uuid"
)

func TestScanner_Scan(t *testing.T) {
	flagOpts := flag.Options{ReportOptions: flag.ReportOptions{Format: "cyclonedx"}}
	tests := []struct {
		name           string
		clusterName    string
		artifacts      []*artifacts.Artifact
		wantComponents []*core.Component
	}{
		{
			name:        "test cluster info with resources",
			clusterName: "test-cluster",
			artifacts: []*artifacts.Artifact{
				{
					Namespace: "kube-system",
					Kind:      "Cluster",
					Name:      "k8s.io/kubernetes",
					RawResource: map[string]any{
						"name":    "k8s.io/kubernetes",
						"version": "1.21.1",
						"type":    "ClusterInfo",
						"Properties": map[string]string{
							"Name": "kind-kind",
							"Type": "cluster",
						},
					},
				},
				{
					Namespace: "kube-system",
					Kind:      "ControlPlaneComponents",
					Name:      "k8s.io/apiserver",
					RawResource: map[string]any{
						"Containers": []any{
							map[string]any{
								"Digest":     "18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f",
								"ID":         "kube-apiserver:v1.21.1",
								"Registry":   "k8s.gcr.io",
								"Repository": "kube-apiserver",
								"Version":    "v1.21.1",
							},
						},
						"Name":    "k8s.io/apiserver",
						"Version": "1.21.1",
					},
				},
				{
					Kind: "NodeComponents",
					Name: "kind-control-plane",
					RawResource: map[string]any{
						"ContainerRuntimeVersion": "containerd://1.5.2",
						"Hostname":                "kind-control-plane",
						"KubeProxyVersion":        "6.2.13-300.fc38.aarch64",
						"KubeletVersion":          "v1.21.1",
						"NodeName":                "kind-control-plane",
						"OsImage":                 "Ubuntu 21.04",
						"Properties": map[string]string{
							"Architecture":    "arm64",
							"HostName":        "kind-control-plane",
							"KernelVersion":   "6.2.15-300.fc38.aarch64",
							"NodeRole":        "master",
							"OperatingSystem": "linux",
						},
					},
				},
			},
			wantComponents: []*core.Component{
				{
					Type:    core.TypeApplication,
					Name:    "github.com/containerd/containerd",
					Version: "v1.5.2",
					Properties: []core.Property{
						{
							Name:      k8sComponentName,
							Value:     "github.com/containerd/containerd",
							Namespace: k8sCoreComponentNamespace,
						},
						{
							Name:      k8sComponentType,
							Value:     "node",
							Namespace: k8sCoreComponentNamespace,
						},
					},
					PkgIdentifier: ftypes.PkgIdentifier{
						PURL: &packageurl.PackageURL{
							Type:       "golang",
							Name:       "github.com/containerd/containerd",
							Version:    "v1.5.2",
							Qualifiers: packageurl.Qualifiers{},
						},
						BOMRef: "pkg:golang/github.com%2Fcontainerd%2Fcontainerd@v1.5.2",
					},
				},
				{
					Type:    core.TypeApplication,
					Name:    "k8s.io/apiserver",
					Version: "v1.21.1",
					PkgIdentifier: ftypes.PkgIdentifier{
						PURL: &packageurl.PackageURL{
							Type:    purl.TypeK8s,
							Name:    "k8s.io/apiserver",
							Version: "v1.21.1",
						},
						BOMRef: "pkg:k8s/k8s.io%2Fapiserver@v1.21.1",
					},
				},
				{
					Type:    core.TypeApplication,
					Name:    "k8s.io/kubelet",
					Version: "v1.21.1",
					Properties: []core.Property{
						{
							Name:      k8sComponentName,
							Value:     "k8s.io/kubelet",
							Namespace: k8sCoreComponentNamespace,
						},
						{
							Name:      k8sComponentType,
							Value:     "node",
							Namespace: k8sCoreComponentNamespace,
						},
					},
					PkgIdentifier: ftypes.PkgIdentifier{
						PURL: &packageurl.PackageURL{
							Type:    "k8s",
							Name:    "k8s.io/kubelet",
							Version: "v1.21.1",
						},
						BOMRef: "pkg:k8s/k8s.io%2Fkubelet@v1.21.1",
					},
				},
				{
					Type: core.TypeApplication,
					Name: "node-core-components",
					PkgIdentifier: ftypes.PkgIdentifier{
						BOMRef: "3ff14136-e09f-4df9-80ea-000000000006",
					},
				},
				{
					Type:    core.TypeContainerImage,
					Name:    "k8s.gcr.io/kube-apiserver",
					Version: "sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f",
					PkgIdentifier: ftypes.PkgIdentifier{
						PURL: &packageurl.PackageURL{
							Type:    "oci",
							Name:    "kube-apiserver",
							Version: "sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f",
							Qualifiers: packageurl.Qualifiers{
								{
									Key:   "repository_url",
									Value: "k8s.gcr.io/kube-apiserver",
								},
							},
						},
						BOMRef: "pkg:oci/kube-apiserver@sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f?repository_url=k8s.gcr.io%2Fkube-apiserver",
					},
					Properties: []core.Property{
						{
							Name:  core.PropertyPkgID,
							Value: "k8s.gcr.io/kube-apiserver:v1.21.1",
						},
						{
							Name:  core.PropertyPkgType,
							Value: "oci",
						},
					},
				},
				{
					Type:    core.TypeOS,
					Name:    "ubuntu",
					Version: "21.04",
					Properties: []core.Property{
						{
							Name:      "Class",
							Value:     "os-pkgs",
							Namespace: "",
						},
						{
							Name:      "Type",
							Value:     "ubuntu",
							Namespace: "",
						},
					},
					PkgIdentifier: ftypes.PkgIdentifier{
						BOMRef: "3ff14136-e09f-4df9-80ea-000000000005",
					},
				},
				{
					Type:    core.TypePlatform,
					Root:    true,
					Name:    "k8s.io/kubernetes",
					Version: "v1.21.1",
					Properties: []core.Property{
						{
							Name:      "Name",
							Value:     "kind-kind",
							Namespace: k8sCoreComponentNamespace,
						},
						{
							Name:      "Type",
							Value:     "cluster",
							Namespace: k8sCoreComponentNamespace,
						},
					},
					PkgIdentifier: ftypes.PkgIdentifier{
						PURL: &packageurl.PackageURL{
							Type:    purl.TypeK8s,
							Name:    "k8s.io/kubernetes",
							Version: "v1.21.1",
						},
						BOMRef: "pkg:k8s/k8s.io%2Fkubernetes@v1.21.1",
					},
				},
				{
					Type: core.TypePlatform,
					Name: "kind-control-plane",
					Properties: []core.Property{
						{
							Name:  "Architecture",
							Value: "arm64",
						},
						{
							Name:  "HostName",
							Value: "kind-control-plane",
						},
						{
							Name:  "KernelVersion",
							Value: "6.2.15-300.fc38.aarch64",
						},
						{
							Name:      k8sComponentName,
							Value:     "kind-control-plane",
							Namespace: k8sCoreComponentNamespace,
						},
						{
							Name:  "NodeRole",
							Value: "master",
						},
						{
							Name:  "OperatingSystem",
							Value: "linux",
						},
						{
							Name:      k8sComponentType,
							Value:     "node",
							Namespace: k8sCoreComponentNamespace,
						},
					},
					PkgIdentifier: ftypes.PkgIdentifier{
						BOMRef: "3ff14136-e09f-4df9-80ea-000000000004",
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := t.Context()
			uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d")

			runner, err := cmd.NewRunner(ctx, flagOpts, cmd.TargetK8s)
			require.NoError(t, err)

			scanner := NewScanner(tt.clusterName, runner, flagOpts)
			got, err := scanner.Scan(ctx, tt.artifacts)
			require.NoError(t, err)

			gotComponents := lo.Values(got.BOM.Components())
			require.Len(t, gotComponents, len(tt.wantComponents))

			sort.Slice(gotComponents, func(i, j int) bool {
				switch {
				case gotComponents[i].Type != gotComponents[j].Type:
					return gotComponents[i].Type < gotComponents[j].Type
				case gotComponents[i].Name != gotComponents[j].Name:
					return gotComponents[i].Name < gotComponents[j].Name
				default:
					return gotComponents[i].Version < gotComponents[j].Version
				}
			})
			for i, want := range tt.wantComponents {
				assert.EqualExportedValues(t, *want, *gotComponents[i], want.Name)
			}
		})
	}
}

func TestTestOsNameVersion(t *testing.T) {
	tests := []struct {
		name        string
		nameVersion string
		compName    string
		compVersion string
	}{

		{
			name:        "valid version",
			nameVersion: "ubuntu 20.04",
			compName:    "ubuntu",
			compVersion: "20.04",
		},
		{
			name:        "valid sem version",
			nameVersion: "ubuntu 20.04.1",
			compName:    "ubuntu",
			compVersion: "20.04.1",
		},
		{
			name:        "non valid version",
			nameVersion: "ubuntu",
			compName:    "ubuntu",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			name, version := osNameVersion(tt.nameVersion)
			assert.Equal(t, name, tt.compName)
			assert.Equal(t, version, tt.compVersion)
		})
	}
}

func TestGeneratePURL(t *testing.T) {
	tests := []struct {
		name        string
		compName    string
		compVersion string
		nodeName    string
		want        string
	}{
		{
			name:        "native k8s component",
			compName:    "k8s.io/kubelet",
			compVersion: "1.24.10",
			nodeName:    "kind-kind",
			want:        "pkg:k8s/k8s.io%2Fkubelet@1.24.10",
		},

		{
			name:        "GKE",
			compName:    "k8s.io/kubelet",
			compVersion: "1.24.10-gke.2300",
			nodeName:    "gke-gke1796-default-pool-768cb718-sk1d",
			want:        "pkg:k8s/gke/k8s.io%2Fkubelet@1.24.10-gke.2300",
		},
		{
			name:        "AKS",
			compName:    "k8s.io/kubelet",
			compVersion: "1.24.10-hotfix.20221110",
			nodeName:    "aks-default-23814474-vmss000000",
			want:        "pkg:k8s/aks/k8s.io%2Fkubelet@1.24.10-hotfix.20221110",
		},
		{
			name:        "EKS",
			compName:    "k8s.io/kubelet",
			compVersion: "1.23.17-eks-8ccc7ba",
			nodeName:    "eks-vmss000000",
			want:        "pkg:k8s/eks/k8s.io%2Fkubelet@1.23.17-eks-8ccc7ba",
		},
		{
			name:        "Rancher",
			compName:    "k8s.io/kubelet",
			compVersion: "1.24.11+rke2r1",
			nodeName:    "ip-10-0-5-23",
			want:        "pkg:k8s/k8s.io%2Fkubelet@1.24.11%2Brke2r1",
		},
		{
			name:        "OCP",
			compName:    "k8s.io/kubelet",
			compVersion: "1.26.7+c7ee51f",
			nodeName:    "ocp413vpool14000-p8vnm-master-2",
			want:        "pkg:k8s/ocp/k8s.io%2Fkubelet@1.26.7%2Bc7ee51f",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := generatePURL(tt.compName, tt.compVersion, tt.nodeName)
			assert.Equal(t, tt.want, got.String())
		})
	}
}

func TestK8sNamespace(t *testing.T) {
	tests := []struct {
		name        string
		compVersion string
		nodeName    string
		want        string
	}{
		{
			name:        "native k8s component",
			compVersion: "1.24.10",
			nodeName:    "kind-kind",
			want:        "kubernetes",
		},

		{
			name:        "GKE",
			compVersion: "1.24.10-gke.2300",
			nodeName:    "gke-gke1796-default-pool-768cb718-sk1d",
			want:        "gke",
		},
		{
			name:        "AKS",
			compVersion: "1.24.10-hotfix.20221110",
			nodeName:    "aks-default-23814474-vmss000000",
			want:        "aks",
		},
		{
			name:        "EKS",
			compVersion: "1.23.17-eks-8ccc7ba",
			nodeName:    "eks-vmss000000",
			want:        "eks",
		},
		{
			name:        "Rancher",
			compVersion: "1.24.11+rke2r1",
			nodeName:    "ip-10-0-5-23",
			want:        "kubernetes",
		},
		{
			name:        "OCP",
			compVersion: "1.26.7+c7ee51f",
			nodeName:    "ocp413vpool14000-p8vnm-master-2",
			want:        "ocp",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := k8sNamespace(tt.compVersion, tt.nodeName)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestRuntimeVersion(t *testing.T) {
	tests := []struct {
		name           string
		runtimeVersion string
		wantName       string
		wantVersion    string
	}{
		{
			name:           "containerd",
			runtimeVersion: "containerd://1.5.2",
			wantName:       "github.com/containerd/containerd",
			wantVersion:    "v1.5.2",
		},
		{
			name:           "cri-o",
			runtimeVersion: "cri-o://1.5.2",
			wantName:       "github.com/cri-o/cri-o",
			wantVersion:    "v1.5.2",
		},
		{
			name:           "cri-dockerd",
			runtimeVersion: "cri-dockerd://1.5.2",
			wantName:       "github.com/Mirantis/cri-dockerd",
			wantVersion:    "v1.5.2",
		},
		{
			name:           "na runtime",
			runtimeVersion: "cri:1.5.2",
			wantName:       "",
			wantVersion:    "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotName, gotVersion := runtimeNameVersion(tt.runtimeVersion)
			assert.Equal(t, tt.wantName, gotName)
			assert.Equal(t, tt.wantVersion, gotVersion)
		})
	}
}

func TestFindNodeName(t *testing.T) {
	tests := []struct {
		name      string
		artifacts []*artifacts.Artifact
		want      string
	}{
		{
			name: "find node name",
			artifacts: []*artifacts.Artifact{
				{
					Namespace:   "kube-system",
					Kind:        "Cluster",
					Name:        "k8s.io/kubernetes",
					RawResource: make(map[string]any),
				},
				{
					Namespace:   "kube-system",
					Kind:        "ControlPlaneComponents",
					Name:        "k8s.io/apiserver",
					RawResource: make(map[string]any),
				},
				{
					Kind:        "NodeComponents",
					Name:        "kind-control-plane",
					RawResource: make(map[string]any),
				},
			},
			want: "kind-control-plane",
		},
		{
			name: "didn't find node name",
			artifacts: []*artifacts.Artifact{
				{
					Namespace:   "kube-system",
					Kind:        "Cluster",
					Name:        "k8s.io/kubernetes",
					RawResource: make(map[string]any),
				},
				{
					Namespace:   "kube-system",
					Kind:        "ControlPlaneComponents",
					Name:        "k8s.io/apiserver",
					RawResource: make(map[string]any),
				},
			},
			want: "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := Scanner{}
			got := s.findNodeName(tt.artifacts)
			assert.Equal(t, tt.want, got)
		})
	}
}
