/*
Copyright 2019 The Kubernetes 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 main

import (
	"io/ioutil"
	"path"
	"testing"

	"github.com/blang/semver/v4"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/tests/integration/channel/simple"
	"k8s.io/kops/util/pkg/architectures"
)

// TestKopsUpgrades tests the version logic for kops versions
func TestKopsUpgrades(t *testing.T) {
	sourcePath := "simple/channel.yaml"

	channel, err := simple.NewMockChannel(sourcePath)
	if err != nil {
		t.Fatalf("failed to create channel: %v", err)
	}

	grid := []struct {
		KopsVersion      string
		ExpectedUpgrade  string
		ExpectedRequired bool
		ExpectedError    bool
	}{
		{
			KopsVersion:      "1.4.4",
			ExpectedUpgrade:  "1.4.5",
			ExpectedRequired: true,
		},
		{
			KopsVersion:     "1.4.5",
			ExpectedUpgrade: "",
		},
		{
			KopsVersion:      "1.5.0-alpha4",
			ExpectedUpgrade:  "1.5.0-beta1",
			ExpectedRequired: true,
		},
		{
			KopsVersion:     "1.5.0-beta1",
			ExpectedUpgrade: "",
		},
		{
			KopsVersion:     "1.5.0-beta2",
			ExpectedUpgrade: "",
		},
		{
			KopsVersion:      "1.5.0",
			ExpectedUpgrade:  "1.5.1",
			ExpectedRequired: false,
		},
		{
			KopsVersion:     "1.5.1",
			ExpectedUpgrade: "",
		},
	}
	for _, g := range grid {
		kopsVersion := semver.MustParse(g.KopsVersion)

		versionInfo := kops.FindKopsVersionSpec(channel.Spec.KopsVersions, kopsVersion)
		if versionInfo == nil {
			t.Errorf("unable to find version information for kops version %q in channel", kopsVersion)
			continue
		}

		actual, err := versionInfo.FindRecommendedUpgrade(kopsVersion)
		if g.ExpectedError {
			if err == nil {
				t.Errorf("expected error from FindRecommendedUpgrade(%q)", g.KopsVersion)
				continue
			}
		} else {
			if err != nil {
				t.Errorf("unexpected error from FindRecommendedUpgrade(%q): %v", g.KopsVersion, err)
				continue
			}
		}
		if semverString(actual) != g.ExpectedUpgrade {
			t.Errorf("unexpected result from IsUpgradeRequired(%q): expected=%q, actual=%q", g.KopsVersion, g.ExpectedUpgrade, actual)
			continue
		}

		required, err := versionInfo.IsUpgradeRequired(kopsVersion)
		if err != nil {
			t.Errorf("unexpected error from IsUpgradeRequired(%q): %v", g.KopsVersion, err)
			continue
		}
		if required != g.ExpectedRequired {
			t.Errorf("unexpected result from IsUpgradeRequired(%q): expected=%t, actual=%t", g.KopsVersion, g.ExpectedRequired, required)
			continue
		}
	}
}

// TestKubernetesUpgrades tests the version logic kubernetes kops versions
func TestKubernetesUpgrades(t *testing.T) {
	srcDir := "simple"
	sourcePath := path.Join(srcDir, "channel.yaml")
	sourceBytes, err := ioutil.ReadFile(sourcePath)
	if err != nil {
		t.Fatalf("unexpected error reading sourcePath %q: %v", sourcePath, err)
	}

	channel, err := kops.ParseChannel(sourceBytes)
	if err != nil {
		t.Fatalf("failed to parse channel: %v", err)
	}

	grid := []struct {
		KubernetesVersion string
		ExpectedUpgrade   string
		ExpectedRequired  bool
		ExpectedError     bool
	}{
		{
			KubernetesVersion: "1.4.0",
			ExpectedUpgrade:   "1.4.8",
			ExpectedRequired:  true,
		},
		{
			KubernetesVersion: "1.4.1",
			ExpectedUpgrade:   "1.4.8",
			ExpectedRequired:  true,
		},
		{
			KubernetesVersion: "1.4.2",
			ExpectedUpgrade:   "1.4.8",
		},
		{
			KubernetesVersion: "1.4.4",
			ExpectedUpgrade:   "1.4.8",
		},
		{
			KubernetesVersion: "1.4.8",
			ExpectedUpgrade:   "",
		},
		{
			KubernetesVersion: "1.5.0",
			ExpectedUpgrade:   "1.5.2",
			ExpectedRequired:  true,
		},
		{
			KubernetesVersion: "1.5.1",
			ExpectedUpgrade:   "1.5.2",
		},
		{
			KubernetesVersion: "1.5.2",
			ExpectedUpgrade:   "",
		},
	}
	for _, g := range grid {
		kubernetesVersion := semver.MustParse(g.KubernetesVersion)

		versionInfo := kops.FindKubernetesVersionSpec(channel.Spec.KubernetesVersions, kubernetesVersion)
		if versionInfo == nil {
			t.Errorf("unable to find version information for kubernetes version %q in channel", kubernetesVersion)
			continue
		}

		actual, err := versionInfo.FindRecommendedUpgrade(kubernetesVersion)
		if g.ExpectedError {
			if err == nil {
				t.Errorf("expected error from FindRecommendedUpgrade(%q)", g.KubernetesVersion)
				continue
			}
		} else {
			if err != nil {
				t.Errorf("unexpected error from FindRecommendedUpgrade(%q): %v", g.KubernetesVersion, err)
				continue
			}
		}
		if semverString(actual) != g.ExpectedUpgrade {
			t.Errorf("unexpected result from IsUpgradeRequired(%q): expected=%q, actual=%q", g.KubernetesVersion, g.ExpectedUpgrade, actual)
			continue
		}

		required, err := versionInfo.IsUpgradeRequired(kubernetesVersion)
		if err != nil {
			t.Errorf("unexpected error from IsUpgradeRequired(%q): %v", g.KubernetesVersion, err)
			continue
		}
		if required != g.ExpectedRequired {
			t.Errorf("unexpected result from IsUpgradeRequired(%q): expected=%t, actual=%t", g.KubernetesVersion, g.ExpectedRequired, required)
			continue
		}
	}
}

// TestFindImage tests the version-based image finding
func TestFindImage(t *testing.T) {
	srcDir := "simple"
	sourcePath := path.Join(srcDir, "channel.yaml")
	sourceBytes, err := ioutil.ReadFile(sourcePath)
	if err != nil {
		t.Fatalf("unexpected error reading sourcePath %q: %v", sourcePath, err)
	}

	channel, err := kops.ParseChannel(sourceBytes)
	if err != nil {
		t.Fatalf("failed to parse channel: %v", err)
	}

	grid := []struct {
		KubernetesVersion string
		Architecture      string
		ExpectedImage     string
	}{
		{
			KubernetesVersion: "1.1.0",
			Architecture:      "amd64",
			ExpectedImage:     "kope.io/k8s-1.11-debian-stretch-amd64-hvm-ebs-2021-02-05",
		},
		{
			KubernetesVersion: "1.17.2",
			Architecture:      "amd64",
			ExpectedImage:     "kope.io/k8s-1.17-debian-stretch-amd64-hvm-ebs-2021-02-05",
		},
		{
			KubernetesVersion: "1.19.1",
			Architecture:      "amd64",
			ExpectedImage:     "099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20210119.1",
		},
		{
			KubernetesVersion: "1.19.1",
			Architecture:      "arm64",
			ExpectedImage:     "099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-arm64-server-20210119.1",
		},
	}
	for _, g := range grid {
		kubernetesVersion := semver.MustParse(g.KubernetesVersion)
		architecture := architectures.Architecture(g.Architecture)

		image := channel.FindImage(kops.CloudProviderAWS, kubernetesVersion, architecture)
		name := ""
		if image != nil {
			name = image.Name
		}
		if name != g.ExpectedImage {
			t.Errorf("unexpected image from FindImage(%q): expected=%q, actual=%q", g.KubernetesVersion, g.ExpectedImage, name)
		}
	}
}

// TestRecommendedKubernetesVersion tests the version logic kubernetes kops versions
func TestRecommendedKubernetesVersion(t *testing.T) {
	srcDir := "simple"
	sourcePath := path.Join(srcDir, "channel.yaml")
	sourceBytes, err := ioutil.ReadFile(sourcePath)
	if err != nil {
		t.Fatalf("unexpected error reading sourcePath %q: %v", sourcePath, err)
	}

	channel, err := kops.ParseChannel(sourceBytes)
	if err != nil {
		t.Fatalf("failed to parse channel: %v", err)
	}

	grid := []struct {
		KopsVersion               string
		ExpectedKubernetesVersion string
	}{
		{
			KopsVersion:               "1.4.4",
			ExpectedKubernetesVersion: "1.4.8",
		},
		{
			KopsVersion:               "1.4.5",
			ExpectedKubernetesVersion: "1.4.8",
		},
		{
			KopsVersion:               "1.5.0",
			ExpectedKubernetesVersion: "1.5.2",
		},
		{
			KopsVersion:               "1.5.0-beta2",
			ExpectedKubernetesVersion: "1.5.0",
		},
	}
	for _, g := range grid {
		kubernetesVersion := kops.RecommendedKubernetesVersion(channel, g.KopsVersion)
		if semverString(kubernetesVersion) != g.ExpectedKubernetesVersion {
			t.Errorf("unexpected result from RecommendedKubernetesVersion(%q): expected=%q, actual=%q", g.KopsVersion, g.ExpectedKubernetesVersion, semverString(kubernetesVersion))
			continue
		}
	}
}

func TestOrdering(t *testing.T) {
	if !semver.MustParse("1.5.0").GTE(semver.MustParse("1.5.0-alpha1")) {
		t.Fatalf("Expected: 1.5.0 >= 1.5.0-alpha1")
	}

	if !semver.MustParseRange(">=1.5.0-alpha1")(semver.MustParse("1.5.0")) {
		t.Fatalf("Expected: '>=1.5.0-alpha1' to include 1.5.0")
	}
}

// TestChannelsSelfConsistent tests the channels have version recommendations that are consistent
// i.e. we don't recommend 1.5.2 and then recommend upgrading it to 1.5.4
func TestChannelsSelfConsistent(t *testing.T) {

	grid := []struct {
		KopsVersion               string
		Channel                   string
		ExpectedKubernetesVersion string
	}{
		{
			KopsVersion: "1.4.4",
			Channel:     "alpha",
		},
		{
			KopsVersion: "1.4.4",
			Channel:     "stable",
		},
		{
			KopsVersion: "1.5.3",
			Channel:     "alpha",
		},
		{
			KopsVersion: "1.5.3",
			Channel:     "stable",
		},
		{
			KopsVersion: "1.6.0-beta.1",
			Channel:     "alpha",
		},
		{
			KopsVersion: "1.6.0-beta.1",
			Channel:     "stable",
		},
		{
			KopsVersion: "1.7.0",
			Channel:     "alpha",
		},
		{
			KopsVersion: "1.7.0",
			Channel:     "stable",
		},
	}
	for _, g := range grid {
		srcDir := "../../../channels/"
		sourcePath := path.Join(srcDir, g.Channel)
		sourceBytes, err := ioutil.ReadFile(sourcePath)
		if err != nil {
			t.Fatalf("unexpected error reading sourcePath %q: %v", sourcePath, err)
		}

		channel, err := kops.ParseChannel(sourceBytes)
		if err != nil {
			t.Fatalf("failed to parse channel %s: %v", g.Channel, err)
		}

		kubernetesVersion := kops.RecommendedKubernetesVersion(channel, g.KopsVersion)

		if kubernetesVersion == nil {
			t.Errorf("RecommendedKubernetesVersion returned nil for %s/%q", g.Channel, kubernetesVersion)
			continue
		}

		versionInfo := kops.FindKubernetesVersionSpec(channel.Spec.KubernetesVersions, *kubernetesVersion)
		if versionInfo == nil {
			t.Errorf("FindKubernetesVersionSpec did not find version for %s/%q", g.Channel, *kubernetesVersion)
			continue
		}

		recommended, err := versionInfo.FindRecommendedUpgrade(*kubernetesVersion)
		if err != nil {
			t.Errorf("FindRecommendedUpgrade have error for %s/%q: %v", g.Channel, *kubernetesVersion, err)
			continue
		}

		if recommended != nil {
			t.Errorf("Kops recommended kubernetesVersion is %q, but then k8s recommended kubernetesVersion is %q (%s)", *kubernetesVersion, *recommended, g.Channel)
			continue
		}
	}
}

func semverString(sv *semver.Version) string {
	if sv == nil {
		return ""
	}
	return sv.String()
}

// All Images in channel should have a specified upstream image prefix
func TestChannelImages(t *testing.T) {
	for _, channel := range []string{"stable", "alpha"} {
		t.Run(channel+"-channel", func(t *testing.T) {
			sourcePath := "../../../channels/" + channel
			sourceBytes, err := ioutil.ReadFile(sourcePath)
			if err != nil {
				t.Fatalf("unexpected error reading sourcePath %q: %v", sourcePath, err)
			}

			channel, err := kops.ParseChannel(sourceBytes)
			if err != nil {
				t.Fatalf("failed to parse channel: %v", err)
			}

			for _, image := range channel.Spec.Images {
				if !channel.HasUpstreamImagePrefix(image.Name) {
					t.Errorf("unknown image prefix: %q", image.Name)
				}
			}
		})
	}
}
