// +build linux

package endpoint

import (
	"net"
	"reflect"
	"testing"
	"time"

	"github.com/weaveworks/tcptracer-bpf/pkg/tracer"

	"github.com/weaveworks/scope/probe/host"
)

func newMockEbpfTracker() *EbpfTracker {
	return &EbpfTracker{
		ready: true,
		dead:  false,

		openConnections: map[ebpfKey]ebpfDetail{},
	}
}

func TestHandleConnection(t *testing.T) {
	var (
		ServerPid  uint32 = 42
		ClientPid  uint32 = 43
		ServerAddr        = [net.IPv4len]byte{127, 0, 0, 1}
		ServerIP          = net.IP(ServerAddr[:])
		ClientAddr        = [net.IPv4len]byte{127, 0, 0, 2}
		ClientIP          = net.IP(ClientAddr[:])
		ServerPort uint16 = 12345
		ClientPort uint16 = 6789
		NetNS      uint32 = 123456789

		IPv4ConnectEvent = tracer.TcpV4{
			CPU:   0,
			Type:  tracer.EventConnect,
			Pid:   ClientPid,
			Comm:  "cmd",
			SAddr: ClientIP,
			DAddr: ServerIP,
			SPort: ClientPort,
			DPort: ServerPort,
			NetNS: NetNS,
		}

		IPv4ConnectEbpfConnection = ebpfDetail{
			incoming: false,
			pid:      ClientPid,
		}

		IPv4ConnectCloseEvent = tracer.TcpV4{
			CPU:   0,
			Type:  tracer.EventClose,
			Pid:   ClientPid,
			Comm:  "cmd",
			SAddr: ClientIP,
			DAddr: ServerIP,
			SPort: ClientPort,
			DPort: ServerPort,
			NetNS: NetNS,
		}

		IPv4AcceptEvent = tracer.TcpV4{
			CPU:   0,
			Type:  tracer.EventAccept,
			Pid:   ServerPid,
			Comm:  "cmd",
			SAddr: ServerIP,
			DAddr: ClientIP,
			SPort: ServerPort,
			DPort: ClientPort,
			NetNS: NetNS,
		}

		IPv4AcceptEbpfConnection = ebpfDetail{
			incoming: true,
			pid:      ServerPid,
		}

		IPv4AcceptCloseEvent = tracer.TcpV4{
			CPU:   0,
			Type:  tracer.EventClose,
			Pid:   ClientPid,
			Comm:  "cmd",
			SAddr: ServerIP,
			DAddr: ClientIP,
			SPort: ServerPort,
			DPort: ClientPort,
			NetNS: NetNS,
		}
	)

	mockEbpfTracker := newMockEbpfTracker()

	tuple := fourTuple{ClientAddr, ServerAddr, IPv4ConnectEvent.SPort, IPv4ConnectEvent.DPort}
	mockEbpfTracker.handleConnection(IPv4ConnectEvent.Type, tuple, int(IPv4ConnectEvent.Pid), NetNS)
	key := makeKey(tuple, NetNS)
	if !reflect.DeepEqual(mockEbpfTracker.openConnections[key], IPv4ConnectEbpfConnection) {
		t.Errorf("Connection mismatch connect event\nTarget connection:%v\nParsed connection:%v",
			IPv4ConnectEbpfConnection, mockEbpfTracker.openConnections[key])
	}

	tuple = fourTuple{ClientAddr, ServerAddr, uint16(IPv4ConnectCloseEvent.SPort), uint16(IPv4ConnectCloseEvent.DPort)}
	mockEbpfTracker.handleConnection(IPv4ConnectCloseEvent.Type, tuple, int(IPv4ConnectCloseEvent.Pid), NetNS)
	key = makeKey(tuple, NetNS)
	if len(mockEbpfTracker.openConnections) != 0 {
		t.Errorf("Connection mismatch close event\nConnection to close:%v",
			mockEbpfTracker.openConnections[key])
	}

	mockEbpfTracker = newMockEbpfTracker()

	tuple = fourTuple{ServerAddr, ClientAddr, uint16(IPv4AcceptEvent.SPort), uint16(IPv4AcceptEvent.DPort)}
	mockEbpfTracker.handleConnection(IPv4AcceptEvent.Type, tuple, int(IPv4AcceptEvent.Pid), NetNS)
	key = makeKey(tuple, NetNS)
	if !reflect.DeepEqual(mockEbpfTracker.openConnections[key], IPv4AcceptEbpfConnection) {
		t.Errorf("Connection mismatch connect event\nTarget connection:%v\nParsed connection:%v",
			IPv4AcceptEbpfConnection, mockEbpfTracker.openConnections[key])
	}

	tuple = fourTuple{ServerAddr, ClientAddr, uint16(IPv4AcceptCloseEvent.SPort), uint16(IPv4AcceptCloseEvent.DPort)}
	mockEbpfTracker.handleConnection(IPv4AcceptCloseEvent.Type, tuple, int(IPv4AcceptCloseEvent.Pid), NetNS)

	if len(mockEbpfTracker.openConnections) != 0 {
		t.Errorf("Connection mismatch close event\nConnection to close:%v",
			mockEbpfTracker.openConnections)
	}
}

func TestWalkConnections(t *testing.T) {
	var (
		cnt         int
		activeKey   = ebpfKey{networkNamespace: 12345}
		inactiveKey = ebpfKey{networkNamespace: 12345}
	)
	mockEbpfTracker := newMockEbpfTracker()
	mockEbpfTracker.openConnections[activeKey] = ebpfDetail{
		pid: 0,
	}
	mockEbpfTracker.closedConnections = append(mockEbpfTracker.closedConnections,
		ebpfClosedConnection{
			key: inactiveKey,
			ebpfDetail: ebpfDetail{
				pid: 0,
			},
		})
	mockEbpfTracker.walkConnections(func(k ebpfKey, e ebpfDetail) {
		cnt++
	})
	if cnt != 2 {
		t.Errorf("walkConnections found %v instead of 2 connections", cnt)
	}
}

func TestInvalidTimeStampDead(t *testing.T) {
	var (
		cnt        int
		ClientPid  uint32 = 43
		ServerIP          = net.ParseIP("127.0.0.1")
		ClientIP          = net.ParseIP("127.0.0.2")
		ServerPort uint16 = 12345
		ClientPort uint16 = 6789
		NetNS      uint32 = 123456789
		event             = tracer.TcpV4{
			CPU:   0,
			Type:  tracer.EventConnect,
			Pid:   ClientPid,
			Comm:  "cmd",
			SAddr: ClientIP,
			DAddr: ServerIP,
			SPort: ClientPort,
			DPort: ServerPort,
			NetNS: NetNS,
		}
	)
	mockEbpfTracker := newMockEbpfTracker()
	event.Timestamp = 0
	mockEbpfTracker.TCPEventV4(event)
	event2 := event
	event2.SPort = 1
	event2.Timestamp = 2
	mockEbpfTracker.TCPEventV4(event2)
	mockEbpfTracker.walkConnections(func(ebpfKey, ebpfDetail) {
		cnt++
	})
	if cnt != 2 {
		t.Errorf("walkConnections found %v instead of 2 connections", cnt)
	}
	if mockEbpfTracker.isDead() {
		t.Errorf("expected ebpfTracker to be alive after events with valid order")
	}
	cnt = 0
	event.Timestamp = 1
	mockEbpfTracker.TCPEventV4(event)
	mockEbpfTracker.walkConnections(func(ebpfKey, ebpfDetail) {
		cnt++
	})
	if cnt != 2 {
		t.Errorf("walkConnections found %v instead of 2 connections", cnt)
	}
	// EbpfTracker is marked as dead asynchronously.
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		if mockEbpfTracker.isDead() {
			break
		}
		time.Sleep(100 * time.Millisecond)
	}
	if !mockEbpfTracker.isDead() {
		t.Errorf("expected ebpfTracker to be set to dead after events with wrong order")
	}
}

func TestIsKernelSupported(t *testing.T) {
	var release, version string
	oldGetKernelReleaseAndVersion := host.GetKernelReleaseAndVersion
	defer func() {
		host.GetKernelReleaseAndVersion = oldGetKernelReleaseAndVersion
	}()
	host.GetKernelReleaseAndVersion = func() (string, string, error) { return release, version, nil }
	testVersions := []struct {
		release   string
		version   string
		supported bool
	}{
		{
			"4.1",
			"",
			false,
		},
		{
			"4.4",
			"",
			true,
		},
		{
			"4.4-custom",
			"",
			true,
		},
		{
			"4.4.127",
			"",
			true,
		},
		{
			"4.4.0-119-generic",
			"#143-Ubuntu SMP Mon Apr 2 16:08:24 UTC 2018",
			false,
		},
		{
			"4.4.0-127-generic",
			"#153-Ubuntu SMP Sat May 19 10:58:46 UTC 2018",
			true,
		},
		{
			"4.4.0-116-generic",
			"#140-Ubuntu SMP Mon Feb 12 21:23:04 UTC 2018",
			true,
		},
		{
			"4.13.0-38-generic",
			"#43-Ubuntu SMP Wed Mar 14 15:20:44 UTC 2018",
			true,
		},
		{
			"4.13.0-119-generic",
			"#43-Ubuntu SMP Wed Apr 1 00:00:00 UTC 2018",
			true,
		},
		{
			"4.9.0-6-amd64",
			"#1 SMP Debian 4.9.82-1+deb9u3 (2018-03-02)",
			true,
		},
	}
	for _, tv := range testVersions {
		release = tv.release
		version = tv.version
		err := isKernelSupported()
		if tv.supported && err != nil {
			t.Errorf("expected kernel release %q version %q to be supported but got error: %v", release, version, err)
		}
		if !tv.supported && err == nil {
			t.Errorf("expected kernel release %q version %q to not be supported", release, version)
		}
	}
}
