/*
Copyright 2017 Luis Pabón luis@portworx.com

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 sanity

import (
	"crypto/rand"
	"fmt"
	"io/ioutil"
	"os"
	"testing"

	"github.com/kubernetes-csi/csi-test/utils"
	yaml "gopkg.in/yaml.v2"

	"google.golang.org/grpc"

	. "github.com/onsi/ginkgo"
	"github.com/onsi/ginkgo/reporters"
	. "github.com/onsi/gomega"
)

// CSISecrets consists of secrets used in CSI credentials.
type CSISecrets struct {
	CreateVolumeSecret              map[string]string `yaml:"CreateVolumeSecret"`
	DeleteVolumeSecret              map[string]string `yaml:"DeleteVolumeSecret"`
	ControllerPublishVolumeSecret   map[string]string `yaml:"ControllerPublishVolumeSecret"`
	ControllerUnpublishVolumeSecret map[string]string `yaml:"ControllerUnpublishVolumeSecret"`
	NodeStageVolumeSecret           map[string]string `yaml:"NodeStageVolumeSecret"`
	NodePublishVolumeSecret         map[string]string `yaml:"NodePublishVolumeSecret"`
	CreateSnapshotSecret            map[string]string `yaml:"CreateSnapshotSecret"`
	DeleteSnapshotSecret            map[string]string `yaml:"DeleteSnapshotSecret"`
}

// Config provides the configuration for the sanity tests. It
// needs to be initialized by the user of the sanity package.
type Config struct {
	TargetPath  string
	StagingPath string
	Address     string
	SecretsFile string

	TestVolumeSize           int64
	TestVolumeParametersFile string
	TestVolumeParameters     map[string]string

	JUnitFile string
}

// SanityContext holds the variables that each test can depend on. It
// gets initialized before each test block runs.
type SanityContext struct {
	Config  *Config
	Conn    *grpc.ClientConn
	Secrets *CSISecrets

	connAddress string
}

// Test will test the CSI driver at the specified address by
// setting up a Ginkgo suite and running it.
func Test(t *testing.T, reqConfig *Config) {
	path := reqConfig.TestVolumeParametersFile
	if len(path) != 0 {
		yamlFile, err := ioutil.ReadFile(path)
		if err != nil {
			panic(fmt.Sprintf("failed to read file %q: %v", path, err))
		}
		err = yaml.Unmarshal(yamlFile, &reqConfig.TestVolumeParameters)
		if err != nil {
			panic(fmt.Sprintf("error unmarshaling yaml: %v", err))
		}
	}

	sc := &SanityContext{
		Config: reqConfig,
	}

	registerTestsInGinkgo(sc)
	RegisterFailHandler(Fail)

	var specReporters []Reporter
	if reqConfig.JUnitFile != "" {
		junitReporter := reporters.NewJUnitReporter(reqConfig.JUnitFile)
		specReporters = append(specReporters, junitReporter)
	}
	RunSpecsWithDefaultAndCustomReporters(t, "CSI Driver Test Suite", specReporters)
	sc.Conn.Close()
}

func GinkgoTest(reqConfig *Config) {
	sc := &SanityContext{
		Config: reqConfig,
	}

	registerTestsInGinkgo(sc)
}

func (sc *SanityContext) setup() {
	var err error

	if len(sc.Config.SecretsFile) > 0 {
		sc.Secrets, err = loadSecrets(sc.Config.SecretsFile)
		Expect(err).NotTo(HaveOccurred())
	} else {
		sc.Secrets = &CSISecrets{}
	}

	// It is possible that a test sets sc.Config.Address
	// dynamically (and differently!) in a BeforeEach, so only
	// reuse the connection if the address is still the same.
	if sc.Conn == nil || sc.connAddress != sc.Config.Address {
		if sc.Conn != nil {
			sc.Conn.Close()
		}
		By("connecting to CSI driver")
		sc.Conn, err = utils.Connect(sc.Config.Address)
		Expect(err).NotTo(HaveOccurred())
		sc.connAddress = sc.Config.Address
	} else {
		By(fmt.Sprintf("reusing connection to CSI driver at %s", sc.connAddress))
	}

	By("creating mount and staging directories")
	err = createMountTargetLocation(sc.Config.TargetPath)
	Expect(err).NotTo(HaveOccurred())
	if len(sc.Config.StagingPath) > 0 {
		err = createMountTargetLocation(sc.Config.StagingPath)
		Expect(err).NotTo(HaveOccurred())
	}
}

func (sc *SanityContext) teardown() {
	// We intentionally do not close the connection to the CSI
	// driver here because the large amount of connection attempts
	// caused test failures
	// (https://github.com/kubernetes-csi/csi-test/issues/101). We
	// could fix this with retries
	// (https://github.com/kubernetes-csi/csi-test/pull/97) but
	// that requires more discussion, so instead we just connect
	// once per process instead of once per test case. This was
	// also said to be faster
	// (https://github.com/kubernetes-csi/csi-test/pull/98).
}

func createMountTargetLocation(targetPath string) error {
	fileInfo, err := os.Stat(targetPath)
	if err != nil && os.IsNotExist(err) {
		return os.MkdirAll(targetPath, 0755)
	} else if err != nil {
		return err
	}
	if !fileInfo.IsDir() {
		return fmt.Errorf("Target location %s is not a directory", targetPath)
	}

	return nil
}

func loadSecrets(path string) (*CSISecrets, error) {
	var creds CSISecrets

	yamlFile, err := ioutil.ReadFile(path)
	if err != nil {
		return &creds, fmt.Errorf("failed to read file %q: #%v", path, err)
	}

	err = yaml.Unmarshal(yamlFile, &creds)
	if err != nil {
		return &creds, fmt.Errorf("error unmarshaling yaml: #%v", err)
	}

	return &creds, nil
}

var uniqueSuffix = "-" + pseudoUUID()

// pseudoUUID returns a unique string generated from random
// bytes, empty string in case of error.
func pseudoUUID() string {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		// Shouldn't happen?!
		return ""
	}
	return fmt.Sprintf("%08X-%08X", b[0:4], b[4:8])
}

// uniqueString returns a unique string by appending a random
// number. In case of an error, just the prefix is returned, so it
// alone should already be fairly unique.
func uniqueString(prefix string) string {
	return prefix + uniqueSuffix
}
