package netlink

import (
	"bytes"
	"net"
	"os"
	"testing"

	"github.com/vishvananda/netlink/nl"
	"golang.org/x/sys/unix"
)

func TestParseIpsetProtocolResult(t *testing.T) {
	msgBytes, err := os.ReadFile("testdata/ipset_protocol_result")
	if err != nil {
		t.Fatalf("reading test fixture failed: %v", err)
	}

	msg := ipsetUnserialize([][]byte{msgBytes})
	if msg.Protocol != 6 {
		t.Errorf("expected msg.Protocol to equal 6, got %d", msg.Protocol)
	}
}

func TestParseIpsetListResult(t *testing.T) {
	msgBytes, err := os.ReadFile("testdata/ipset_list_result")
	if err != nil {
		t.Fatalf("reading test fixture failed: %v", err)
	}

	msg := ipsetUnserialize([][]byte{msgBytes})
	if msg.SetName != "clients" {
		t.Errorf(`expected SetName to equal "clients", got %q`, msg.SetName)
	}
	if msg.TypeName != "hash:mac" {
		t.Errorf(`expected TypeName to equal "hash:mac", got %q`, msg.TypeName)
	}
	if msg.Protocol != 6 {
		t.Errorf("expected Protocol to equal 6, got %d", msg.Protocol)
	}
	if msg.References != 0 {
		t.Errorf("expected References to equal 0, got %d", msg.References)
	}
	if msg.NumEntries != 2 {
		t.Errorf("expected NumEntries to equal 2, got %d", msg.NumEntries)
	}
	if msg.HashSize != 1024 {
		t.Errorf("expected HashSize to equal 1024, got %d", msg.HashSize)
	}
	if *msg.Timeout != 3600 {
		t.Errorf("expected Timeout to equal 3600, got %d", *msg.Timeout)
	}
	if msg.MaxElements != 65536 {
		t.Errorf("expected MaxElements to equal 65536, got %d", msg.MaxElements)
	}
	if msg.CadtFlags != nl.IPSET_FLAG_WITH_COMMENT|nl.IPSET_FLAG_WITH_COUNTERS {
		t.Error("expected CadtFlags to be IPSET_FLAG_WITH_COMMENT and IPSET_FLAG_WITH_COUNTERS")
	}
	if len(msg.Entries) != 2 {
		t.Fatalf("expected 2 Entries, got %d", len(msg.Entries))
	}

	// first entry
	ent := msg.Entries[0]
	if int(*ent.Timeout) != 3577 {
		t.Errorf("expected Timeout for first entry to equal 3577, got %d", *ent.Timeout)
	}
	if int(*ent.Bytes) != 4121 {
		t.Errorf("expected Bytes for first entry to equal 4121, got %d", *ent.Bytes)
	}
	if int(*ent.Packets) != 42 {
		t.Errorf("expected Packets for first entry to equal 42, got %d", *ent.Packets)
	}
	if ent.Comment != "foo bar" {
		t.Errorf("unexpected Comment for first entry: %q", ent.Comment)
	}
	expectedMAC := net.HardwareAddr{0xde, 0xad, 0x0, 0x0, 0xbe, 0xef}
	if !bytes.Equal(ent.MAC, expectedMAC) {
		t.Errorf("expected MAC for first entry to be %s, got %s", expectedMAC.String(), ent.MAC.String())
	}

	// second entry
	ent = msg.Entries[1]
	expectedMAC = net.HardwareAddr{0x1, 0x2, 0x3, 0x0, 0x1, 0x2}
	if !bytes.Equal(ent.MAC, expectedMAC) {
		t.Errorf("expected MAC for second entry to be %s, got %s", expectedMAC.String(), ent.MAC.String())
	}
}

func TestIpsetCreateListAddDelDestroy(t *testing.T) {
	tearDown := setUpNetlinkTest(t)
	defer tearDown()
	timeout := uint32(3)
	err := IpsetCreate("my-test-ipset-1", "hash:ip", IpsetCreateOptions{
		Replace:  true,
		Timeout:  &timeout,
		Counters: true,
		Comments: true,
		Skbinfo:  false,
	})
	if err != nil {
		t.Fatal(err)
	}

	err = IpsetCreate("my-test-ipset-2", "hash:net", IpsetCreateOptions{
		Replace:  true,
		Timeout:  &timeout,
		Counters: false,
		Comments: true,
		Skbinfo:  true,
	})
	if err != nil {
		t.Fatal(err)
	}

	results, err := IpsetListAll()

	if err != nil {
		t.Fatal(err)
	}

	if len(results) != 2 {
		t.Fatalf("expected 2 IPSets to be created, got %d", len(results))
	}

	if results[0].SetName != "my-test-ipset-1" {
		t.Errorf("expected name to be 'my-test-ipset-1', but got '%s'", results[0].SetName)
	}

	if results[1].SetName != "my-test-ipset-2" {
		t.Errorf("expected name to be 'my-test-ipset-2', but got '%s'", results[1].SetName)
	}

	if results[0].TypeName != "hash:ip" {
		t.Errorf("expected type to be 'hash:ip', but got '%s'", results[0].TypeName)
	}

	if results[1].TypeName != "hash:net" {
		t.Errorf("expected type to be 'hash:net', but got '%s'", results[1].TypeName)
	}

	if *results[0].Timeout != 3 {
		t.Errorf("expected timeout to be 3, but got '%d'", *results[0].Timeout)
	}

	ip := net.ParseIP("10.99.99.99")
	exist, err := IpsetTest("my-test-ipset-1", &IPSetEntry{
		IP: ip,
	})
	if err != nil {
		t.Fatal(err)
	}
	if exist {
		t.Errorf("entry should not exist before being added: %s", ip.String())
	}

	err = IpsetAdd("my-test-ipset-1", &IPSetEntry{
		Comment: "test comment",
		IP:      ip,
		Replace: false,
	})

	if err != nil {
		t.Fatal(err)
	}

	exist, err = IpsetTest("my-test-ipset-1", &IPSetEntry{
		IP: ip,
	})
	if err != nil {
		t.Fatal(err)
	}
	if !exist {
		t.Errorf("entry should exist after being added: %s", ip.String())
	}

	result, err := IpsetList("my-test-ipset-1")

	if err != nil {
		t.Fatal(err)
	}

	if len(result.Entries) != 1 {
		t.Fatalf("expected 1 entry be created, got '%d'", len(result.Entries))
	}
	if result.Entries[0].IP.String() != "10.99.99.99" {
		t.Fatalf("expected entry to be '10.99.99.99', got '%s'", result.Entries[0].IP.String())
	}

	if result.Entries[0].Comment != "test comment" {
		// This is only supported in the kernel module from revision 2 or 4, so comments may be ignored.
		t.Logf("expected comment to be 'test comment', got '%s'", result.Entries[0].Comment)
	}

	err = IpsetDel("my-test-ipset-1", &IPSetEntry{
		Comment: "test comment",
		IP:      net.ParseIP("10.99.99.99"),
	})
	if err != nil {
		t.Fatal(err)
	}

	result, err = IpsetList("my-test-ipset-1")
	if err != nil {
		t.Fatal(err)
	}

	if len(result.Entries) != 0 {
		t.Fatalf("expected 0 entries to exist, got %d", len(result.Entries))
	}

	err = IpsetDestroy("my-test-ipset-1")
	if err != nil {
		t.Fatal(err)
	}

	err = IpsetDestroy("my-test-ipset-2")
	if err != nil {
		t.Fatal(err)
	}
}

func TestIpsetCreateListAddDelDestroyWithTestCases(t *testing.T) {
	timeout := uint32(3)
	protocalTCP := uint8(unix.IPPROTO_TCP)
	port := uint16(80)

	testCases := []struct {
		desc     string
		setname  string
		typename string
		options  IpsetCreateOptions
		entry    *IPSetEntry
	}{
		{
			desc:     "Type-hash:ip",
			setname:  "my-test-ipset-1",
			typename: "hash:ip",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: true,
				Comments: true,
				Skbinfo:  false,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("10.99.99.99"),
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:net",
			setname:  "my-test-ipset-2",
			typename: "hash:net",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: false,
				Comments: true,
				Skbinfo:  true,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("10.99.99.0"),
				CIDR:    24,
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:net,net",
			setname:  "my-test-ipset-4",
			typename: "hash:net,net",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: false,
				Comments: true,
				Skbinfo:  true,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("10.99.99.0"),
				CIDR:    24,
				IP2:     net.ParseIP("10.99.0.0"),
				CIDR2:   24,
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:ip,ip",
			setname:  "my-test-ipset-5",
			typename: "hash:net,net",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: false,
				Comments: true,
				Skbinfo:  true,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("10.99.99.0"),
				IP2:     net.ParseIP("10.99.0.0"),
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:ip,port",
			setname:  "my-test-ipset-6",
			typename: "hash:ip,port",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: false,
				Comments: true,
				Skbinfo:  true,
			},
			entry: &IPSetEntry{
				Comment:  "test comment",
				IP:       net.ParseIP("10.99.99.1"),
				Protocol: &protocalTCP,
				Port:     &port,
				Replace:  false,
			},
		},
		{
			desc:     "Type-hash:net,port,net",
			setname:  "my-test-ipset-7",
			typename: "hash:net,port,net",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: false,
				Comments: true,
				Skbinfo:  true,
			},
			entry: &IPSetEntry{
				Comment:  "test comment",
				IP:       net.ParseIP("10.99.99.0"),
				CIDR:     24,
				IP2:      net.ParseIP("10.99.0.0"),
				CIDR2:    24,
				Protocol: &protocalTCP,
				Port:     &port,
				Replace:  false,
			},
		},
		{
			desc:     "Type-hash:mac",
			setname:  "my-test-ipset-8",
			typename: "hash:mac",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: true,
				Comments: true,
				Skbinfo:  false,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				MAC:     net.HardwareAddr{0x26, 0x6f, 0x0d, 0x5b, 0xc1, 0x9d},
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:net,iface",
			setname:  "my-test-ipset-9",
			typename: "hash:net,iface",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: true,
				Comments: true,
				Skbinfo:  false,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("10.99.99.0"),
				CIDR:    24,
				IFace:   "eth0",
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:ip,mark",
			setname:  "my-test-ipset-10",
			typename: "hash:ip,mark",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: true,
				Comments: true,
				Skbinfo:  false,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("10.99.99.0"),
				Mark:    &timeout,
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:net6",
			setname:  "my-test-ipset-11",
			typename: "hash:net",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: false,
				Comments: true,
				Skbinfo:  true,
				Family:   unix.AF_INET6,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("::1"),
				CIDR:    128,
				Replace: false,
			},
		},
		{
			desc:     "Type-hash:net6:net6",
			setname:  "my-test-ipset-11",
			typename: "hash:net,net",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: false,
				Comments: true,
				Skbinfo:  true,
				Family:   unix.AF_INET6,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("::1"),
				CIDR:    128,
				IP2:     net.ParseIP("::2"),
				CIDR2:   128,
				Replace: false,
			},
		},
	}

	for _, tC := range testCases {
		t.Run(tC.desc, func(t *testing.T) {
			tearDown := setUpNetlinkTest(t)
			defer tearDown()

			err := IpsetCreate(tC.setname, tC.typename, tC.options)
			if err != nil {
				t.Fatal(err)
			}

			result, err := IpsetList(tC.setname)
			if err != nil {
				t.Fatal(err)
			}

			if result.SetName != tC.setname {
				t.Errorf("expected name to be '%s', but got '%s'", tC.setname, result.SetName)
			}

			if result.TypeName != tC.typename {
				t.Errorf("expected type to be '%s', but got '%s'", tC.typename, result.TypeName)
			}

			if *result.Timeout != timeout {
				t.Errorf("expected timeout to be %d, but got '%d'", timeout, *result.Timeout)
			}

			err = IpsetAdd(tC.setname, tC.entry)

			if err != nil {
				t.Error(result.Protocol, result.Family)
				t.Fatal(err)
			}

			exist, err := IpsetTest(tC.setname, tC.entry)
			if err != nil {
				t.Fatal(err)
			}
			if !exist {
				t.Errorf("entry should exist, but 'test' got false, case: %s", tC.desc)
			}

			result, err = IpsetList(tC.setname)

			if err != nil {
				t.Fatal(err)
			}

			if len(result.Entries) != 1 {
				t.Fatalf("expected 1 entry be created, got '%d'", len(result.Entries))
			}

			if tC.entry.IP != nil {
				if !tC.entry.IP.Equal(result.Entries[0].IP) {
					t.Fatalf("expected entry to be '%v', got '%v'", tC.entry.IP, result.Entries[0].IP)
				}
			}

			if tC.entry.CIDR > 0 {
				if result.Entries[0].CIDR != tC.entry.CIDR {
					t.Fatalf("expected cidr to be '%d', got '%d'", tC.entry.CIDR, result.Entries[0].CIDR)
				}
			}

			if tC.entry.IP2 != nil {
				if !tC.entry.IP2.Equal(result.Entries[0].IP2) {
					t.Fatalf("expected entry.ip2 to be '%v', got '%v'", tC.entry.IP2, result.Entries[0].IP2)
				}
			}

			if tC.entry.CIDR2 > 0 {
				if result.Entries[0].CIDR2 != tC.entry.CIDR2 {
					t.Fatalf("expected cidr2 to be '%d', got '%d'", tC.entry.CIDR2, result.Entries[0].CIDR2)
				}
			}

			if tC.entry.Port != nil {
				if *result.Entries[0].Protocol != *tC.entry.Protocol {
					t.Fatalf("expected protocol to be '%d', got '%d'", *tC.entry.Protocol, *result.Entries[0].Protocol)
				}
				if *result.Entries[0].Port != *tC.entry.Port {
					t.Fatalf("expected port to be '%d', got '%d'", *tC.entry.Port, *result.Entries[0].Port)
				}
			}

			if tC.entry.MAC != nil {
				if result.Entries[0].MAC.String() != tC.entry.MAC.String() {
					t.Fatalf("expected mac to be '%v', got '%v'", tC.entry.MAC, result.Entries[0].MAC)
				}
			}

			if tC.entry.IFace != "" {
				if result.Entries[0].IFace != tC.entry.IFace {
					t.Fatalf("expected iface to be '%v', got '%v'", tC.entry.IFace, result.Entries[0].IFace)
				}
			}

			if tC.entry.Mark != nil {
				if *result.Entries[0].Mark != *tC.entry.Mark {
					t.Fatalf("expected mark to be '%v', got '%v'", *tC.entry.Mark, *result.Entries[0].Mark)
				}
			}

			if result.Entries[0].Comment != tC.entry.Comment {
				// This is only supported in the kernel module from revision 2 or 4, so comments may be ignored.
				t.Logf("expected comment to be '%s', got '%s'", tC.entry.Comment, result.Entries[0].Comment)
			}

			err = IpsetDel(tC.setname, tC.entry)
			if err != nil {
				t.Fatal(err)
			}

			exist, err = IpsetTest(tC.setname, tC.entry)
			if err != nil {
				t.Fatal(err)
			}
			if exist {
				t.Errorf("entry should be deleted, but 'test' got true, case: %s", tC.desc)
			}

			result, err = IpsetList(tC.setname)
			if err != nil {
				t.Fatal(err)
			}

			if len(result.Entries) != 0 {
				t.Fatalf("expected 0 entries to exist, got %d", len(result.Entries))
			}

			err = IpsetDestroy(tC.setname)
			if err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestIpsetBitmapCreateListWithTestCases(t *testing.T) {
	timeout := uint32(3)

	testCases := []struct {
		desc     string
		setname  string
		typename string
		options  IpsetCreateOptions
		entry    *IPSetEntry
	}{
		{
			desc:     "Type-bitmap:port",
			setname:  "my-test-ipset-11",
			typename: "bitmap:port",
			options: IpsetCreateOptions{
				Replace:  true,
				Timeout:  &timeout,
				Counters: true,
				Comments: false,
				Skbinfo:  false,
				PortFrom: 100,
				PortTo:   600,
			},
			entry: &IPSetEntry{
				Comment: "test comment",
				IP:      net.ParseIP("10.99.99.0"),
				CIDR:    26,
				Mark:    &timeout,
				Replace: false,
			},
		},
	}

	for _, tC := range testCases {
		t.Run(tC.desc, func(t *testing.T) {
			tearDown := setUpNetlinkTest(t)
			defer tearDown()

			err := IpsetCreate(tC.setname, tC.typename, tC.options)
			if err != nil {
				t.Fatal(err)
			}

			result, err := IpsetList(tC.setname)
			if err != nil {
				t.Fatal(err)
			}

			if tC.typename == "bitmap:port" {
				if result.PortFrom != tC.options.PortFrom || result.PortTo != tC.options.PortTo {
					t.Fatalf("expected port range %d-%d, got %d-%d", tC.options.PortFrom, tC.options.PortTo, result.PortFrom, result.PortTo)
				}
			} else if tC.typename == "bitmap:ip" {
				if result.IPFrom == nil || result.IPTo == nil || result.IPFrom.Equal(tC.options.IPFrom) || result.IPTo.Equal(tC.options.IPTo) {
					t.Fatalf("expected ip range %v-%v, got %v-%v", tC.options.IPFrom, tC.options.IPTo, result.IPFrom, result.IPTo)
				}
			}

		})
	}
}

func TestIpsetSwap(t *testing.T) {
	tearDown := setUpNetlinkTest(t)
	defer tearDown()

	ipset1 := "my-test-ipset-swap-1"
	ipset2 := "my-test-ipset-swap-2"

	err := IpsetCreate(ipset1, "hash:ip", IpsetCreateOptions{
		Replace: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_ = IpsetDestroy(ipset1)
	}()

	err = IpsetCreate(ipset2, "hash:ip", IpsetCreateOptions{
		Replace: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_ = IpsetDestroy(ipset2)
	}()

	err = IpsetAdd(ipset1, &IPSetEntry{
		IP: net.ParseIP("10.99.99.99"),
	})
	if err != nil {
		t.Fatal(err)
	}

	assertHasOneEntry := func(name string) {
		result, err := IpsetList(name)
		if err != nil {
			t.Fatal(err)
		}
		if len(result.Entries) != 1 {
			t.Fatalf("expected 1 entry be created, got '%d'", len(result.Entries))
		}
		if result.Entries[0].IP.String() != "10.99.99.99" {
			t.Fatalf("expected entry to be '10.99.99.99', got '%s'", result.Entries[0].IP.String())
		}
	}

	assertIsEmpty := func(name string) {
		result, err := IpsetList(name)
		if err != nil {
			t.Fatal(err)
		}
		if len(result.Entries) != 0 {
			t.Fatalf("expected 0 entry be created, got '%d'", len(result.Entries))
		}
	}

	assertHasOneEntry(ipset1)
	assertIsEmpty(ipset2)

	err = IpsetSwap(ipset1, ipset2)
	if err != nil {
		t.Fatal(err)
	}

	assertIsEmpty(ipset1)
	assertHasOneEntry(ipset2)
}

func nextIP(ip net.IP) {
	for j := len(ip) - 1; j >= 0; j-- {
		ip[j]++
		if ip[j] > 0 {
			break
		}
	}
}

// TestIpsetMaxElements tests that we can create an ipset containing
// 128k elements, which is double the default size (64k elements).
func TestIpsetMaxElements(t *testing.T) {
	tearDown := setUpNetlinkTest(t)
	defer tearDown()

	ipsetName := "my-test-ipset-max"
	maxElements := uint32(128 << 10)

	err := IpsetCreate(ipsetName, "hash:ip", IpsetCreateOptions{
		Replace:     true,
		MaxElements: maxElements,
	})
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		_ = IpsetDestroy(ipsetName)
	}()

	ip := net.ParseIP("10.0.0.0")
	for i := uint32(0); i < maxElements; i++ {
		err = IpsetAdd(ipsetName, &IPSetEntry{
			IP: ip,
		})
		if err != nil {
			t.Fatal(err)
		}
		nextIP(ip)
	}

	result, err := IpsetList(ipsetName)
	if err != nil {
		t.Fatal(err)
	}
	if len(result.Entries) != int(maxElements) {
		t.Fatalf("expected '%d' entry be created, got '%d'", maxElements, len(result.Entries))
	}
}

func TestIpsetDefaultRevision(t *testing.T) {
	testCases := []struct {
		desc             string
		typename         string
		options          IpsetCreateOptions
		expectedRevision uint8
	}{
		{
			desc:     "Type-hash:ip,port",
			typename: "hash:ip,port",
			options: IpsetCreateOptions{
				Counters: true,
				Comments: true,
				Skbinfo:  false,
			},
			expectedRevision: 3,
		},
		{
			desc:     "Type-hash:ip,port_nocomment",
			typename: "hash:ip,port",
			options: IpsetCreateOptions{
				Counters: true,
				Comments: false,
				Skbinfo:  false,
			},
			expectedRevision: 2,
		},
		{
			desc:     "Type-hash:ip,port_skbinfo",
			typename: "hash:ip,port",
			options: IpsetCreateOptions{
				Counters: true,
				Comments: false,
				Skbinfo:  true,
			},
			expectedRevision: 5,
		},
		{
			desc:     "Type-hash:ip,port,net",
			typename: "hash:ip,port,net",
			options: IpsetCreateOptions{
				Counters: true,
				Comments: false,
				Skbinfo:  true,
			},
			expectedRevision: 7,
		},
		{
			desc:     "Type-hash:net,port_baseline_revision_no_opts",
			typename: "hash:net,port",
			options: IpsetCreateOptions{
				Counters: false,
				Comments: false,
				Skbinfo:  false,
			},
			expectedRevision: 2,
		},
	}

	for _, tC := range testCases {
		t.Run(tC.desc, func(t *testing.T) {

			cadtFlags := optionsToBitflag(tC.options)

			defRev := getIpsetDefaultRevision(tC.typename, cadtFlags)

			if defRev != tC.expectedRevision {
				t.Fatalf("expected default revision of '%d', got '%d'", tC.expectedRevision, defRev)
			}
		})
	}
}
