// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package cmd

import (
	"bytes"
	"encoding/json"
	"io"
	"os"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/cilium/cilium/pkg/byteorder"
	"github.com/cilium/cilium/pkg/command"
	"github.com/cilium/cilium/pkg/maps/ctmap"
	"github.com/cilium/cilium/pkg/testutils/mockmaps"
	"github.com/cilium/cilium/pkg/tuple"
	"github.com/cilium/cilium/pkg/types"
)

var (
	ctKey4 = ctmap.CtKey4{
		TupleKey4: tuple.TupleKey4{
			DestAddr:   types.IPv4{10, 10, 10, 1},
			SourceAddr: types.IPv4{10, 10, 10, 2},
			DestPort:   byteorder.HostToNetwork16(80),
			SourcePort: byteorder.HostToNetwork16(13579),
			NextHeader: 6,
			Flags:      123,
		},
	}
	ctKey6 = ctmap.CtKey6{
		TupleKey6: tuple.TupleKey6{
			DestAddr:   types.IPv6{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
			SourceAddr: types.IPv6{1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 121, 98, 219, 61},
			DestPort:   byteorder.HostToNetwork16(443),
			SourcePort: byteorder.HostToNetwork16(7878),
			NextHeader: 17,
			Flags:      31,
		},
	}
	ctValue = ctmap.CtEntry{
		Packets:          4 + 1,
		Bytes:            2048 + 512,
		Lifetime:         12345,
		Flags:            3,
		RevNAT:           byteorder.HostToNetwork16(27),
		TxFlagsSeen:      88,
		RxFlagsSeen:      99,
		SourceSecurityID: 6789,
		LastTxReport:     0,
		LastRxReport:     7777,
	}
)

type ctRecord4 struct {
	Key   tuple.TupleKey4
	Value ctmap.CtEntry
}

type ctRecord6 struct {
	Key   tuple.TupleKey6
	Value ctmap.CtEntry
}

func dumpAndRead[T any](t *testing.T, maps []T, dump func([]T, ...any), args ...any) string {
	// dumpCt() prints to standard output. Let's redirect it to a pipe, and
	// read the dump from there.
	stdout := os.Stdout
	readEnd, writeEnd, err := os.Pipe()
	require.NoError(t, err, "failed to create pipe: '%s'", err)
	os.Stdout = writeEnd
	defer func() { os.Stdout = stdout }()

	command.ForceJSON()
	dump(maps, args...)

	channel := make(chan string)
	go func() {
		var buf bytes.Buffer
		_, err = io.Copy(&buf, readEnd)
		channel <- buf.String()
	}()

	writeEnd.Close()
	// Even though we have a defer, restore os.Stdout already if we can
	// (for the assert)
	os.Stdout = stdout
	rawDump := <-channel
	require.NoError(t, err, "failed to read data: '%s'", err)

	return rawDump
}

func TestDumpCt4(t *testing.T) {
	ctMaps := []ctmap.CtMap{
		mockmaps.NewCtMockMap(
			[]ctmap.CtMapRecord{
				{
					Key:   &ctKey4,
					Value: ctValue,
				},
				{
					Key:   &ctKey4,
					Value: ctValue,
				},
			},
		),
		mockmaps.NewCtMockMap(
			[]ctmap.CtMapRecord{
				{
					Key:   &ctKey4,
					Value: ctValue,
				},
			},
		),
	}

	rawDump := dumpAndRead(t, ctMaps, dumpCt, "")

	var ctDump []ctRecord4
	err := json.Unmarshal([]byte(rawDump), &ctDump)
	require.NoError(t, err, "invalid JSON output: '%s', '%s'", err, rawDump)

	// JSON output may reorder the entries, but in our case they are all
	// the same.
	ctRecordDump := ctmap.CtMapRecord{
		Key:   &ctmap.CtKey4{TupleKey4: ctDump[0].Key},
		Value: ctDump[0].Value,
	}
	require.Equal(t, ctRecordDump, ctMaps[0].(*mockmaps.CtMockMap).Entries[0])
}

func TestDumpCt6(t *testing.T) {
	ctMaps := []ctmap.CtMap{
		mockmaps.NewCtMockMap(
			[]ctmap.CtMapRecord{
				{
					Key:   &ctKey6,
					Value: ctValue,
				},
				{
					Key:   &ctKey6,
					Value: ctValue,
				},
			},
		),
		mockmaps.NewCtMockMap(
			[]ctmap.CtMapRecord{
				{
					Key:   &ctKey6,
					Value: ctValue,
				},
			},
		),
	}

	rawDump := dumpAndRead(t, ctMaps, dumpCt, "")

	var ctDump []ctRecord6
	err := json.Unmarshal([]byte(rawDump), &ctDump)
	require.NoError(t, err, "invalid JSON output: '%s', '%s'", err, rawDump)

	// JSON output may reorder the entries, but in our case they are all
	// the same.
	ctRecordDump := ctmap.CtMapRecord{
		Key:   &ctmap.CtKey6{TupleKey6: ctDump[0].Key},
		Value: ctDump[0].Value,
	}
	require.Equal(t, ctRecordDump, ctMaps[0].(*mockmaps.CtMockMap).Entries[0])
}
