// Copyright (c) 2017 Intel Corporation
//
// 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 types

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"testing"

	"github.com/containernetworking/cni/pkg/skel"
	types020 "github.com/containernetworking/cni/pkg/types/020"
	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/containernetworking/plugins/pkg/testutils"
	testhelpers "github.com/intel/multus-cni/testing"

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

func TestConf(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "conf")
}

var _ = Describe("config operations", func() {
	var testNS ns.NetNS
	var tmpDir string

	BeforeEach(func() {
		// Create a new NetNS so we don't modify the host
		var err error
		testNS, err = testutils.NewNS()
		Expect(err).NotTo(HaveOccurred())
		os.Setenv("CNI_NETNS", testNS.Path())
		os.Setenv("CNI_PATH", "/some/path")

		tmpDir, err = ioutil.TempDir("", "multus_tmp")
		Expect(err).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		Expect(testNS.Close()).To(Succeed())
		os.Unsetenv("CNI_PATH")
		os.Unsetenv("CNI_ARGS")
		err := os.RemoveAll(tmpDir)
		Expect(err).NotTo(HaveOccurred())
	})

	It("parses a valid multus configuration", func() {
		conf := `{
    "name": "node-cni-network",
    "type": "multus",
    "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
    "delegates": [{
        "type": "weave-net"
    }],
	"runtimeConfig": {
      "portMappings": [
        {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
      ]
    }

}`
		netConf, err := LoadNetConf([]byte(conf))
		Expect(err).NotTo(HaveOccurred())
		Expect(len(netConf.Delegates)).To(Equal(1))
		Expect(netConf.Delegates[0].Conf.Type).To(Equal("weave-net"))
		Expect(netConf.Delegates[0].MasterPlugin).To(BeTrue())
		Expect(len(netConf.RuntimeConfig.PortMaps)).To(Equal(1))
	})

	It("fails to load invalid multus configuration (bad json)", func() {
		conf := `{
	  "name": "node-cni-network",
	  "type": "multus",
	  "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
	  "delegates": [{
	      "type": "weave-net"
	  }],
	"runtimeConfig": {
	    "portMappings": [
	      {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
	    ]
		}`
		// Error in conf json: missing end bracket

		_, err := LoadNetConf([]byte(conf))
		Expect(err).To(HaveOccurred())
		_, err = LoadDelegateNetConf([]byte(conf), nil, "")
		Expect(err).To(HaveOccurred())
		err = LoadDelegateNetConfList([]byte(conf), &DelegateNetConf{})
		Expect(err).To(HaveOccurred())
		_, err = addDeviceIDInConfList([]byte(conf), "")
		Expect(err).To(HaveOccurred())
		_, err = delegateAddDeviceID([]byte(conf), "")
		Expect(err).To(HaveOccurred())
	})

	It("checks if logFile and logLevel are set correctly", func() {
		conf := `{
	    "name": "node-cni-network",
			"type": "multus",
			"logLevel": "debug",
			"logFile": "/var/log/multus.log",
	    "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
	    "delegates": [{
	        "type": "weave-net"
	    }],
		"runtimeConfig": {
	      "portMappings": [
	        {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
	      ]
	    }

	}`
		netConf, err := LoadNetConf([]byte(conf))
		Expect(err).NotTo(HaveOccurred())
		Expect(netConf.LogLevel).To(Equal("debug"))
		Expect(netConf.LogFile).To(Equal("/var/log/multus.log"))
	})

	It("prevResult with no errors", func() {
		conf := `{
	    "name": "node-cni-network",
			"type": "multus",
	    "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
			"prevResult": {
				"ips": [
					{
						"version": "4",
						"address": "10.0.0.5/32",
						"interface": 2
					}
			]},
			"delegates": [{
	        "type": "weave-net"
			}],
		"runtimeConfig": {
	      "portMappings": [
	        {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
	      ]
	    }

	}`
		_, err := LoadNetConf([]byte(conf))
		Expect(err).NotTo(HaveOccurred())
	})

	It("succeeds if only delegates are set", func() {
		conf := `{
    "name": "node-cni-network",
    "type": "multus",
    "delegates": [{
        "type": "weave-net"
    },{
        "type": "foobar"
    }]
}`
		netConf, err := LoadNetConf([]byte(conf))
		Expect(err).NotTo(HaveOccurred())
		Expect(len(netConf.Delegates)).To(Equal(2))
		Expect(netConf.Delegates[0].Conf.Type).To(Equal("weave-net"))
		Expect(netConf.Delegates[0].MasterPlugin).To(BeTrue())
		Expect(netConf.Delegates[1].Conf.Type).To(Equal("foobar"))
		Expect(netConf.Delegates[1].MasterPlugin).To(BeFalse())
	})

	It("fails if no kubeconfig or delegates are set", func() {
		conf := `{
    "name": "node-cni-network",
    "type": "multus"
}`
		_, err := LoadNetConf([]byte(conf))
		Expect(err).To(HaveOccurred())
	})

	It("fails if kubeconfig is present but no delegates are set", func() {
		conf := `{
    "name": "node-cni-network",
    "type": "multus",
    "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml"
}`
		_, err := LoadNetConf([]byte(conf))
		Expect(err).To(HaveOccurred())
	})

	It("fails when delegate field exists but fields are named incorrectly", func() {
		conf := `{
	"name": "node-cni-network",
		"type": "multus",
    "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
		"prevResult": {
			"ips": [
				{
					"version": "4",
					"address": "10.0.0.5/32",
					"interface": 2
				}
			]
		},
		"delegates": [{
			"_not_type": "weave-net"
		}],
	"runtimeConfig": {
		"portMappings": [
			{"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
	    ]
	}
}`
		_, err := LoadNetConf([]byte(conf))
		Expect(err).To(HaveOccurred())
	})

	It("has defaults set for network readiness", func() {
		conf := `{
    "name": "defaultnetwork",
    "type": "multus",
    "kubeconfig": "/etc/kubernetes/kubelet.conf",
    "delegates": [{
      "cniVersion": "0.3.0",
      "name": "defaultnetwork",
      "type": "flannel",
      "isDefaultGateway": true
    }]
}`
		netConf, err := LoadNetConf([]byte(conf))
		Expect(err).NotTo(HaveOccurred())
		Expect(netConf.ReadinessIndicatorFile).To(Equal(""))
	})

	It("honors overrides for network readiness", func() {
		conf := `{
    "name": "defaultnetwork",
    "type": "multus",
    "readinessindicatorfile": "/etc/cni/net.d/foo",
    "kubeconfig": "/etc/kubernetes/kubelet.conf",
    "delegates": [{
      "cniVersion": "0.3.0",
      "name": "defaultnetwork",
      "type": "flannel",
      "isDefaultGateway": true
    }]
}`
		netConf, err := LoadNetConf([]byte(conf))
		Expect(err).NotTo(HaveOccurred())
		Expect(netConf.ReadinessIndicatorFile).To(Equal("/etc/cni/net.d/foo"))
	})

	It("check CheckSystemNamespaces() works fine", func() {
		b1 := CheckSystemNamespaces("foobar", []string{"barfoo", "bafoo", "foobar"})
		Expect(b1).To(Equal(true))
		b2 := CheckSystemNamespaces("foobar1", []string{"barfoo", "bafoo", "foobar"})
		Expect(b2).To(Equal(false))
	})

	It("assigns deviceID in delegated conf", func() {
		conf := `{
    "name": "second-network",
    "type": "sriov"
}`
		type sriovNetConf struct {
			Name     string `json:"name"`
			Type     string `json:"type"`
			DeviceID string `json:"deviceID"`
		}
		sriovConf := &sriovNetConf{}
		delegateNetConf, err := LoadDelegateNetConf([]byte(conf), nil, "0000:00:00.0")
		Expect(err).NotTo(HaveOccurred())

		err = json.Unmarshal(delegateNetConf.Bytes, &sriovConf)
		Expect(err).NotTo(HaveOccurred())
		Expect(sriovConf.DeviceID).To(Equal("0000:00:00.0"))
	})

	It("assigns deviceID in delegated conf list", func() {
		conf := `{
    "name": "second-network",
    "plugins": [
      {
        "type": "sriov"
      }
    ]
}`
		type sriovNetConf struct {
			Name     string `json:"name"`
			Type     string `json:"type"`
			DeviceID string `json:"deviceID"`
		}
		type sriovNetConfList struct {
			Plugins []*sriovNetConf `json:"plugins"`
		}
		sriovConfList := &sriovNetConfList{}
		delegateNetConf, err := LoadDelegateNetConf([]byte(conf), nil, "0000:00:00.1")
		Expect(err).NotTo(HaveOccurred())

		err = json.Unmarshal(delegateNetConf.Bytes, &sriovConfList)
		Expect(err).NotTo(HaveOccurred())
		Expect(sriovConfList.Plugins[0].DeviceID).To(Equal("0000:00:00.1"))
	})

	It("assigns pciBusID in delegated conf", func() {
		conf := `{
    "name": "second-network",
    "type": "host-device"
}`
		type hostDeviceNetConf struct {
			Name     string `json:"name"`
			Type     string `json:"type"`
			PCIBusID string `json:"pciBusID"`
		}
		hostDeviceConf := &hostDeviceNetConf{}
		delegateNetConf, err := LoadDelegateNetConf([]byte(conf), nil, "0000:00:00.2")
		Expect(err).NotTo(HaveOccurred())

		err = json.Unmarshal(delegateNetConf.Bytes, &hostDeviceConf)
		Expect(err).NotTo(HaveOccurred())
		Expect(hostDeviceConf.PCIBusID).To(Equal("0000:00:00.2"))
	})

	It("assigns pciBusID in delegated conf list", func() {
		conf := `{
    "name": "second-network",
    "plugins": [
      {
        "type": "host-device"
      }
    ]
}`
		type hostDeviceNetConf struct {
			Name     string `json:"name"`
			Type     string `json:"type"`
			PCIBusID string `json:"pciBusID"`
		}
		type hostDeviceNetConfList struct {
			Plugins []*hostDeviceNetConf `json:"plugins"`
		}
		hostDeviceConfList := &hostDeviceNetConfList{}
		delegateNetConf, err := LoadDelegateNetConf([]byte(conf), nil, "0000:00:00.3")
		Expect(err).NotTo(HaveOccurred())

		err = json.Unmarshal(delegateNetConf.Bytes, &hostDeviceConfList)
		Expect(err).NotTo(HaveOccurred())
		Expect(hostDeviceConfList.Plugins[0].PCIBusID).To(Equal("0000:00:00.3"))
	})

	It("add cni-args in config", func() {
		var args map[string]interface{}
		conf := `{
    "name": "second-network",
    "type": "bridge"
}`
		cniArgs := `{
    "args1": "val1"
}`
		type bridgeNetConf struct {
			Name     string `json:"name"`
			Type     string `json:"type"`
			Args     struct {
				CNI map[string]string `json:"cni"`
			} `json:"args"`
		}

                err := json.Unmarshal([]byte(cniArgs), &args)
		Expect(err).NotTo(HaveOccurred())
                net := &NetworkSelectionElement{
			Name: "test-elem",
			CNIArgs: &args,
		}
		delegateNetConf, err := LoadDelegateNetConf([]byte(conf), net, "")
		Expect(err).NotTo(HaveOccurred())
		bridgeConf := &bridgeNetConf{}
		err = json.Unmarshal(delegateNetConf.Bytes, bridgeConf)
		Expect(bridgeConf.Args.CNI["args1"]).To(Equal("val1"))
	})

	It("add cni-args in config which has cni args already (merge case)", func() {
		var args map[string]interface{}
		conf := `{
    "name": "second-network",
    "type": "bridge",
    "args": {
       "cni": {
         "args0": "val0",
         "args1": "val1"
       }
    }
}`
		cniArgs := `{
    "args1": "val1a"
}`
		type bridgeNetConf struct {
			Name     string `json:"name"`
			Type     string `json:"type"`
			Args     struct {
				CNI map[string]string `json:"cni"`
			} `json:"args"`
		}

                err := json.Unmarshal([]byte(cniArgs), &args)
		Expect(err).NotTo(HaveOccurred())
                net := &NetworkSelectionElement{
			Name: "test-elem",
			CNIArgs: &args,
		}
		delegateNetConf, err := LoadDelegateNetConf([]byte(conf), net, "")
		Expect(err).NotTo(HaveOccurred())
		bridgeConf := &bridgeNetConf{}
		err = json.Unmarshal(delegateNetConf.Bytes, bridgeConf)
		Expect(bridgeConf.Args.CNI["args0"]).To(Equal("val0"))
		Expect(bridgeConf.Args.CNI["args1"]).To(Equal("val1a"))
	})

	It("add cni-args in conflist", func() {
		var args map[string]interface{}
		conf := `{
    "name": "second-network",
    "plugins": [
      {
        "type": "bridge"
      }
    ]
}`
		cniArgs := `{
    "args1": "val1"
}`
		type bridgeNetConf struct {
			Type     string `json:"type"`
			Args     struct {
				CNI map[string]string `json:"cni"`
			} `json:"args"`
		}
		type bridgeNetConfList struct {
			Name     string `json:"name"`
			Plugins []*bridgeNetConf `json:"plugins"`
		}

                err := json.Unmarshal([]byte(cniArgs), &args)
		Expect(err).NotTo(HaveOccurred())
                net := &NetworkSelectionElement{
			Name: "test-elem",
			CNIArgs: &args,
		}
		delegateNetConf, err := LoadDelegateNetConf([]byte(conf), net, "")
		Expect(err).NotTo(HaveOccurred())
		bridgeConflist := &bridgeNetConfList{}
		err = json.Unmarshal(delegateNetConf.Bytes, bridgeConflist)
		Expect(bridgeConflist.Plugins[0].Args.CNI["args1"]).To(Equal("val1"))
	})

	It("creates a valid CNI runtime config", func() {
		args := &skel.CmdArgs{
			ContainerID: "123456789",
			Netns:       testNS.Path(),
			IfName:      "eth0",
			StdinData: []byte(`{
    "name": "node-cni-network",
    "type": "multus",
    "defaultnetworkfile": "/tmp/foo.multus.conf",
    "defaultnetworkwaitseconds": 3,
    "delegates": [{
        "name": "weave1",
        "cniVersion": "0.2.0",
        "type": "weave-net"
    },{
        "name": "other1",
        "cniVersion": "0.2.0",
        "type": "other-plugin"
    }]
}`),
		}

		k8sArgs := &K8sArgs{K8S_POD_NAME: "dummy", K8S_POD_NAMESPACE: "namespacedummy", K8S_POD_INFRA_CONTAINER_ID: "123456789"}

		rc := &RuntimeConfig{}
		rc.PortMaps = make([]*PortMapEntry, 2)

		rc.PortMaps[0] = &PortMapEntry{
			HostPort:      0,
			ContainerPort: 1,
			Protocol:      "sampleProtocol",
			HostIP:        "sampleHostIP",
		}
		rc.PortMaps[1] = &PortMapEntry{
			HostPort:      1,
			ContainerPort: 2,
			Protocol:      "anotherSampleProtocol",
			HostIP:        "anotherSampleHostIP",
		}

		rt := CreateCNIRuntimeConf(args, k8sArgs, "", rc)
		fmt.Println("rt.ContainerID: ", rt.ContainerID)
		Expect(rt.ContainerID).To(Equal("123456789"))
		Expect(rt.NetNS).To(Equal(args.Netns))
		Expect(rt.IfName).To(Equal(""))
		Expect(rt.CapabilityArgs["portMappings"]).To(Equal(rc.PortMaps))
	})

	It("can loadnetworkstatus", func() {
		result := &types020.Result{
			CNIVersion: "0.2.0",
			IP4: &types020.IPConfig{
				IP: *testhelpers.EnsureCIDR("1.1.1.2/24"),
			},
		}

		conf := `{
    "name": "node-cni-network",
    "type": "multus",
    "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
    "delegates": [{
        "type": "weave-net"
    }],
  "runtimeConfig": {
      "portMappings": [
        {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
      ]
    }
}`

		delegate, err := LoadDelegateNetConf([]byte(conf), nil, "0000:00:00.0")
		Expect(err).NotTo(HaveOccurred())

		delegateNetStatus, err := LoadNetworkStatus(result, delegate.Conf.Name, delegate.MasterPlugin)

		GinkgoT().Logf("delegateNetStatus %+v\n", delegateNetStatus)

		Expect(err).NotTo(HaveOccurred())
	})

	It("cannot loadnetworkstatus given incompatible CNIVersion", func() {

		result := &testhelpers.Result{
			CNIVersion: "1.2.3",
			IP4: &types020.IPConfig{
				IP: *testhelpers.EnsureCIDR("1.1.1.2/24"),
			},
		}

		conf := `{
    "name": "node-cni-network",
    "type": "multus",
    "kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
    "delegates": [{
        "type": "weave-net"
    }],
  "runtimeConfig": {
      "portMappings": [
        {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
      ]
    }
}`

		delegate, err := LoadDelegateNetConf([]byte(conf), nil, "0000:00:00.0")
		Expect(err).NotTo(HaveOccurred())
		fmt.Println("result.Version: ", result.Version())
		delegateNetStatus, err := LoadNetworkStatus(result, delegate.Conf.Name, delegate.MasterPlugin)

		GinkgoT().Logf("delegateNetStatus %+v\n", delegateNetStatus)

		Expect(err).To(HaveOccurred())
	})

	It("verify the network selection elements goes into delegateconf", func() {
		cniConfig := `{
        "name": "weave1",
        "cniVersion": "0.2.0",
        "type": "weave-net"
    }`
		bandwidthEntry1 := &BandwidthEntry{
			IngressRate:  100,
			IngressBurst: 200,
			EgressRate:   100,
			EgressBurst:  200,
		}

		portMapEntry1 := &PortMapEntry{
			HostPort:      8080,
			ContainerPort: 80,
			Protocol:      "tcp",
			HostIP:        "10.0.0.1",
		}

		networkSelection := &NetworkSelectionElement{
			Name:                "testname",
			InterfaceRequest:    "testIF1",
			MacRequest:          "c2:11:22:33:44:66",
			IPRequest:           []string{"10.0.0.1/24"},
			BandwidthRequest:    bandwidthEntry1,
			PortMappingsRequest: []*PortMapEntry{portMapEntry1},
		}

		delegateConf, err := LoadDelegateNetConf([]byte(cniConfig), networkSelection, "")
		Expect(err).NotTo(HaveOccurred())
		Expect(delegateConf.IfnameRequest).To(Equal(networkSelection.InterfaceRequest))
		Expect(delegateConf.MacRequest).To(Equal(networkSelection.MacRequest))
		Expect(delegateConf.IPRequest).To(Equal(networkSelection.IPRequest))
		Expect(delegateConf.BandwidthRequest).To(Equal(networkSelection.BandwidthRequest))
		Expect(delegateConf.PortMappingsRequest).To(Equal(networkSelection.PortMappingsRequest))
	})

	It("test MergeCNIRuntimeConfig with masterPlugin", func() {
		conf := `{
			"name": "node-cni-network",
			"type": "multus",
			"kubeconfig": "/etc/kubernetes/node-kubeconfig.yaml",
			"delegates": [{
				"type": "weave-net"
			}],
		  "runtimeConfig": {
			  "portMappings": [
				{"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
			  ]
			}
		}`
		bandwidthEntry1 := &BandwidthEntry{
			IngressRate:  100,
			IngressBurst: 200,
			EgressRate:   100,
			EgressBurst:  200,
		}
		portMapEntry1 := &PortMapEntry{
			HostPort:      8080,
			ContainerPort: 80,
			Protocol:      "tcp",
			HostIP:        "10.0.0.1",
		}

		networkSelection := &NetworkSelectionElement{
			Name:                "testname",
			InterfaceRequest:    "testIF1",
			MacRequest:          "c2:11:22:33:44:66",
			IPRequest:           []string{"10.0.0.1/24"},
			BandwidthRequest:    bandwidthEntry1,
			PortMappingsRequest: []*PortMapEntry{portMapEntry1},
		}
		delegate, err := LoadDelegateNetConf([]byte(conf), networkSelection, "")
		delegate.MasterPlugin = true
		Expect(err).NotTo(HaveOccurred())
		runtimeConf := MergeCNIRuntimeConfig(&RuntimeConfig{}, delegate)
		Expect(runtimeConf.PortMaps).To(BeNil())
		Expect(runtimeConf.Bandwidth).To(BeNil())
	})

	It("test MergeCNIRuntimeConfig with delegate plugin", func() {
		conf := `{
			"name": "weave1",
			"cniVersion": "0.2.0",
			"type": "weave-net"
		}`
		bandwidthEntry1 := &BandwidthEntry{
			IngressRate:  100,
			IngressBurst: 200,
			EgressRate:   100,
			EgressBurst:  200,
		}
		portMapEntry1 := &PortMapEntry{
			HostPort:      8080,
			ContainerPort: 80,
			Protocol:      "tcp",
			HostIP:        "10.0.0.1",
		}

		networkSelection := &NetworkSelectionElement{
			Name:                "testname",
			InterfaceRequest:    "testIF1",
			MacRequest:          "c2:11:22:33:44:66",
			IPRequest:           []string{"10.0.0.1/24"},
			BandwidthRequest:    bandwidthEntry1,
			PortMappingsRequest: []*PortMapEntry{portMapEntry1},
		}
		delegate, err := LoadDelegateNetConf([]byte(conf), networkSelection, "")
		Expect(err).NotTo(HaveOccurred())
		runtimeConf := MergeCNIRuntimeConfig(&RuntimeConfig{}, delegate)
		Expect(runtimeConf.PortMaps).NotTo(BeNil())
		Expect(len(runtimeConf.PortMaps)).To(BeEquivalentTo(1))
		Expect(runtimeConf.PortMaps[0]).To(Equal(portMapEntry1))
		Expect(runtimeConf.Bandwidth).To(Equal(bandwidthEntry1))
		Expect(len(runtimeConf.IPs)).To(BeEquivalentTo(1))
		Expect(runtimeConf.Mac).To(Equal("c2:11:22:33:44:66"))
	})
})
