package volume

import (
	"reflect"
	"testing"
	"time"

	"k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var nodeItems = []v1.Node{
	{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "minikube",
			SelfLink:          "/api/v1/nodes/minikube",
			CreationTimestamp: metav1.NewTime(time.Now()),
			Labels: map[string]string{
				"beta.kubernetes.io/arch": "amd64",
				"beta.kubernetes.io/os":   "linux",
				"kubernetes.io/hostname":  "minikube",
				// Label node select is overridden by node name select.
				"beta.kubernetes.io/disk-type": "ssd",
			},
		},
	},
	{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "minikubeSSD",
			SelfLink:          "/api/v1/nodes/minikubeSSD",
			CreationTimestamp: metav1.NewTime(time.Now()),
			Labels: map[string]string{
				"beta.kubernetes.io/arch":      "amd64",
				"beta.kubernetes.io/os":        "linux",
				"kubernetes.io/hostname":       "minikube2",
				"beta.kubernetes.io/disk-type": "ssd",
			},
		},
	},
	{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "foobar",
			SelfLink:          "/api/v1/nodes/foobar",
			CreationTimestamp: metav1.NewTime(time.Now()),
			Labels: map[string]string{
				"beta.kubernetes.io/arch": "alpha",
				"beta.kubernetes.io/os":   "dec",
				"kubernetes.io/hostname":  "foobar",
			},
		},
	},
	{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "default",
			SelfLink:          "/api/v1/nodes/foobar2",
			CreationTimestamp: metav1.NewTime(time.Now()),
			Labels: map[string]string{
				"beta.kubernetes.io/arch": "alpha",
				"beta.kubernetes.io/os":   "dec",
				"kubernetes.io/hostname":  "foobar2",
			},
		},
	},
}

func TestVolumes(t *testing.T) {
	vp := NewVolumePool(mockConfig{
		"volume": `nodes:
- name: gke-staging-default-pool-f3ab8378-65g9
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi
- name: gke-staging-default-pool-f3ab8378-cqtd
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi`,
	})

	vol, err := vp.Volumes()
	if err != nil {
		t.Fatal(err)
	}
	if len(vol.Nodes) != 2 {
		t.Log(vol.Nodes)
		t.Error("invalid nodes")
	}
	// We should not flatten yet.
	if len(vol.Volumes) != 0 {
		t.Log(vol.Volumes)
		t.Error("invalid volumes")
	}
}

func TestVolumesFromNodeSelector(t *testing.T) {
	vp := NewVolumePool(mockConfig{
		"volume": `nodes:
- name: default
  nodeSelector:
    beta.kubernetes.io/disk-type: ssd
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi
- name: minikube
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi
- name: nomatch
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi`,
	})
	tv, err := vp.Volumes()
	if err != nil {
		t.Fatal(err)
	}
	tv, err = tv.From(nodeItems)
	if err != nil {
		t.Fatal(err)
	}
	if n := len(tv.Volumes); n != 4 {
		t.Error("invalid number of volumes", n)
	}

	size100, _ := resource.ParseQuantity("100Gi")
	size2, _ := resource.ParseQuantity("2Gi")
	expected := []Volume{
		{
			NodeName: "minikube",
			Path:     "/tidb/tikv-dir-1",
			Size:     size100,
			Priority: 0,
		},
		{
			NodeName: "minikube",
			Path:     "/tidb/pd-dir-1",
			Size:     size2,
			Priority: 0,
		},
		{
			NodeName: "minikubeSSD",
			Path:     "/tidb/tikv-dir-1",
			Size:     size100,
			Priority: 0,
		},
		{
			NodeName: "minikubeSSD",
			Path:     "/tidb/pd-dir-1",
			Size:     size2,
			Priority: 0,
		},
	}
	if !reflect.DeepEqual(tv.Volumes, expected) {
		t.Log(tv.Volumes)
		t.Error("unexpected volumes")
	}
}

func TestVolumesFromConflict(t *testing.T) {
	testcases := map[string]struct {
		config   mockConfig
		expected error
	}{
		"normal": {
			mockConfig{
				"volume": `nodes:
- name: default
  nodeSelector:
    beta.kubernetes.io/disk-type: ssd
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi
- name: minikube
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi`,
			},
			nil,
		},
		"node select conflict": {
			mockConfig{
				"volume": `nodes:
- name: default
  nodeSelector:
    beta.kubernetes.io/disk-type: ssd
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi
- name: linux
  nodeSelector:
    beta.kubernetes.io/os: linux
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi`,
			},
			ErrVolumeConflict,
		},
		"node name conflict": {
			mockConfig{
				"volume": `nodes:
- name: foobar
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi
- name: foobar
  directories:
  - path: /tidb/tikv-dir-1
    size: 100Gi
  - path: /tidb/pd-dir-1
    size: 2Gi`,
			},
			ErrVolumeConflict,
		},
	}

	for desc, tc := range testcases {
		t.Run(desc, func(t *testing.T) {
			vp := NewVolumePool(tc.config)
			tv, err := vp.Volumes()
			if err != nil {
				t.Fatal(err)
			}
			if _, err := tv.From(nodeItems); err != tc.expected {
				t.Log(err)
				t.Errorf("expected error value: %v", tc.expected)
			}
		})
	}
}

type mockConfig map[string]string

func (mockConfig) Name() string { return "mock" }

func (cfg mockConfig) Config() (map[string]string, error) {
	return cfg, nil
}
