// Copyright (c) 2022 Multus 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 multus

// disable dot-imports only for testing
//revive:disable:dot-imports
import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"path/filepath"
	"strings"
	"testing"

	cnitypes "github.com/containernetworking/cni/pkg/types"
	cni020 "github.com/containernetworking/cni/pkg/types/020"
	cni040 "github.com/containernetworking/cni/pkg/types/040"
	cni100 "github.com/containernetworking/cni/pkg/types/100"
	cniversion "github.com/containernetworking/cni/pkg/version"
	netfake "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/fake"
	"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/k8sclient"
	"k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/tools/record"

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

func TestMultus(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "multus")
}

type fakePlugin struct {
	expectedEnv    []string
	expectedConf   string
	expectedIfname string
	result         cnitypes.Result
	err            error
}

type fakeExec struct {
	cniversion.PluginDecoder

	addIndex        int
	delIndex        int
	chkIndex        int
	statusIndex     int
	gcIndex         int
	expectedDelSkip int
	plugins         map[string]*fakePlugin
}

func newFakeExec() *fakeExec {
	return &fakeExec{
		plugins: map[string]*fakePlugin{},
	}
}

func (f *fakeExec) addPlugin100(expectedEnv []string, expectedIfname, expectedConf string, result *cni100.Result, err error) {
	f.plugins[expectedIfname] = &fakePlugin{
		expectedEnv:    expectedEnv,
		expectedConf:   expectedConf,
		expectedIfname: expectedIfname,
		result:         result,
		err:            err,
	}
	if err != nil && err.Error() == "missing network name" {
		f.expectedDelSkip++
	}
}

func (f *fakeExec) addPlugin040(expectedEnv []string, expectedIfname, expectedConf string, result *cni040.Result, err error) {
	f.plugins[expectedIfname] = &fakePlugin{
		expectedEnv:    expectedEnv,
		expectedConf:   expectedConf,
		expectedIfname: expectedIfname,
		result:         result,
		err:            err,
	}
	if err != nil && err.Error() == "missing network name" {
		f.expectedDelSkip++
	}
}

func (f *fakeExec) addPlugin020(expectedEnv []string, expectedIfname, expectedConf string, result *cni020.Result, err error) {
	f.plugins[expectedIfname] = &fakePlugin{
		expectedEnv:    expectedEnv,
		expectedConf:   expectedConf,
		expectedIfname: expectedIfname,
		result:         result,
		err:            err,
	}
	if err != nil && err.Error() == "missing network name" {
		f.expectedDelSkip++
	}
}

func matchArray(a1, a2 []string) {
	Expect(len(a1)).To(Equal(len(a2)))
	for _, e1 := range a1 {
		found := ""
		for _, e2 := range a2 {
			if e1 == e2 {
				found = e2
				break
			}
		}
		// Compare element values for more descriptive test failure
		Expect(e1).To(Equal(found))
	}
}

// When faking plugin execution the ExecPlugin() call environ is not populated
// (while it would be for real exec). Filter the environment variables for
// CNI-specific ones that testcases will care about.
func gatherCNIEnv(environ []string) []string {
	filtered := make([]string, 0)
	for _, v := range environ {
		if strings.HasPrefix(v, "CNI_") {
			filtered = append(filtered, v)
		}
	}
	return filtered
}

func ParseEnvironment(environ []string) map[string]string {
	m := map[string]string{}

	for _, e := range environ {
		if e != "" {
			parts := strings.SplitN(e, "=", 2)
			ExpectWithOffset(2, len(parts)).To(Equal(2))
			m[parts[0]] = parts[1]
		}
	}
	return m
}

func (f *fakeExec) ExecPlugin(_ context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
	envMap := ParseEnvironment(environ)
	cmd := envMap["CNI_COMMAND"]
	var index int
	var err error
	var resultJSON []byte

	switch cmd {
	case "ADD":
		Expect(len(f.plugins)).To(BeNumerically(">", f.addIndex))
		index = f.addIndex
		f.addIndex++
	case "CHECK":
		Expect(len(f.plugins)).To(BeNumerically("==", f.addIndex))
		index = f.chkIndex
		f.chkIndex++
	case "DEL":
		Expect(len(f.plugins)).To(BeNumerically(">", f.delIndex))
		index = len(f.plugins) - f.expectedDelSkip - f.delIndex - 1
		f.delIndex++
	case "GC":
		Expect(len(f.plugins)).To(BeNumerically(">", f.statusIndex))
		index = f.gcIndex
		f.gcIndex++
	case "STATUS":
		Expect(len(f.plugins)).To(BeNumerically(">", f.statusIndex))
		index = f.statusIndex
		f.statusIndex++
	default:
		// Should never be reached
		Expect(false).To(BeTrue())
	}
	plugin := f.plugins[envMap["CNI_IFNAME"]]

	//GinkgoT().Logf("[%s %d] exec plugin %q found %+v\n", cmd, index, pluginPath, plugin)
	fmt.Printf("[%s %d] exec plugin %q found %+v\n", cmd, index, pluginPath, plugin)

	// strip prevResult from stdinData; tests don't need it
	var m map[string]interface{}
	reader := strings.NewReader(string(stdinData))
	writer := new(bytes.Buffer)

	dec := json.NewDecoder(reader)
	enc := json.NewEncoder(writer)
	err = dec.Decode(&m)
	Expect(err).NotTo(HaveOccurred())
	for k := range m {
		if k == "prevResult" {
			delete(m, k)
		}
	}
	err = enc.Encode(&m)
	Expect(err).NotTo(HaveOccurred())

	if plugin.expectedConf != "" {
		Expect(writer).To(MatchJSON(plugin.expectedConf))
	}
	if plugin.expectedIfname != "" {
		Expect(envMap["CNI_IFNAME"]).To(Equal(plugin.expectedIfname))
	}

	if len(plugin.expectedEnv) > 0 {
		cniEnv := gatherCNIEnv(environ)
		for _, expectedCniEnvVar := range plugin.expectedEnv {
			Expect(cniEnv).To(ContainElement(expectedCniEnvVar))
		}
	}

	if plugin.err != nil {
		return nil, plugin.err
	}

	resultJSON, err = json.Marshal(plugin.result)
	Expect(err).NotTo(HaveOccurred())
	return resultJSON, nil
}

func (f *fakeExec) FindInPath(plugin string, paths []string) (string, error) {
	Expect(len(paths)).To(BeNumerically(">", 0))
	return filepath.Join(paths[0], plugin), nil
}

// NewFakeClientInfo returns fake client (just for testing)
func NewFakeClientInfo() *k8sclient.ClientInfo {
	return &k8sclient.ClientInfo{
		Client:        fake.NewSimpleClientset(),
		NetClient:     netfake.NewSimpleClientset(),
		EventRecorder: record.NewFakeRecorder(10),
	}
}

func collectEvents(source <-chan string) []string {
	done := false
	events := make([]string, 0)
	for !done {
		select {
		case ev := <-source:
			events = append(events, ev)
		default:
			done = true
		}
	}
	return events
}
