package store

import (
	"math/rand"
	"net/netip"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/netbirdio/netbird/client/internal/netflow/types"
	"github.com/stretchr/testify/assert"
)

var random = rand.New(rand.NewSource(time.Now().UnixNano()))

func TestFlowAggregation(t *testing.T) {
	var protocols = []types.Protocol{types.ICMP, types.ICMPv6, types.TCP, types.UDP}
	var tests = []struct {
		description string
		addresses   [][]netip.Addr
		dstPort     uint16
		eventTypes  []types.Type
	}{
		{
			description: "start and stop",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeStart, types.TypeEnd},
		},
		{
			description: "start and drop",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeStart, types.TypeDrop},
		},
		{
			description: "start only",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeStart},
		},
		{
			description: "drop only",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeDrop},
		}}

	for _, protocol := range protocols {
		for _, tt := range tests {
			t.Run(tt.description+" "+protocol.String(), func(t *testing.T) {
				store := NewAggregatingMemoryStore()
				store.WindowEnd = time.Now().Add(5 * time.Second)

				allExpected := make([]*types.Event, 0)

				for _, srcAndDst := range tt.addresses {
					inEvents, expected := generateEvents(srcAndDst[0], srcAndDst[1], tt.dstPort, tt.eventTypes, protocol, types.Ingress, 0, store.WindowStart, store.WindowEnd)
					for _, e := range inEvents {
						store.StoreEvent(e)
					}
					allExpected = append(allExpected, expected)
				}

				events := store.GetAggregatedEvents()
				assert.ElementsMatch(t, events, allExpected)
			})
		}
	}
}

func TestIcmpEventAggregation(t *testing.T) {
	var protocols = []types.Protocol{types.ICMP, types.ICMPv6}
	var icmpTypes = []uint8{1, 2, 3}

	var tests = []struct {
		description string
		addresses   [][]netip.Addr
		eventTypes  []types.Type
	}{
		{
			description: "start and stop",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}},
			eventTypes:  []types.Type{types.TypeStart, types.TypeEnd},
		},
		{
			description: "start and drop",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}},
			eventTypes:  []types.Type{types.TypeStart, types.TypeDrop},
		},
		{
			description: "start only",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}},
			eventTypes:  []types.Type{types.TypeStart},
		},
		{
			description: "drop only",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}},
			eventTypes:  []types.Type{types.TypeDrop},
		}}

	for _, protocol := range protocols {
		for _, tt := range tests {
			t.Run(tt.description+" "+protocol.String(), func(t *testing.T) {
				store := NewAggregatingMemoryStore()
				store.WindowEnd = time.Now().Add(5 * time.Second)

				allExpected := make([]*types.Event, 0)
				for _, icmpType := range icmpTypes {
					events, expected := generateEvents(tt.addresses[0][0], tt.addresses[0][1], 0, tt.eventTypes, protocol, types.Ingress, icmpType, store.WindowStart, store.WindowEnd)
					for _, e := range events {
						store.StoreEvent(e)
					}
					allExpected = append(allExpected, expected)
				}
				aggregatedEvents := store.GetAggregatedEvents()
				assert.Len(t, aggregatedEvents, len(allExpected))
				assert.ElementsMatch(t, aggregatedEvents, allExpected)
			})
		}
	}
}

func TestFlowAggregationOfUnknownProtocols(t *testing.T) {
	var tests = []struct {
		description string
		addresses   [][]netip.Addr
		dstPort     uint16
		eventTypes  []types.Type
	}{
		{
			description: "start and stop",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeStart, types.TypeEnd},
		},
		{
			description: "start and drop",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeStart, types.TypeDrop},
		},
		{
			description: "start only",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeStart},
		},
		{
			description: "drop only",
			addresses:   [][]netip.Addr{{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, {netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("2.2.2.2")}},
			dstPort:     uint16(random.Uint32() >> 16),
			eventTypes:  []types.Type{types.TypeDrop},
		}}

	for _, tt := range tests {
		t.Run(tt.description+" "+types.ProtocolUnknown.String(), func(t *testing.T) {
			store := NewAggregatingMemoryStore()
			store.WindowEnd = time.Now().Add(5 * time.Second)

			allExpected := make([]*types.Event, 0)

			for _, srcAndDst := range tt.addresses {
				inEvents, expected := generateEventsForUnknownProtocol(srcAndDst[0], srcAndDst[1], tt.dstPort, tt.eventTypes, types.ProtocolUnknown, types.Ingress, store.WindowStart, store.WindowEnd)
				for _, e := range inEvents {
					store.StoreEvent(e)
				}
				allExpected = append(allExpected, expected...)
			}

			events := store.GetAggregatedEvents()
			assert.ElementsMatch(t, events, allExpected)
		})
	}
}

func TestResetAggregationWindow(t *testing.T) {
	now := time.Now()
	nowFunc := func() time.Time { return now }
	store := NewAggregatingMemoryStoreWithTimeFunc(nowFunc)
	store.StoreEvent(&types.Event{
		ID:        uuid.New(),
		Timestamp: time.Now(),
		EventFields: types.EventFields{
			FlowID:           uuid.New(),
			Type:             types.TypeStart,
			Protocol:         types.TCP,
			RuleID:           []byte("rule-id-1"),
			Direction:        types.Ingress,
			SourceIP:         netip.MustParseAddr("1.1.1.1"),
			SourcePort:       1234,
			DestIP:           netip.MustParseAddr("2.2.2.2"),
			DestPort:         5678,
			SourceResourceID: []byte("source-resource-id"),
			DestResourceID:   []byte("dest-resource-id"),
			RxPackets:        random.Uint64(),
			TxPackets:        random.Uint64(),
			RxBytes:          random.Uint64(),
			TxBytes:          random.Uint64(),
		},
	})

	now = now.Add(1 * time.Second)
	reset := store.ResetAggregationWindow()
	previousEvents, ok := reset.(*AggregatingMemory)
	assert.True(t, ok)
	assert.NotEqual(t, previousEvents.WindowStart, store.WindowStart)
	assert.Equal(t, previousEvents.WindowEnd, store.WindowStart)
	assert.NotEmpty(t, previousEvents.events)
	assert.Empty(t, store.events)
}

func generateEvents(srcIp, dstIp netip.Addr, dstPort uint16, eventTypes []types.Type, protocol types.Protocol,
	direction types.Direction, icmpType uint8, windowStart, windowEnd time.Time) ([]*types.Event, *types.Event) {
	var rxPackets, txPackets, rxBytes, txBytes uint64
	inEvents := make([]*types.Event, 0)
	ts := time.Now()
	flowId := uuid.New()
	srcPort := uint16(random.Uint32() >> 16)

	for idx, eventType := range eventTypes {
		e := &types.Event{
			ID:        uuid.New(),
			Timestamp: ts.Add(time.Duration(idx) * time.Second),
			EventFields: types.EventFields{
				FlowID:           flowId,
				Type:             eventType,
				Protocol:         protocol,
				RuleID:           []byte("rule-id-1"),
				Direction:        direction,
				SourceIP:         srcIp,
				SourcePort:       srcPort,
				DestIP:           dstIp,
				DestPort:         dstPort,
				SourceResourceID: []byte("source-resource-id"),
				DestResourceID:   []byte("dest-resource-id"),
				RxPackets:        random.Uint64(),
				TxPackets:        random.Uint64(),
				RxBytes:          random.Uint64(),
				TxBytes:          random.Uint64(),
			}}
		rxBytes += e.RxBytes
		txBytes += e.TxBytes
		rxPackets += e.RxPackets
		txPackets += e.TxPackets
		inEvents = append(inEvents, e)
		if protocol == types.ICMP || protocol == types.ICMPv6 {
			e.ICMPType = icmpType
		}
	}

	var start, end, drop uint64
	for _, eventType := range eventTypes {
		switch eventType {
		case types.TypeStart:
			start += 1
		case types.TypeDrop:
			drop += 1
		case types.TypeEnd:
			end += 1
		}
	}
	aggregatedEvent := &types.Event{
		ID:          inEvents[0].ID,
		Timestamp:   inEvents[0].Timestamp,
		WindowStart: windowStart,
		WindowEnd:   windowEnd,
		EventFields: types.EventFields{
			FlowID:           flowId,
			Type:             types.TypeUnknown,
			Protocol:         inEvents[0].Protocol,
			RuleID:           []byte("rule-id-1"),
			Direction:        inEvents[0].Direction,
			SourceIP:         srcIp,
			SourcePort:       srcPort,
			DestIP:           dstIp,
			DestPort:         dstPort,
			SourceResourceID: []byte("source-resource-id"),
			DestResourceID:   []byte("dest-resource-id"),
			RxPackets:        rxPackets,
			TxPackets:        txPackets,
			RxBytes:          rxBytes,
			TxBytes:          txBytes,
			NumOfStarts:      start,
			NumOfEnds:        end,
			NumOfDrops:       drop,
		}}
	if protocol == types.ICMP || protocol == types.ICMPv6 {
		aggregatedEvent.ICMPType = icmpType
	}

	return inEvents, aggregatedEvent
}

func generateEventsForUnknownProtocol(srcIp, dstIp netip.Addr, dstPort uint16, eventTypes []types.Type, protocol types.Protocol,
	direction types.Direction, windowStart, windowEnd time.Time) ([]*types.Event, []*types.Event) {
	inEvents := make([]*types.Event, 0)
	expectedEvents := make([]*types.Event, 0)

	ts := time.Now()
	flowId := uuid.New()
	srcPort := uint16(random.Uint32() >> 16)

	for idx, eventType := range eventTypes {
		e := &types.Event{
			ID:        uuid.New(),
			Timestamp: ts.Add(time.Duration(idx) * time.Second),
			EventFields: types.EventFields{
				FlowID:           flowId,
				Type:             eventType,
				Protocol:         protocol,
				RuleID:           []byte("rule-id-1"),
				Direction:        direction,
				SourceIP:         srcIp,
				SourcePort:       srcPort,
				DestIP:           dstIp,
				DestPort:         dstPort,
				SourceResourceID: []byte("source-resource-id"),
				DestResourceID:   []byte("dest-resource-id"),
				RxPackets:        random.Uint64(),
				TxPackets:        random.Uint64(),
				RxBytes:          random.Uint64(),
				TxBytes:          random.Uint64(),
			}}
		inEvents = append(inEvents, e)

		var start, end, drop uint64
		switch eventType {
		case types.TypeStart:
			start = 1
		case types.TypeDrop:
			drop = 1
		case types.TypeEnd:
			end = 1
		}

		expectedEvents = append(expectedEvents, &types.Event{
			ID:          e.ID,
			Timestamp:   e.Timestamp,
			WindowStart: windowStart,
			WindowEnd:   windowEnd,
			EventFields: types.EventFields{
				FlowID:           flowId,
				Type:             types.TypeUnknown,
				Protocol:         e.Protocol,
				RuleID:           []byte("rule-id-1"),
				Direction:        e.Direction,
				SourceIP:         srcIp,
				SourcePort:       srcPort,
				DestIP:           dstIp,
				DestPort:         dstPort,
				SourceResourceID: []byte("source-resource-id"),
				DestResourceID:   []byte("dest-resource-id"),
				RxPackets:        e.RxPackets,
				TxPackets:        e.TxPackets,
				RxBytes:          e.RxBytes,
				TxBytes:          e.TxBytes,
				NumOfStarts:      start,
				NumOfEnds:        end,
				NumOfDrops:       drop,
			}})
	}

	return inEvents, expectedEvents
}
