// Copyright 2017 CNI 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 (
	"fmt"

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

	"github.com/containernetworking/cni/pkg/types"
)

var _ = Describe("portmapping configuration", func() {
	for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} {
		// Redefine ver inside for scope so real value is picked up by each dynamically defined It()
		// See Gingkgo's "Patterns for dynamically generating tests" documentation.
		ver := ver

		Context("config parsing", func() {
			It(fmt.Sprintf("[%s] correctly parses an ADD config", ver), func() {
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"backend": "iptables",
					"runtimeConfig": {
						"portMappings": [
							{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"},
							{ "hostPort": 8081, "containerPort": 81, "protocol": "udp"}
						]
					},
					"snat": false,
					"conditionsV4": ["-s", "1.2.3.4"],
					"conditionsV6": ["!", "-s", "12::34"],
					"prevResult": {
						"interfaces": [
							{"name": "host"},
							{"name": "container", "sandbox":"netns"}
						],
						"ips": [
							{
								"version": "4",
								"address": "10.0.0.1/24",
								"gateway": "10.0.0.1",
								"interface": 0
							},
							{
								"version": "6",
								"address": "2001:db8:1::2/64",
								"gateway": "2001:db8:1::1",
								"interface": 1
							},
							{
								"version": "4",
								"address": "10.0.0.2/24",
								"gateway": "10.0.0.1",
								"interface": 1
							}
						]
					}
				}`, ver))
				c, _, err := parseConfig(configBytes, "container")
				Expect(err).NotTo(HaveOccurred())
				Expect(c.CNIVersion).To(Equal(ver))
				Expect(c.ConditionsV4).To(Equal(&[]string{"-s", "1.2.3.4"}))
				Expect(c.ConditionsV6).To(Equal(&[]string{"!", "-s", "12::34"}))
				fvar := false
				Expect(c.SNAT).To(Equal(&fvar))
				Expect(c.Name).To(Equal("test"))

				n, err := types.ParseCIDR("10.0.0.2/24")
				Expect(err).NotTo(HaveOccurred())
				Expect(c.ContIPv4).To(Equal(*n))
				n, err = types.ParseCIDR("2001:db8:1::2/64")
				Expect(err).NotTo(HaveOccurred())
				Expect(c.ContIPv6).To(Equal(*n))
			})

			It(fmt.Sprintf("[%s] correctly parses a DEL config", ver), func() {
				// When called with DEL, neither runtimeConfig nor prevResult may be specified
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"backend": "iptables",
					"snat": false,
					"conditionsV4": ["-s", "1.2.3.4"],
					"conditionsV6": ["-s", "12::34"]
				}`, ver))
				c, _, err := parseConfig(configBytes, "container")
				Expect(err).NotTo(HaveOccurred())
				Expect(c.CNIVersion).To(Equal(ver))
				Expect(c.ConditionsV4).To(Equal(&[]string{"-s", "1.2.3.4"}))
				Expect(c.ConditionsV6).To(Equal(&[]string{"-s", "12::34"}))
				fvar := false
				Expect(c.SNAT).To(Equal(&fvar))
				Expect(c.Name).To(Equal("test"))
			})

			It(fmt.Sprintf("[%s] fails with invalid mappings", ver), func() {
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"backend": "iptables",
					"snat": false,
					"conditionsV4": ["-s", "1.2.3.4"],
					"conditionsV6": ["-s", "12::34"],
					"runtimeConfig": {
						"portMappings": [
							{ "hostPort": 0, "containerPort": 80, "protocol": "tcp"}
						]
					}
				}`, ver))
				_, _, err := parseConfig(configBytes, "container")
				Expect(err).To(MatchError("Invalid host port number: 0"))
			})

			It(fmt.Sprintf("[%s] defaults to iptables when backend is not specified", ver), func() {
				// "defaults to iptables" is only true if iptables is installed
				// (or if neither iptables nor nftables is installed), but the
				// other unit tests would fail if iptables wasn't installed, so
				// we know it must be.
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s"
				}`, ver))
				c, _, err := parseConfig(configBytes, "container")
				Expect(err).NotTo(HaveOccurred())
				Expect(c.CNIVersion).To(Equal(ver))
				Expect(c.Backend).To(Equal(&iptablesBackend))
				Expect(c.Name).To(Equal("test"))
			})

			It(fmt.Sprintf("[%s] uses nftables if requested", ver), func() {
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"backend": "nftables"
				}`, ver))
				c, _, err := parseConfig(configBytes, "container")
				Expect(err).NotTo(HaveOccurred())
				Expect(c.CNIVersion).To(Equal(ver))
				Expect(c.Backend).To(Equal(&nftablesBackend))
				Expect(c.Name).To(Equal("test"))
			})

			It(fmt.Sprintf("[%s] allows nftables conditions if nftables is requested", ver), func() {
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"backend": "nftables",
					"conditionsV4": ["ip", "saddr", "1.2.3.4"],
					"conditionsV6": ["ip6", "saddr", "12::34"]
				}`, ver))
				c, _, err := parseConfig(configBytes, "container")
				Expect(err).NotTo(HaveOccurred())
				Expect(c.CNIVersion).To(Equal(ver))
				Expect(c.Backend).To(Equal(&nftablesBackend))
				Expect(c.ConditionsV4).To(Equal(&[]string{"ip", "saddr", "1.2.3.4"}))
				Expect(c.ConditionsV6).To(Equal(&[]string{"ip6", "saddr", "12::34"}))
				Expect(c.Name).To(Equal("test"))
			})

			It(fmt.Sprintf("[%s] rejects nftables options with 'backend: iptables'", ver), func() {
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"backend": "iptables",
					"conditionsV4": ["ip", "saddr", "1.2.3.4"],
					"conditionsV6": ["ip6", "saddr", "12::34"]
				}`, ver))
				_, _, err := parseConfig(configBytes, "container")
				Expect(err).To(MatchError("iptables backend was requested but configuration contains nftables-specific options [conditionsV4 conditionsV6]"))
			})

			It(fmt.Sprintf("[%s] rejects iptables options with 'backend: nftables'", ver), func() {
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"backend": "nftables",
					"externalSetMarkChain": "KUBE-MARK-MASQ",
					"conditionsV4": ["-s", "1.2.3.4"],
					"conditionsV6": ["-s", "12::34"]
				}`, ver))
				_, _, err := parseConfig(configBytes, "container")
				Expect(err).To(MatchError("nftables backend was requested but configuration contains iptables-specific options [externalSetMarkChain conditionsV4 conditionsV6]"))
			})

			It(fmt.Sprintf("[%s] does not fail on missing prevResult interface index", ver), func() {
				configBytes := []byte(fmt.Sprintf(`{
					"name": "test",
					"type": "portmap",
					"cniVersion": "%s",
					"runtimeConfig": {
						"portMappings": [
							{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
						]
					},
					"conditionsV4": ["-s", "1.2.3.4"],
					"prevResult": {
						"interfaces": [
							{"name": "host"}
						],
						"ips": [
							{
								"version": "4",
								"address": "10.0.0.1/24",
								"gateway": "10.0.0.1"
							}
						]
					}
				}`, ver))
				_, _, err := parseConfig(configBytes, "container")
				Expect(err).NotTo(HaveOccurred())
			})
		})
	}
})
