// Copyright 2018 the u-root Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build go1.12

package nclient4

import (
	"bytes"
	"context"
	"fmt"
	"net"
	"sync"
	"testing"
	"time"

	"github.com/hugelgupf/socketpair"
	"github.com/insomniacslk/dhcp/dhcpv4"
	"github.com/insomniacslk/dhcp/dhcpv4/server4"
)

type handler struct {
	mu       sync.Mutex
	received []*dhcpv4.DHCPv4

	// Each received packet can have more than one response (in theory,
	// from different servers sending different Advertise, for example).
	responses [][]*dhcpv4.DHCPv4
}

func (h *handler) handle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
	h.mu.Lock()
	defer h.mu.Unlock()

	h.received = append(h.received, m)

	if len(h.responses) > 0 {
		for _, resp := range h.responses[0] {
			_, _ = conn.WriteTo(resp.ToBytes(), peer)
		}
		h.responses = h.responses[1:]
	}
}

func serveAndClient(ctx context.Context, responses [][]*dhcpv4.DHCPv4, opts ...ClientOpt) (*Client, net.PacketConn) {
	// Fake PacketConn connection.
	clientRawConn, serverRawConn, err := socketpair.PacketSocketPair()
	if err != nil {
		panic(err)
	}

	clientConn := NewBroadcastUDPConn(clientRawConn, &net.UDPAddr{Port: ClientPort})
	serverConn := NewBroadcastUDPConn(serverRawConn, &net.UDPAddr{Port: ServerPort})

	o := []ClientOpt{WithRetry(1), WithTimeout(2 * time.Second)}
	o = append(o, opts...)
	mc, err := NewWithConn(clientConn, net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf}, o...)
	if err != nil {
		panic(err)
	}

	h := &handler{responses: responses}
	s, err := server4.NewServer("", nil, h.handle, server4.WithConn(serverConn))
	if err != nil {
		panic(err)
	}
	go func() {
		_ = s.Serve()
	}()

	return mc, serverConn
}

func ComparePacket(got *dhcpv4.DHCPv4, want *dhcpv4.DHCPv4) error {
	if got == nil && got == want {
		return nil
	}
	if (want == nil || got == nil) && (got != want) {
		return fmt.Errorf("packet got %v, want %v", got, want)
	}
	if !bytes.Equal(got.ToBytes(), want.ToBytes()) {
		return fmt.Errorf("packet got %v, want %v", got, want)
	}
	return nil
}

func pktsExpected(got []*dhcpv4.DHCPv4, want []*dhcpv4.DHCPv4) error {
	if len(got) != len(want) {
		return fmt.Errorf("got %d packets, want %d packets", len(got), len(want))
	}

	for i := range got {
		if err := ComparePacket(got[i], want[i]); err != nil {
			return err
		}
	}
	return nil
}

func newPacketWeirdHWAddr(op dhcpv4.OpcodeType, xid dhcpv4.TransactionID) *dhcpv4.DHCPv4 {
	p, err := dhcpv4.New()
	if err != nil {
		panic(fmt.Sprintf("newpacket: %v", err))
	}
	p.OpCode = op
	p.TransactionID = xid
	p.ClientHWAddr = net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 1, 2, 3, 4, 5, 6}
	return p
}

func newPacket(op dhcpv4.OpcodeType, xid dhcpv4.TransactionID) *dhcpv4.DHCPv4 {
	p, err := dhcpv4.New()
	if err != nil {
		panic(fmt.Sprintf("newpacket: %v", err))
	}
	p.OpCode = op
	p.TransactionID = xid
	p.ClientHWAddr = net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf}
	return p
}

func TestSendAndRead(t *testing.T) {
	for _, tt := range []struct {
		desc   string
		send   *dhcpv4.DHCPv4
		server []*dhcpv4.DHCPv4

		// If want is nil, we assume server[0] contains what is wanted.
		want    *dhcpv4.DHCPv4
		wantErr error
	}{
		{
			desc: "two response packets",
			send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}),
			server: []*dhcpv4.DHCPv4{
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
			},
			want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
		},
		{
			desc: "one response packet",
			send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}),
			server: []*dhcpv4.DHCPv4{
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
			},
			want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
		},
		{
			desc: "one response packet, one invalid XID, one invalid opcode, one invalid hwaddr",
			send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}),
			server: []*dhcpv4.DHCPv4{
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x77, 0x33, 0x33, 0x33}),
				newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}),
				newPacketWeirdHWAddr(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
			},
			want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
		},
		{
			desc: "discard wrong XID",
			send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}),
			server: []*dhcpv4.DHCPv4{
				newPacket(dhcpv4.OpcodeBootReply, [4]byte{0, 0, 0, 0}),
			},
			want:    nil, // Explicitly empty.
			wantErr: ErrNoResponse,
		},
		{
			desc:    "no response, timeout",
			send:    newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}),
			wantErr: ErrNoResponse,
		},
	} {
		t.Run(tt.desc, func(t *testing.T) {
			// Both server and client only get 2 seconds.
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()

			mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{tt.server},
				// Use an unbuffered channel to make sure we
				// have no deadlocks.
				withBufferCap(0))
			defer mc.Close()

			rcvd, err := mc.SendAndRead(context.Background(), DefaultServers, tt.send, nil)
			if err != tt.wantErr {
				t.Error(err)
			}

			if err := ComparePacket(rcvd, tt.want); err != nil {
				t.Errorf("got unexpected packets: %v", err)
			}
		})
	}
}

func TestParallelSendAndRead(t *testing.T) {
	pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33})

	// Both the server and client only get 2 seconds.
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{},
		WithTimeout(10*time.Second),
		// Use an unbuffered channel to make sure nothing blocks.
		withBufferCap(0))
	defer mc.Close()

	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse {
			t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse)
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()

		time.Sleep(4 * time.Second)

		if err := mc.Close(); err != nil {
			t.Errorf("closing failed: %v", err)
		}
	}()

	wg.Wait()
}

func TestReuseXID(t *testing.T) {
	pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33})

	// Both the server and client only get 2 seconds.
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{})
	defer mc.Close()

	if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse {
		t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse)
	}

	if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse {
		t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse)
	}
}

func TestSimpleSendAndReadDiscardGarbage(t *testing.T) {
	pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33})

	responses := newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33})

	// Both the server and client only get 2 seconds.
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	mc, udpConn := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{{responses}})
	defer mc.Close()

	// Too short for valid DHCPv4 packet.
	_, _ = udpConn.WriteTo([]byte{0x01}, nil)
	_, _ = udpConn.WriteTo([]byte{0x01, 0x2}, nil)

	rcvd, err := mc.SendAndRead(ctx, DefaultServers, pkt, nil)
	if err != nil {
		t.Errorf("SendAndRead(%v) = %v, want nil", pkt, err)
	}

	if err := ComparePacket(rcvd, responses); err != nil {
		t.Errorf("got unexpected packets: %v", err)
	}
}

func TestMultipleSendAndRead(t *testing.T) {
	for _, tt := range []struct {
		desc    string
		send    []*dhcpv4.DHCPv4
		server  [][]*dhcpv4.DHCPv4
		wantErr []error
	}{
		{
			desc: "two requests, two responses",
			send: []*dhcpv4.DHCPv4{
				newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}),
				newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x44, 0x44, 0x44, 0x44}),
			},
			server: [][]*dhcpv4.DHCPv4{
				[]*dhcpv4.DHCPv4{ // Response for first packet.
					newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}),
				},
				[]*dhcpv4.DHCPv4{ // Response for second packet.
					newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x44, 0x44, 0x44, 0x44}),
				},
			},
			wantErr: []error{
				nil,
				nil,
			},
		},
	} {
		// Both server and client only get 2 seconds.
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()

		mc, _ := serveAndClient(ctx, tt.server)
		defer mc.Close()

		for i, send := range tt.send {
			ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			rcvd, err := mc.SendAndRead(ctx, DefaultServers, send, nil)

			if wantErr := tt.wantErr[i]; err != wantErr {
				t.Errorf("SendAndReadOne(%v): got %v, want %v", send, err, wantErr)
			}
			if err := pktsExpected([]*dhcpv4.DHCPv4{rcvd}, tt.server[i]); err != nil {
				t.Errorf("got unexpected packets: %v", err)
			}
		}
	}
}
