package client

import (
	"net"
	"net/netip"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// newIDIndex is a helper function that returns a client index filled with
// persistent clients from the m.  It also generates a UID for each client.
func newIDIndex(m []*Persistent) (ci *Index) {
	ci = NewIndex()

	for _, c := range m {
		c.UID = MustNewUID()
		ci.Add(c)
	}

	return ci
}

func TestClientIndex_Find(t *testing.T) {
	const (
		cliIPNone = "1.2.3.4"
		cliIP1    = "1.1.1.1"
		cliIP2    = "2.2.2.2"

		cliIPv6 = "1:2:3::4"

		cliSubnet   = "2.2.2.0/24"
		cliSubnetIP = "2.2.2.222"

		cliID  = "client-id"
		cliMAC = "11:11:11:11:11:11"

		linkLocalIP     = "fe80::abcd:abcd:abcd:ab%eth0"
		linkLocalSubnet = "fe80::/16"
	)

	var (
		clientWithBothFams = &Persistent{
			Name: "client1",
			IPs: []netip.Addr{
				netip.MustParseAddr(cliIP1),
				netip.MustParseAddr(cliIPv6),
			},
		}

		clientWithSubnet = &Persistent{
			Name:    "client2",
			IPs:     []netip.Addr{netip.MustParseAddr(cliIP2)},
			Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
		}

		clientWithMAC = &Persistent{
			Name: "client_with_mac",
			MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
		}

		clientWithID = &Persistent{
			Name:      "client_with_id",
			ClientIDs: []string{cliID},
		}

		clientLinkLocal = &Persistent{
			Name:    "client_link_local",
			Subnets: []netip.Prefix{netip.MustParsePrefix(linkLocalSubnet)},
		}
	)

	clients := []*Persistent{
		clientWithBothFams,
		clientWithSubnet,
		clientWithMAC,
		clientWithID,
		clientLinkLocal,
	}
	ci := newIDIndex(clients)

	testCases := []struct {
		want *Persistent
		name string
		ids  []string
	}{{
		name: "ipv4_ipv6",
		ids:  []string{cliIP1, cliIPv6},
		want: clientWithBothFams,
	}, {
		name: "ipv4_subnet",
		ids:  []string{cliIP2, cliSubnetIP},
		want: clientWithSubnet,
	}, {
		name: "mac",
		ids:  []string{cliMAC},
		want: clientWithMAC,
	}, {
		name: "client_id",
		ids:  []string{cliID},
		want: clientWithID,
	}, {
		name: "client_link_local_subnet",
		ids:  []string{linkLocalIP},
		want: clientLinkLocal,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			for _, id := range tc.ids {
				c, ok := ci.Find(id)
				require.True(t, ok)

				assert.Equal(t, tc.want, c)
			}
		})
	}

	t.Run("not_found", func(t *testing.T) {
		_, ok := ci.Find(cliIPNone)
		assert.False(t, ok)
	})
}

func TestClientIndex_Clashes(t *testing.T) {
	const (
		cliIP1      = "1.1.1.1"
		cliSubnet   = "2.2.2.0/24"
		cliSubnetIP = "2.2.2.222"
		cliID       = "client-id"
		cliMAC      = "11:11:11:11:11:11"
	)

	clients := []*Persistent{{
		Name: "client_with_ip",
		IPs:  []netip.Addr{netip.MustParseAddr(cliIP1)},
	}, {
		Name:    "client_with_subnet",
		Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
	}, {
		Name: "client_with_mac",
		MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
	}, {
		Name:      "client_with_id",
		ClientIDs: []string{cliID},
	}}

	ci := newIDIndex(clients)

	testCases := []struct {
		client *Persistent
		name   string
	}{{
		name:   "ipv4",
		client: clients[0],
	}, {
		name:   "subnet",
		client: clients[1],
	}, {
		name:   "mac",
		client: clients[2],
	}, {
		name:   "client_id",
		client: clients[3],
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			clone := tc.client.ShallowClone()
			clone.UID = MustNewUID()

			err := ci.Clashes(clone)
			require.Error(t, err)

			ci.Delete(tc.client)
			err = ci.Clashes(clone)
			require.NoError(t, err)
		})
	}
}

// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
// error.
func mustParseMAC(s string) (mac net.HardwareAddr) {
	mac, err := net.ParseMAC(s)
	if err != nil {
		panic(err)
	}

	return mac
}

func TestMACToKey(t *testing.T) {
	testCases := []struct {
		want any
		name string
		in   string
	}{{
		name: "column6",
		in:   "00:00:5e:00:53:01",
		want: [6]byte(mustParseMAC("00:00:5e:00:53:01")),
	}, {
		name: "column8",
		in:   "02:00:5e:10:00:00:00:01",
		want: [8]byte(mustParseMAC("02:00:5e:10:00:00:00:01")),
	}, {
		name: "column20",
		in:   "00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01",
		want: [20]byte(mustParseMAC("00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01")),
	}, {
		name: "hyphen6",
		in:   "00-00-5e-00-53-01",
		want: [6]byte(mustParseMAC("00-00-5e-00-53-01")),
	}, {
		name: "hyphen8",
		in:   "02-00-5e-10-00-00-00-01",
		want: [8]byte(mustParseMAC("02-00-5e-10-00-00-00-01")),
	}, {
		name: "hyphen20",
		in:   "00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01",
		want: [20]byte(mustParseMAC("00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01")),
	}, {
		name: "dot6",
		in:   "0000.5e00.5301",
		want: [6]byte(mustParseMAC("0000.5e00.5301")),
	}, {
		name: "dot8",
		in:   "0200.5e10.0000.0001",
		want: [8]byte(mustParseMAC("0200.5e10.0000.0001")),
	}, {
		name: "dot20",
		in:   "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001",
		want: [20]byte(mustParseMAC("0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001")),
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mac := mustParseMAC(tc.in)

			key := macToKey(mac)
			assert.Equal(t, tc.want, key)
		})
	}

	assert.Panics(t, func() {
		mac := net.HardwareAddr([]byte{1, 2, 3})
		_ = macToKey(mac)
	})
}

func TestIndex_FindByIPWithoutZone(t *testing.T) {
	var (
		ip         = netip.MustParseAddr("fe80::a098:7654:32ef:ff1")
		ipWithZone = netip.MustParseAddr("fe80::1ff:fe23:4567:890a%eth2")
	)

	var (
		clientNoZone = &Persistent{
			Name: "client",
			IPs:  []netip.Addr{ip},
		}

		clientWithZone = &Persistent{
			Name: "client_with_zone",
			IPs:  []netip.Addr{ipWithZone},
		}
	)

	ci := newIDIndex([]*Persistent{
		clientNoZone,
		clientWithZone,
	})

	testCases := []struct {
		ip   netip.Addr
		want *Persistent
		name string
	}{{
		name: "without_zone",
		ip:   ip,
		want: clientNoZone,
	}, {
		name: "with_zone",
		ip:   ipWithZone,
		want: clientWithZone,
	}, {
		name: "zero_address",
		ip:   netip.Addr{},
		want: nil,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			c := ci.FindByIPWithoutZone(tc.ip.WithZone(""))
			require.Equal(t, tc.want, c)
		})
	}
}

func TestClientIndex_RangeByName(t *testing.T) {
	sortedClients := []*Persistent{{
		Name:      "clientA",
		ClientIDs: []string{"A"},
	}, {
		Name:      "clientB",
		ClientIDs: []string{"B"},
	}, {
		Name:      "clientC",
		ClientIDs: []string{"C"},
	}, {
		Name:      "clientD",
		ClientIDs: []string{"D"},
	}, {
		Name:      "clientE",
		ClientIDs: []string{"E"},
	}}

	testCases := []struct {
		name string
		want []*Persistent
	}{{
		name: "basic",
		want: sortedClients,
	}, {
		name: "nil",
		want: nil,
	}, {
		name: "one_element",
		want: sortedClients[:1],
	}, {
		name: "two_elements",
		want: sortedClients[:2],
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ci := newIDIndex(tc.want)

			var got []*Persistent
			ci.RangeByName(func(c *Persistent) (cont bool) {
				got = append(got, c)

				return true
			})

			assert.Equal(t, tc.want, got)
		})
	}
}
