// Copyright 2018 Google Inc.
//
// 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 dhcp4d

import (
	"encoding/binary"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/krolaw/dhcp4"
)

func messageType(p dhcp4.Packet) dhcp4.MessageType {
	opts := p.ParseOptions()
	return dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0])
}

func newPacket(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts []dhcp4.Option) dhcp4.Packet {
	return dhcp4.RequestPacket(
		mt,
		hwaddr,                         // MAC address
		addr,                           // requested IP address
		[]byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID
		false,                          // broadcast,
		opts,
	)
}

func request(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
	return newPacket(dhcp4.Request, addr, hwaddr, opts)
}

func discover(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
	return newPacket(dhcp4.Discover, addr, hwaddr, opts)
}

func decline(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
	return newPacket(dhcp4.Decline, addr, hwaddr, opts)
}

const goldenInterfaces = `
{
  "interfaces":[
    {
      "hardware_addr": "02:73:53:00:ca:fe",
      "name": "uplink0"
    },
    {
      "hardware_addr": "02:73:53:00:b0:0c",
      "name": "lan0",
      "addr": "192.168.42.1/24"
    }
  ]
}
`

type noopSink struct{}

func (*noopSink) LocalAddr() net.Addr                                { return nil }
func (*noopSink) Close() error                                       { return nil }
func (*noopSink) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (*noopSink) SetDeadline(t time.Time) error                      { return nil }
func (*noopSink) SetReadDeadline(t time.Time) error                  { return nil }
func (*noopSink) SetWriteDeadline(t time.Time) error                 { return nil }
func (*noopSink) ReadFrom(buf []byte) (int, net.Addr, error)         { return 0, nil, nil }

func testHandler(t *testing.T) (_ *Handler, cleanup func()) {
	tmpdir, err := ioutil.TempDir("", "dhcp4dtest")
	if err != nil {
		t.Fatal(err)
	}
	if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil {
		t.Fatal(err)
	}
	handler, err := NewHandler(
		tmpdir,
		&net.Interface{
			HardwareAddr: net.HardwareAddr([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}),
		},
		"lan0",
		&noopSink{},
	)
	if err != nil {
		t.Fatal(err)
	}
	return handler, func() { os.RemoveAll(tmpdir) }
}

func TestLease(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()
	var (
		addr         = net.IP{192, 168, 42, 23}
		hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
		hostname     = "xps"
	)
	leasedCalled := false
	handler.Leases = func(leases []*Lease, latest *Lease) {
		if got, want := len(leases), 1; got != want {
			t.Fatalf("unexpected number of leases: got %d, want %d", got, want)
		}
		l := leases[0]
		if got, want := l.Addr, addr; !got.Equal(want) {
			t.Fatalf("unexpected lease.Addr: got %v, want %v", got, want)
		}
		if got, want := l.HardwareAddr, hardwareAddr.String(); got != want {
			t.Fatalf("unexpected lease.HardwareAddr: got %q, want %q", got, want)
		}
		if got, want := l.Hostname, hostname; got != want {
			t.Fatalf("unexpected lease.Hostname: got %q, want %q", got, want)
		}
		leasedCalled = true
	}
	p := dhcp4.RequestPacket(
		dhcp4.Request,
		hardwareAddr,                   // MAC address
		addr,                           // requested IP address
		[]byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID
		false,                          // broadcast,
		[]dhcp4.Option{
			{
				Code:  dhcp4.OptionHostName,
				Value: []byte(hostname),
			},
		},
	)
	handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if !leasedCalled {
		t.Fatalf("leased callback not called")
	}
}

func TestPreferredAddress(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()

	var (
		addr         = net.IP{192, 168, 42, 23}
		hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
		hostname     = "xps"
	)

	t.Run("no requested IP", func(t *testing.T) {
		p := request(net.IPv4zero, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if got, want := resp.YIAddr().To4(), addr.To4(); got.Equal(want) {
			t.Errorf("DHCPOFFER for wrong IP: got %v, want anything else", got)
		}
	})

	t.Run("requested CIAddr", func(t *testing.T) {
		p := request(addr, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
			t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
		}
	})

	t.Run("requested option", func(t *testing.T) {
		// p := request(net.IPv4zero, hardwareAddr)
		p := dhcp4.RequestPacket(
			dhcp4.Discover,
			hardwareAddr,                   // MAC address
			net.IPv4zero,                   // requested IP address
			[]byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID
			false,                          // broadcast,
			[]dhcp4.Option{
				{
					Code:  dhcp4.OptionHostName,
					Value: []byte(hostname),
				},
				{
					Code:  dhcp4.OptionRequestedIPAddress,
					Value: addr,
				},
			},
		)
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
			t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
		}
	})
}

func TestPoolBoundaries(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()

	var (
		addr         = net.IP{192, 168, 42, 23}
		hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
	)

	for _, last := range []byte{1, 242} {
		addr[len(addr)-1] = last
		p := request(addr, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
		if got, want := messageType(resp), dhcp4.NAK; got != want {
			t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
		}
	}
}

func TestPreviousLease(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()

	var (
		addr1         = net.IP{192, 168, 42, 23}
		addr2         = net.IP{192, 168, 42, 42}
		hardwareAddr1 = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
		hardwareAddr2 = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x77}
	)

	p := request(addr1, hardwareAddr1)
	resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if got, want := resp.YIAddr().To4(), addr1.To4(); !got.Equal(want) {
		t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
	}

	p = request(addr1, hardwareAddr2)
	resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if got, want := messageType(resp), dhcp4.NAK; got != want {
		t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
	}

	p = discover(net.IPv4zero, hardwareAddr1)
	resp = handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
	if got, want := resp.YIAddr().To4(), addr1.To4(); !got.Equal(want) {
		t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
	}

	// Free addr1 by requesting addr2
	p = request(addr2, hardwareAddr1)
	resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if got, want := resp.YIAddr().To4(), addr2.To4(); !got.Equal(want) {
		t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
	}

	// Verify addr1 is now available to other clients
	p = request(addr1, hardwareAddr2)
	resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if got, want := resp.YIAddr().To4(), addr1.To4(); !got.Equal(want) {
		t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
	}
}

func TestPermanentLease(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()
	now := time.Now()
	handler.timeNow = func() time.Time { return now }

	var (
		addr         = net.IP{192, 168, 42, 23}
		hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
	)

	handler.SetLeases([]*Lease{
		{
			Num:          2,
			Addr:         addr,
			HardwareAddr: hardwareAddr.String(),
		},
	})

	p := request(addr, hardwareAddr)
	resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
		t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
	}

	now = now.Add(3 * time.Hour)

	hardwareAddr[len(hardwareAddr)-1] = 0x77

	p = request(addr, hardwareAddr)
	resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if got, want := messageType(resp), dhcp4.NAK; got != want {
		t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
	}
}

func TestExpiration(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()
	now := time.Now()
	handler.timeNow = func() time.Time { return now }

	var (
		addr         = net.IP{192, 168, 42, 23}
		hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
	)

	t.Run("allocate entire pool", func(t *testing.T) {
		// 1 is the DHCP server,
		for i := 1; i < 1+230; i++ {
			addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast)
			hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1]
			p := request(addr, hardwareAddr)
			resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
			if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
				t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
			}
		}
	})

	t.Run("re-allocate", func(t *testing.T) {
		// 1 is the DHCP server,
		for i := 1; i < 1+230; i++ {
			addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast)
			hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1]
			p := request(addr, hardwareAddr)
			resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
			if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
				t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
			}
		}
	})

	t.Run("full", func(t *testing.T) {
		// 1 is the DHCP server,
		for i := 1; i < 1+230; i++ {
			addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast)
			hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1] - 1
			p := request(addr, hardwareAddr)
			resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
			if got, want := messageType(resp), dhcp4.NAK; got != want {
				t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
			}
		}

		hardwareAddr[len(hardwareAddr)-1] = 0
		p := discover(addr, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if resp != nil {
			t.Errorf("DHCPDISCOVER(%v) resulted in unexpected offer of %v", addr, resp.YIAddr())
		}
	})

	now = now.Add(3 * time.Hour)

	t.Run("re-allocate after expiration", func(t *testing.T) {
		// 1 is the DHCP server,
		for i := 1; i < 1+230; i++ {
			addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast)
			p := request(addr, hardwareAddr)
			resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
			if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
				t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
			}
		}
	})
}

func TestRequestExpired(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()
	now := time.Now()
	handler.timeNow = func() time.Time { return now }

	addr := net.IP{192, 168, 42, 23}

	hardwareAddr := map[string]net.HardwareAddr{
		"xps": net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
		"mbp": net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
	}

	t.Run("mbp grabs an address", func(t *testing.T) {
		p := request(addr, hardwareAddr["mbp"])
		resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
		if got, want := messageType(resp), dhcp4.ACK; got != want {
			t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
		}
	})

	now = now.Add(3 * time.Hour)

	t.Run("xps grabs the same address", func(t *testing.T) {
		p := request(addr, hardwareAddr["xps"])
		resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
		if got, want := messageType(resp), dhcp4.ACK; got != want {
			t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
		}
	})

	t.Run("mbp requests its old address", func(t *testing.T) {
		p := request(addr, hardwareAddr["mbp"])
		resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
		if got, want := messageType(resp), dhcp4.NAK; got != want {
			t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
		}
	})

	t.Run("mbp requests any", func(t *testing.T) {
		p := request(addr, hardwareAddr["mbp"])
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if got, want := resp.YIAddr().To4(), addr.To4(); got.Equal(want) {
			t.Errorf("DHCPOFFER for wrong IP: got offered %v (in use!)", got)
		}
	})
}

func TestServerID(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()

	var (
		addr         = net.IP{192, 168, 42, 23}
		hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
	)

	p := request(addr, hardwareAddr, dhcp4.Option{
		Code:  dhcp4.OptionServerIdentifier,
		Value: net.IP{192, 168, 1, 1},
	})
	resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
	if resp != nil {
		t.Errorf("DHCPDISCOVER(%v) resulted in unexpected offer of %v", addr, resp.YIAddr())
	}
}

func TestPersistentStorage(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()

	var (
		addr         = net.IP{192, 168, 42, 4}
		hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
	)

	handler.SetLeases([]*Lease{
		{
			Num:          2,
			Addr:         addr,
			HardwareAddr: hardwareAddr.String(),
		},
	})

	p := request(net.IPv4zero, hardwareAddr)
	resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
	if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
		t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
	}
}

func TestMinimumLeaseTime(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()

	addr := net.IP{192, 168, 42, 23}

	for _, tt := range []struct {
		hwaddr        net.HardwareAddr
		wantLeaseTime time.Duration
	}{
		{
			hwaddr:        net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
			wantLeaseTime: 20 * time.Minute,
		},

		{
			// Nintendo MAC address range for Nintendo Switch specific minimum
			// lease time quirk:
			hwaddr:        net.HardwareAddr{0x7c, 0xbb, 0x8a, 0x11, 0x22, 0x33},
			wantLeaseTime: 1 * time.Hour,
		},
	} {
		p := discover(addr, tt.hwaddr)
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if resp == nil {
			t.Fatalf("DHCPDISCOVER(%v) = nil", tt.hwaddr)
		}
		if got, want := messageType(resp), dhcp4.Offer; got != want {
			t.Errorf("DHCPDISCOVER resulted in unexpected message type: got %v, want %v", got, want)
		}

		opts := resp.ParseOptions()
		leaseTimeBytes, ok := opts[dhcp4.OptionIPAddressLeaseTime]
		if !ok {
			t.Fatalf("DHCPDISCOVER(%v): lease time option not set", tt.hwaddr)
		}
		leaseTimeSecs := binary.BigEndian.Uint32(leaseTimeBytes)
		want := uint32(tt.wantLeaseTime.Seconds())
		if got := leaseTimeSecs; got != want {
			t.Errorf("unexpected lease time for hwaddr %v: got %d, want %d", tt.hwaddr, got, want)
		}
	}
}

func TestClientDecline(t *testing.T) {
	handler, cleanup := testHandler(t)
	defer cleanup()

	now := time.Now()
	handler.timeNow = func() time.Time { return now }

	addr := net.IP{192, 168, 42, 23}

	hardwareAddr := net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}

	// macbook requests a new lease
	t.Run("mbp grabs an address", func(t *testing.T) {
		p := request(addr, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
		if got, want := messageType(resp), dhcp4.ACK; got != want {
			t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
		}
	})

	// macbook lease expires
	now = now.Add(3 * time.Hour)

	t.Run("mbp requests previous address", func(t *testing.T) {
		p := request(addr, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
			t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
		}
		p = request(addr, hardwareAddr)
		resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
		if got, want := messageType(resp), dhcp4.ACK; got != want {
			t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
		}
	})

	t.Run("mbp declines address", func(t *testing.T) {
		p := decline(addr, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Decline, p.ParseOptions())
		if resp != nil {
			t.Fatalf("DHCPDECLINE was unexpectedly answered: %v", messageType(resp))
		}
	})

	t.Run("mbp requests any address", func(t *testing.T) {
		p := request(net.IPv4zero, hardwareAddr)
		resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
		if got, want := resp.YIAddr().To4(), addr.To4(); got.Equal(want) {
			t.Errorf("DHCPOFFER returned unexpected address: %v", got)
		}
	})
}
