package dhcpv4

import (
	"bytes"
	"net"
	"strconv"
	"strings"
	"testing"

	"github.com/insomniacslk/dhcp/iana"
	"github.com/stretchr/testify/require"
	"github.com/u-root/uio/uio"
)

func TestGetExternalIPv4Addrs(t *testing.T) {
	addrs4and6 := []net.Addr{
		&net.IPAddr{IP: net.IP{1, 2, 3, 4}},
		&net.IPAddr{IP: net.IP{4, 3, 2, 1}},
		&net.IPNet{IP: net.IP{4, 3, 2, 0}},
		&net.IPAddr{IP: net.IP{1, 2, 3, 4, 1, 1, 1, 1}},
		&net.IPAddr{IP: net.IP{4, 3, 2, 1, 1, 1, 1, 1}},
		&net.IPAddr{},                         // nil IP
		&net.IPAddr{IP: net.IP{127, 0, 0, 1}}, // loopback IP
	}

	expected := []net.IP{
		net.IP{1, 2, 3, 4},
		net.IP{4, 3, 2, 1},
		net.IP{4, 3, 2, 0},
	}
	actual, err := GetExternalIPv4Addrs(addrs4and6)
	require.NoError(t, err)
	require.Equal(t, expected, actual)
}

func TestFromBytes(t *testing.T) {
	data := []byte{
		1,                      // dhcp request
		1,                      // ethernet hw type
		6,                      // hw addr length
		3,                      // hop count
		0xaa, 0xbb, 0xcc, 0xdd, // transaction ID, big endian (network)
		0, 3, // number of seconds
		0, 1, // broadcast
		0, 0, 0, 0, // client IP address
		0, 0, 0, 0, // your IP address
		0, 0, 0, 0, // server IP address
		0, 0, 0, 0, // gateway IP address
		0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding
	}

	// server host name
	expectedHostname := []byte{}
	for i := 0; i < 64; i++ {
		expectedHostname = append(expectedHostname, 0)
	}
	data = append(data, expectedHostname...)
	// boot file name
	expectedBootfilename := []byte{}
	for i := 0; i < 128; i++ {
		expectedBootfilename = append(expectedBootfilename, 0)
	}
	data = append(data, expectedBootfilename...)
	// magic cookie, then no options
	data = append(data, magicCookie[:]...)

	d, err := FromBytes(data)
	require.NoError(t, err)
	require.Equal(t, d.OpCode, OpcodeBootRequest)
	require.Equal(t, d.HWType, iana.HWTypeEthernet)
	require.Equal(t, d.HopCount, byte(3))
	require.Equal(t, d.TransactionID, TransactionID{0xaa, 0xbb, 0xcc, 0xdd})
	require.Equal(t, d.NumSeconds, uint16(3))
	require.Equal(t, d.Flags, uint16(1))
	require.True(t, d.ClientIPAddr.Equal(net.IPv4zero))
	require.True(t, d.YourIPAddr.Equal(net.IPv4zero))
	require.True(t, d.GatewayIPAddr.Equal(net.IPv4zero))
	require.Equal(t, d.ClientHWAddr, net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
	require.Equal(t, d.ServerHostName, "")
	require.Equal(t, d.BootFileName, "")
	// no need to check Magic Cookie as it is already validated in FromBytes
	// above
}

func TestFromBytesGenericInvalid(t *testing.T) {
	data := [][]byte{
		{},
		{1, 1, 6, 0},
	}
	t.Parallel()
	for i, packet := range data {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			_, err := FromBytes(packet)
			require.Error(t, err)
		})
	}
}

func TestFromBytesInvalidOptions(t *testing.T) {
	data := []byte{
		1,                      // dhcp request
		1,                      // ethernet hw type
		6,                      // hw addr length
		0,                      // hop count
		0xaa, 0xbb, 0xcc, 0xdd, // transaction ID
		3, 0, // number of seconds
		1, 0, // broadcast
		0, 0, 0, 0, // client IP address
		0, 0, 0, 0, // your IP address
		0, 0, 0, 0, // server IP address
		0, 0, 0, 0, // gateway IP address
		0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding
	}
	// server host name
	for i := 0; i < 64; i++ {
		data = append(data, 0)
	}
	// boot file name
	for i := 0; i < 128; i++ {
		data = append(data, 0)
	}
	// invalid magic cookie, forcing option parsing to fail
	data = append(data, []byte{99, 130, 83, 98}...)
	_, err := FromBytes(data)
	require.Error(t, err)
}

func TestToStringMethods(t *testing.T) {
	d, err := New()
	if err != nil {
		t.Fatal(err)
	}

	// FlagsToString
	d.SetUnicast()
	require.Equal(t, "Unicast", d.FlagsToString())
	d.SetBroadcast()
	require.Equal(t, "Broadcast", d.FlagsToString())
	d.Flags = 0xffff
	require.Equal(t, "Broadcast (reserved bits not zeroed)", d.FlagsToString())
}

func TestNewToBytes(t *testing.T) {
	// the following bytes match what dhcpv4.New would create. Keep them in
	// sync!
	expected := []byte{
		1,                      // Opcode BootRequest
		1,                      // HwType Ethernet
		6,                      // HwAddrLen
		0,                      // HopCount
		0x11, 0x22, 0x33, 0x44, // TransactionID
		0, 0, // NumSeconds
		0, 0, // Flags
		0, 0, 0, 0, // ClientIPAddr
		0, 0, 0, 0, // YourIPAddr
		0, 0, 0, 0, // ServerIPAddr
		0, 0, 0, 0, // GatewayIPAddr
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ClientHwAddr
	}
	// ServerHostName
	expected = append(expected, bytes.Repeat([]byte{0}, 64)...)
	// BootFileName
	expected = append(expected, bytes.Repeat([]byte{0}, 128)...)

	// Magic Cookie
	expected = append(expected, magicCookie[:]...)

	// End
	expected = append(expected, 0xff)

	// Minimum message length padding.
	//
	// 236 + 4 byte cookie + 1 byte end + 59 bytes padding.
	expected = append(expected, bytes.Repeat([]byte{0}, 59)...)

	d, err := New()
	require.NoError(t, err)
	// fix TransactionID to match the expected one, since it's randomly
	// generated in New()
	d.TransactionID = TransactionID{0x11, 0x22, 0x33, 0x44}
	got := d.ToBytes()
	require.Equal(t, expected, got)
}

func TestToBytesStringTooLong(t *testing.T) {
	d, err := New()
	if err != nil {
		t.Fatal(err)
	}
	d.ServerHostName = strings.Repeat("a", 256)
	d.BootFileName = strings.Repeat("a", 256)

	require.NotPanics(t, func() { _ = d.ToBytes() })
}

func TestGetOption(t *testing.T) {
	d, err := New()
	if err != nil {
		t.Fatal(err)
	}

	hostnameOpt := OptGeneric(OptionHostName, []byte("darkstar"))
	bootFileOpt := OptBootFileName("boot.img")
	d.UpdateOption(hostnameOpt)
	d.UpdateOption(bootFileOpt)

	require.Equal(t, d.GetOneOption(OptionHostName), []byte("darkstar"))
	require.Equal(t, d.GetOneOption(OptionBootfileName), []byte("boot.img"))
	require.Equal(t, d.GetOneOption(OptionRouter), []byte(nil))
}

func TestUpdateOption(t *testing.T) {
	d, err := New()
	require.NoError(t, err)

	hostnameOpt := OptGeneric(OptionHostName, []byte("darkstar"))
	bootFileOpt1 := OptBootFileName("boot.img")
	bootFileOpt2 := OptBootFileName("boot2.img")
	d.UpdateOption(hostnameOpt)
	d.UpdateOption(bootFileOpt1)
	d.UpdateOption(bootFileOpt2)

	options := d.Options
	require.Equal(t, len(options), 2)
	require.Equal(t, d.GetOneOption(OptionHostName), []byte("darkstar"))
	require.Equal(t, d.GetOneOption(OptionBootfileName), []byte("boot2.img"))
}

func TestDHCPv4NewRequestFromOffer(t *testing.T) {
	offer, err := New()
	require.NoError(t, err)
	offer.SetBroadcast()
	offer.UpdateOption(OptMessageType(MessageTypeOffer))
	offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1)))

	// Broadcast request
	var req *DHCPv4
	req, err = NewRequestFromOffer(offer)
	require.NoError(t, err)
	require.Equal(t, MessageTypeRequest, req.MessageType())
	require.False(t, req.IsUnicast())
	require.True(t, req.IsBroadcast())
	// Following options are standard in other dhcp clients (isc-dhclient/udhcp) and required
	// for final ACK to have all info for a proper lease setup (like used in u-root/pkgs/dhclient).
	require.True(t, req.IsOptionRequested(OptionRouter))
	require.True(t, req.IsOptionRequested(OptionSubnetMask))
	require.True(t, req.IsOptionRequested(OptionDomainName))
	require.True(t, req.IsOptionRequested(OptionDomainNameServer))

	// Unicast request
	offer.SetUnicast()
	req, err = NewRequestFromOffer(offer)
	require.NoError(t, err)
	require.True(t, req.IsUnicast())
	require.False(t, req.IsBroadcast())
}

func TestDHCPv4NewRequestFromOfferWithModifier(t *testing.T) {
	offer, err := New()
	require.NoError(t, err)
	offer.UpdateOption(OptMessageType(MessageTypeOffer))
	offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1)))
	userClass := WithUserClass("linuxboot", false)
	req, err := NewRequestFromOffer(offer, userClass)
	require.NoError(t, err)
	require.Equal(t, MessageTypeRequest, req.MessageType())
}

func TestDHCPv4NewRenewFromAck(t *testing.T) {
	ack, err := New()
	require.NoError(t, err)
	ack.SetBroadcast()
	ack.UpdateOption(OptMessageType(MessageTypeAck))
	ack.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1)))
	ack.YourIPAddr = net.IPv4(192, 168, 0, 1)

	var req *DHCPv4
	req, err = NewRenewFromAck(ack)
	require.NoError(t, err)
	require.Equal(t, MessageTypeRequest, req.MessageType())
	require.Nil(t, req.GetOneOption(OptionServerIdentifier))
	require.Nil(t, req.GetOneOption(OptionRequestedIPAddress))
	require.Equal(t, ack.YourIPAddr, req.ClientIPAddr)
	// RFC 2131: RENEW-style requests will be unicast
	require.True(t, req.IsUnicast())
	require.False(t, req.IsBroadcast())
	// Renewals should behave identically to initial requests regarding requested options
	require.True(t, req.IsOptionRequested(OptionRouter))
	require.True(t, req.IsOptionRequested(OptionSubnetMask))
	require.True(t, req.IsOptionRequested(OptionDomainName))
	require.True(t, req.IsOptionRequested(OptionDomainNameServer))
}

func TestDHCPv4NewRenewFromAckWithModifier(t *testing.T) {
	ack, err := New()
	require.NoError(t, err)
	ack.UpdateOption(OptMessageType(MessageTypeAck))
	userClass := WithUserClass("linuxboot", false)
	req, err := NewRenewFromAck(ack, userClass)
	require.NoError(t, err)
	require.Equal(t, MessageTypeRequest, req.MessageType())
	require.Contains(t, req.UserClass(), "linuxboot")
}

func TestNewReplyFromRequest(t *testing.T) {
	discover, err := New()
	require.NoError(t, err)
	discover.GatewayIPAddr = net.IPv4(192, 168, 0, 1)
	reply, err := NewReplyFromRequest(discover)
	require.NoError(t, err)
	require.Equal(t, discover.TransactionID, reply.TransactionID)
	require.Equal(t, discover.GatewayIPAddr, reply.GatewayIPAddr)
}

func TestNewReplyFromRequestWithModifier(t *testing.T) {
	discover, err := New()
	require.NoError(t, err)
	discover.GatewayIPAddr = net.IPv4(192, 168, 0, 1)
	userClass := WithUserClass("linuxboot", false)
	reply, err := NewReplyFromRequest(discover, userClass)
	require.NoError(t, err)
	require.Equal(t, discover.TransactionID, reply.TransactionID)
}

func TestDHCPv4MessageTypeNil(t *testing.T) {
	m, err := New()
	require.NoError(t, err)
	require.Equal(t, MessageTypeNone, m.MessageType())
}

func TestNewDiscovery(t *testing.T) {
	hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6}
	m, err := NewDiscovery(hwAddr)
	require.NoError(t, err)
	require.Equal(t, MessageTypeDiscover, m.MessageType())

	// Validate fields of DISCOVER packet.
	require.Equal(t, OpcodeBootRequest, m.OpCode)
	require.Equal(t, iana.HWTypeEthernet, m.HWType)
	require.Equal(t, hwAddr, m.ClientHWAddr)
	require.True(t, m.Options.Has(OptionParameterRequestList))
}

func TestNewInform(t *testing.T) {
	hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6}
	localIP := net.IPv4(10, 10, 11, 11)
	m, err := NewInform(hwAddr, localIP)

	require.NoError(t, err)
	require.Equal(t, OpcodeBootRequest, m.OpCode)
	require.Equal(t, iana.HWTypeEthernet, m.HWType)
	require.Equal(t, hwAddr, m.ClientHWAddr)
	require.Equal(t, MessageTypeInform, m.MessageType())
	require.True(t, m.ClientIPAddr.Equal(localIP))
}

func TestDHCPv4NewInformWithModifier(t *testing.T) {
	hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6}
	localIP := net.IPv4(10, 10, 11, 11)
	serverID := net.IPv4(192, 168, 0, 1)
	m, err := NewInform(hwAddr, localIP, WithOption(OptServerIdentifier(serverID)))

	require.NoError(t, err)
	require.True(t, serverID.Equal(m.ServerIdentifier()))
}

func TestIsOptionRequested(t *testing.T) {
	pkt, err := New()
	require.NoError(t, err)
	require.True(t, pkt.IsOptionRequested(OptionDomainNameServer))

	optprl := OptParameterRequestList(OptionRouter, OptionDomainName)
	pkt.UpdateOption(optprl)
	require.False(t, pkt.IsOptionRequested(OptionDomainNameServer))

	optprl = OptParameterRequestList(OptionDomainNameServer)
	pkt.UpdateOption(optprl)
	require.True(t, pkt.IsOptionRequested(OptionDomainNameServer))
}

func TestIsGenericOptionRequested(t *testing.T) {
	pkt, err := New()
	require.NoError(t, err)

	var genericOption224 = GenericOptionCode(224)
	var genericOption225 = GenericOptionCode(225)
	pkt.UpdateOption(OptParameterRequestList(OptionBroadcastAddress, genericOption224))
	require.True(t, pkt.IsOptionRequested(OptionBroadcastAddress))
	require.False(t, pkt.IsOptionRequested(OptionSwapServer))
	require.True(t, pkt.IsOptionRequested(genericOption224), "Did not detect generic option 224 in <%v>", pkt.ParameterRequestList())
	require.False(t, pkt.IsOptionRequested(genericOption225), "Detected generic option 225 in <%v>", pkt.ParameterRequestList())
}

// TODO
//      test Summary() and String()
func TestSummary(t *testing.T) {
	packet, err := New(WithMessageType(MessageTypeInform))
	packet.TransactionID = [4]byte{1, 1, 1, 1}
	require.NoError(t, err)

	want := "DHCPv4 Message\n" +
		"  opcode: BootRequest\n" +
		"  hwtype: Ethernet\n" +
		"  hopcount: 0\n" +
		"  transaction ID: 0x01010101\n" +
		"  num seconds: 0\n" +
		"  flags: Unicast (0x00)\n" +
		"  client IP: 0.0.0.0\n" +
		"  your IP: 0.0.0.0\n" +
		"  server IP: 0.0.0.0\n" +
		"  gateway IP: 0.0.0.0\n" +
		"  client MAC: 00:00:00:00:00:00\n" +
		"  server hostname: \n" +
		"  bootfile name: \n" +
		"  options:\n" +
		"    DHCP Message Type: INFORM\n"
	require.Equal(t, want, packet.Summary())
}

func Test_withIP(t *testing.T) {
	buff := uio.NewBigEndianBuffer(make([]byte, 0, 20))
	// Converting a string into IP, ip1 will be in 16 byte format
	ip1 := net.ParseIP("10.0.0.1")
	writeIP(buff, ip1)
	b := buff.Buffer
	require.Equal(t, b.Len(), 4, "Testing no of bytes written by writeIP func")
}

func FuzzDHCPv4(f *testing.F) {

	data_0 := []byte{
		1,                      // dhcp request
		1,                      // ethernet hw type
		6,                      // hw addr length
		3,                      // hop count
		0xaa, 0xbb, 0xcc, 0xdd, // transaction ID, big endian (network)
		0, 3, // number of seconds
		0, 1, // broadcast
		0, 0, 0, 0, // client IP address
		0, 0, 0, 0, // your IP address
		0, 0, 0, 0, // server IP address
		0, 0, 0, 0, // gateway IP address
		0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding
	}

	data_1 := []byte{
		1,                      // dhcp request
		1,                      // ethernet hw type
		6,                      // hw addr length
		0,                      // hop count
		0xaa, 0xbb, 0xcc, 0xdd, // transaction ID
		3, 0, // number of seconds
		1, 0, // broadcast
		0, 0, 0, 0, // client IP address
		0, 0, 0, 0, // your IP address
		0, 0, 0, 0, // server IP address
		0, 0, 0, 0, // gateway IP address
		0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding
	}

	f.Add(data_0)
	f.Add(data_1)

	f.Fuzz(func(t *testing.T, data []byte) {
		msg, err := FromBytes(data)
		if err != nil {
			return
		}
		msg.ToBytes()
	})
}
