//go:build !windows
// +build !windows

package net_test

import (
	"errors"
	"strings"

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

	boshlog "github.com/cloudfoundry/bosh-utils/logger"
	fakesys "github.com/cloudfoundry/bosh-utils/system/fakes"

	. "github.com/cloudfoundry/bosh-agent/v2/platform/net"
	fakearp "github.com/cloudfoundry/bosh-agent/v2/platform/net/arp/fakes"
	fakednsresolver "github.com/cloudfoundry/bosh-agent/v2/platform/net/dnsresolver/fakes"
	fakenet "github.com/cloudfoundry/bosh-agent/v2/platform/net/fakes"
	boship "github.com/cloudfoundry/bosh-agent/v2/platform/net/ip"
	fakeip "github.com/cloudfoundry/bosh-agent/v2/platform/net/ip/fakes"
	"github.com/cloudfoundry/bosh-agent/v2/platform/net/netfakes"
	boshsettings "github.com/cloudfoundry/bosh-agent/v2/settings"
)

var _ = Describe("UbuntuNetManager (IPv6)", func() {
	var (
		fs                            *fakesys.FakeFileSystem
		cmdRunner                     *fakesys.FakeCmdRunner
		ipResolver                    *fakeip.FakeResolver
		addressBroadcaster            *fakearp.FakeAddressBroadcaster
		interfaceAddrsProvider        *fakeip.FakeInterfaceAddressesProvider
		kernelIPv6                    *fakenet.FakeKernelIPv6
		netManager                    UbuntuNetManager
		interfaceConfigurationCreator InterfaceConfigurationCreator
		fakeMACAddressDetector        *netfakes.FakeMACAddressDetector
	)

	stubInterfaces := func(physicalInterfaces map[string]boshsettings.Network) {
		addresses := map[string]string{}
		for iface, networkSettings := range physicalInterfaces {
			addresses[networkSettings.Mac] = iface
		}

		fakeMACAddressDetector.DetectMacAddressesReturns(addresses, nil)
	}

	BeforeEach(func() {
		fs = fakesys.NewFakeFileSystem()
		cmdRunner = fakesys.NewFakeCmdRunner()
		ipResolver = &fakeip.FakeResolver{}
		logger := boshlog.NewLogger(boshlog.LevelNone)
		fakeMACAddressDetector = &netfakes.FakeMACAddressDetector{}
		interfaceConfigurationCreator = NewInterfaceConfigurationCreator(logger)
		addressBroadcaster = &fakearp.FakeAddressBroadcaster{}
		interfaceAddrsProvider = &fakeip.FakeInterfaceAddressesProvider{}
		fakeDnsResolver := &fakednsresolver.FakeDNSResolver{}
		kernelIPv6 = &fakenet.FakeKernelIPv6{}
		netManager = NewUbuntuNetManager(
			fs,
			cmdRunner,
			ipResolver,
			fakeMACAddressDetector,
			interfaceConfigurationCreator,
			interfaceAddrsProvider,
			fakeDnsResolver,
			addressBroadcaster,
			kernelIPv6,
			logger,
		).(UbuntuNetManager)
	})

	Describe("SetupNetworking", func() {
		BeforeEach(func() {
			err := fs.WriteFileString("/etc/resolv.conf", "nameserver 8.8.8.8\nnameserver 9.9.9.9")
			Expect(err).ToNot(HaveOccurred())

			err = fs.WriteFileString("/boot/grub/grub.cfg", "")
			Expect(err).ToNot(HaveOccurred())

			interfaceAddrsProvider.GetInterfaceAddresses = []boship.InterfaceAddress{
				boship.NewSimpleInterfaceAddress("ethstatic1", "2001:db8::103"),
				boship.NewSimpleInterfaceAddress("ethstatic2", "1.2.3.4"),
				boship.NewSimpleInterfaceAddress("ethstatic3", "3fff::100"),
			}
		})

		It("enables IPv6 if there are any static IPv6 addresses", func() {
			static1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "2001:db8::103",
				Netmask: "ffff:ffff:ffff:ffff:0000:0000:0000:0000",
				Gateway: "2001:db8::1",
				Default: []string{"gateway", "dns"},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "mac1",
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic1": static1Net,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"net1": static1Net}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Expect(kernelIPv6.Enabled).To(BeTrue())
		})

		It("enables IPv6 if there are any dhcp IPv6 addresses", func() {
			dhcp1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "2001:db8::103",
				Netmask: "ffff:ffff:ffff:ffff:0000:0000:0000:0000",
				Gateway: "2001:db8::1",
				Default: []string{"gateway", "dns"},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "mac1",
				UseDHCP: true,
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethdhcp1": dhcp1Net,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"net1": dhcp1Net}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Expect(kernelIPv6.Enabled).To(BeTrue())
		})

		It("enables IPv6 if there are IPv4 and IPv6 addresses with the same mac address", func() {
			static1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Default: nil,
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "mac1",
				UseDHCP: true,
				Routes: boshsettings.Routes{boshsettings.Route{
					Destination: "1.2.3.4",
					Gateway:     "3.4.5.6",
					Netmask:     "255.255.255.0",
				},
				},
			}
			static2Net := boshsettings.Network{
				Type:    "manual",
				IP:      "2001:db8::103",
				Netmask: "ffff:ffff:ffff:ffff:0000:0000:0000:0000",
				Gateway: "2001:db8::1",
				Default: []string{"gateway", "dns"},
				DNS:     []string{"2001:4860:4860::8888", "2001:4860:4860::8844"},
				Mac:     "mac1",
				UseDHCP: true,
				Routes: boshsettings.Routes{boshsettings.Route{
					Destination: "2001:db8:1234::",
					Gateway:     "2001:db8::1",
					Netmask:     "ffff:ffff:ffff:0000:0000:0000:0000:0000",
				},
				},
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic1": static1Net,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"net1": static1Net, "net2": static2Net}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Expect(kernelIPv6.Enabled).To(BeTrue())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).NotTo(HaveOccurred())
			Expect(matches).To(ConsistOf(
				"/etc/systemd/network/10_ethstatic1.network",
			))

			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_ethstatic1.network")
			Expect(networkConfig).ToNot(BeNil())

			Expect(strings.Contains(networkConfig.StringContents(), `
[Match]
Name=ethstatic1
`)).To(BeTrue())
			Expect(strings.Contains(networkConfig.StringContents(), `
[Network]
DHCP=yes
IPv6AcceptRA=true
DNS=2001:4860:4860::8888
DNS=2001:4860:4860::8844
`)).To(BeTrue())
			Expect(strings.Contains(networkConfig.StringContents(), `
[DHCP]
UseDomains=yes
UseMTU=yes
`)).To(BeTrue())
			Expect(strings.Contains(networkConfig.StringContents(), `
[Route]
Destination=1.2.3.4/24
Gateway=3.4.5.6
`)).To(BeTrue())
			Expect(strings.Contains(networkConfig.StringContents(), `
[Route]
Destination=2001:db8:1234::/48
Gateway=2001:db8::1
`)).To(BeTrue())
		})

		It("returns error if enabling IPv6 in kernel fails", func() {
			static1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "2001:db8::103",
				Netmask: "ffff:ffff:ffff:ffff:0000:0000:0000:0000",
				Gateway: "2001:db8::1",
				Default: []string{"gateway", "dns"},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "mac1",
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic1": static1Net,
			})

			kernelIPv6.EnableErr = errors.New("fake-err")

			err := netManager.SetupNetworking(boshsettings.Networks{"net1": static1Net}, "", nil)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("fake-err"))
		})

		It("does not enable IPv6 if there aren't any static IPv6 addresses", func() {
			static1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Default: nil,
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "mac2",
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic2": static1Net,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"net1": static1Net}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Expect(kernelIPv6.Enabled).To(BeFalse())
		})

		It("does not enable IPv6 if there aren't any dhcp IPv6 addresses", func() {
			dhcp1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Default: nil,
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "mac2",
				UseDHCP: true,
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic2": dhcp1Net,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{"net1": dhcp1Net}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			Expect(kernelIPv6.Enabled).To(BeFalse())
		})

		It("writes /etc/network/interfaces with static inet6 configuration when manual network is used", func() {
			static1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "2001:db8::103",
				Netmask: "ffff:ffff:ffff:ffff:0000:0000:0000:0000",
				Gateway: "2001:db8::1",
				Default: []string{"gateway", "dns"},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "mac1",
			}
			static2Net := boshsettings.Network{
				Type:    "manual",
				IP:      "1.2.3.4",
				Default: nil,
				Netmask: "255.255.255.0",
				Gateway: "3.4.5.6",
				Mac:     "mac2",
			}
			static3Net := boshsettings.Network{
				Type:    "manual",
				IP:      "3fff::100",
				Netmask: "ffff:ffff:ffff:ffff:ffff:0000:0000:0000",
				Gateway: "2601:646:100:eeee::",
				Default: []string{},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "mac3",
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic1": static1Net,
				"ethstatic2": static2Net,
				"ethstatic3": static3Net,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{
				"net1": static1Net,
				"net2": static2Net,
				"net3": static3Net,
			}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).NotTo(HaveOccurred())
			Expect(matches).To(ConsistOf(
				"/etc/systemd/network/10_ethstatic1.network",
				"/etc/systemd/network/10_ethstatic2.network",
				"/etc/systemd/network/10_ethstatic3.network",
			))
			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_ethstatic1.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethstatic1

[Address]
Address=2001:db8::103/64

[Network]
Gateway=2001:db8::1
IPv6AcceptRA=true
DNS=8.8.8.8
DNS=9.9.9.9

`))
			networkConfig = fs.GetFileTestStat("/etc/systemd/network/10_ethstatic2.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethstatic2

[Address]
Address=1.2.3.4/24

[Network]
DNS=8.8.8.8
DNS=9.9.9.9

`))
			networkConfig = fs.GetFileTestStat("/etc/systemd/network/10_ethstatic3.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethstatic3

[Address]
Address=3fff::100/80

[Network]
IPv6AcceptRA=true
DNS=8.8.8.8
DNS=9.9.9.9

`))
		})

		It("configures postup routes for static network", func() {
			static1Net := boshsettings.Network{
				Type:    "manual",
				IP:      "2001:db8::103",
				Netmask: "ffff:ffff:ffff:ffff:0000:0000:0000:0000",
				Gateway: "2001:db8::1",
				Default: []string{"gateway", "dns"},
				DNS:     []string{"8.8.8.8", "9.9.9.9"},
				Mac:     "mac1",
				Routes: []boshsettings.Route{
					boshsettings.Route{
						Destination: "2001:db8:1234::",
						Gateway:     "2001:db8::1",
						Netmask:     "ffff:ffff:ffff:0000:0000:0000:0000:0000",
					},
				},
			}

			stubInterfaces(map[string]boshsettings.Network{
				"ethstatic1": static1Net,
			})

			err := netManager.SetupNetworking(boshsettings.Networks{
				"net1": static1Net,
			}, "", nil)
			Expect(err).ToNot(HaveOccurred())

			matches, err := fs.Ls("/etc/systemd/network/")
			Expect(err).NotTo(HaveOccurred())
			Expect(matches).To(ConsistOf(
				"/etc/systemd/network/10_ethstatic1.network",
			))
			networkConfig := fs.GetFileTestStat("/etc/systemd/network/10_ethstatic1.network")
			Expect(networkConfig).ToNot(BeNil())
			Expect(networkConfig.StringContents()).To(Equal(`# Generated by bosh-agent
[Match]
Name=ethstatic1

[Address]
Address=2001:db8::103/64

[Network]
Gateway=2001:db8::1
IPv6AcceptRA=true
DNS=8.8.8.8
DNS=9.9.9.9

[Route]
Destination=2001:db8:1234::/48
Gateway=2001:db8::1

`))
		})
	})
})
