// Copyright (c) 2025 Tigera, Inc. All rights reserved.

// 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 types_test

import (
	"reflect"
	"testing"

	"github.com/stretchr/testify/require"
	googleproto "google.golang.org/protobuf/proto"

	"github.com/projectcalico/calico/goldmane/pkg/types"
	"github.com/projectcalico/calico/goldmane/proto"
)

type fromProtoTest struct {
	name  string
	proto proto.Flow
}

func TestTranslation(t *testing.T) {
	// Assert that bidirection translation works.
	tests := []*fromProtoTest{
		{
			name:  "empty proto.Flow",
			proto: proto.Flow{},
		},
		{
			name: "proto.Flow with all fields set",
			proto: proto.Flow{
				Key: &proto.FlowKey{
					SourceName:           "source-name",
					SourceNamespace:      "source-namespace",
					SourceType:           proto.EndpointType_WorkloadEndpoint,
					DestName:             "dest-name",
					DestNamespace:        "dest-namespace",
					DestType:             proto.EndpointType_NetworkSet,
					DestPort:             1234,
					DestServiceName:      "dest-service-name",
					DestServiceNamespace: "dest-service-namespace",
					DestServicePortName:  "dest-service-port-name",
					DestServicePort:      5678,
					Proto:                "proto",
					Reporter:             proto.Reporter_Dst,
					Action:               proto.Action_Allow,
					Policies: &proto.PolicyTrace{
						EnforcedPolicies: []*proto.PolicyHit{
							{Name: "policy-1"},
							{Name: "policy-2"},
						},
						PendingPolicies: []*proto.PolicyHit{
							{Name: "pending-policy-1"},
							{Name: "pending-policy-2"},
						},
					},
				},
				StartTime:               1234567890,
				EndTime:                 1234567891,
				SourceLabels:            []string{"source-label-1", "source-label-2"},
				DestLabels:              []string{"dest-label-1", "dest-label-2"},
				PacketsIn:               123,
				PacketsOut:              456,
				BytesIn:                 789,
				BytesOut:                101112,
				NumConnectionsStarted:   131415,
				NumConnectionsCompleted: 161718,
				NumConnectionsLive:      192021,
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			f := types.ProtoToFlow(&test.proto)
			p := types.FlowToProto(f)
			if !googleproto.Equal(&test.proto, p) {
				t.Fatalf("translated proto.Flow does not match the original proto.Flow: %v != %v", p, test.proto.String())
			}
		})
	}
}

func TestKeyEquality(t *testing.T) {
	// We need to assert that key translation works and that translating the same flow
	// results in the same key.
	keys := []*proto.FlowKey{
		{
			// Fully specified.
			SourceName:           "source-name",
			SourceNamespace:      "source-namespace",
			SourceType:           proto.EndpointType_WorkloadEndpoint,
			DestName:             "dest-name",
			DestNamespace:        "dest-namespace",
			DestType:             proto.EndpointType_NetworkSet,
			DestPort:             1234,
			DestServiceName:      "dest-service-name",
			DestServiceNamespace: "dest-service-namespace",
			DestServicePortName:  "dest-service-port-name",
			DestServicePort:      5678,
			Proto:                "proto",
			Reporter:             proto.Reporter_Dst,
			Action:               proto.Action_Allow,
			Policies: &proto.PolicyTrace{
				EnforcedPolicies: []*proto.PolicyHit{
					{Name: "policy-1"},
					{Name: "policy-2"},
				},
				PendingPolicies: []*proto.PolicyHit{
					{Name: "pending-policy-1"},
					{Name: "pending-policy-2"},
				},
			},
		},
		{
			// No Policies.
			SourceName:           "source-name",
			SourceNamespace:      "source-namespace",
			SourceType:           proto.EndpointType_HostEndpoint,
			DestName:             "dest-name",
			DestNamespace:        "dest-namespace",
			DestType:             proto.EndpointType_WorkloadEndpoint,
			DestPort:             1234,
			DestServiceName:      "dest-service-name",
			DestServiceNamespace: "dest-service-namespace",
			DestServicePortName:  "dest-service-port-name",
			DestServicePort:      5678,
			Proto:                "proto",
			Reporter:             proto.Reporter_Src,
			Action:               proto.Action_Deny,
		},
	}

	// For each key, translate it and use it as a map key. Then, translate it again and
	// confirm it is the same. This ensures we don't accidentally introduce inequality by, for example,
	// using pointer fields in the struct.
	for _, key := range keys {
		m := map[types.FlowKey]struct{}{}
		t.Run(key.String(), func(t *testing.T) {
			k := types.ProtoToFlowKey(key)
			m[*k] = struct{}{}
			k2 := types.ProtoToFlowKey(key)
			require.Equal(t, k, k2)
			if _, ok := m[*k2]; !ok {
				t.Fatalf("expected key to be in map")
			}
		})
	}
}

func TestFlowLogPolicyToProtoSortsByPolicyIndex(t *testing.T) {
	// Policies stored in goldmane come from Felix where they are iterated from a Go map,
	// so the order is non-deterministic. FlowLogPolicyToProto should sort them by PolicyIndex
	// so the UI displays them in evaluation order.
	trace := &proto.PolicyTrace{
		EnforcedPolicies: []*proto.PolicyHit{
			{Name: "end-of-tier", Tier: "default", PolicyIndex: 1, Action: proto.Action_Deny},
			{Name: "pass-to-np", Tier: "kube-admin", PolicyIndex: 0, Action: proto.Action_Pass},
		},
		PendingPolicies: []*proto.PolicyHit{
			{Name: "staged-c", Tier: "tier-c", PolicyIndex: 2},
			{Name: "staged-a", Tier: "tier-a", PolicyIndex: 0},
			{Name: "staged-b", Tier: "tier-b", PolicyIndex: 1},
		},
	}

	h := types.ProtoToFlowLogPolicy(trace)
	result := types.FlowLogPolicyToProto(h)

	require.Equal(t, "pass-to-np", result.EnforcedPolicies[0].Name)
	require.Equal(t, "end-of-tier", result.EnforcedPolicies[1].Name)

	require.Equal(t, "staged-a", result.PendingPolicies[0].Name)
	require.Equal(t, "staged-b", result.PendingPolicies[1].Name)
	require.Equal(t, "staged-c", result.PendingPolicies[2].Name)
}

// TestIdentical verifies that the exported fields on types.Flow and proto.Flow are identical. This ensures
// we don't accidentally add new fields to one type and forget to add them to the other.
func TestIdentical(t *testing.T) {
	p := reflect.ValueOf(proto.Flow{})
	f := reflect.ValueOf(types.Flow{})

	// Check each field in types.Flow is present in proto.Flow.
	for _, fField := range reflect.VisibleFields(f.Type()) {
		found := false
		if !fField.IsExported() {
			continue
		}
		for _, pField := range reflect.VisibleFields(p.Type()) {
			if fField.Name == pField.Name {
				found = true
				break
			}
		}
		require.True(t, found, "field %s not found in proto.Flow", fField.Name)
	}

	// Check each field in proto.Flow is present in types.Flow.
	for _, pField := range reflect.VisibleFields(p.Type()) {
		found := false
		if !pField.IsExported() {
			continue
		}
		for _, fField := range reflect.VisibleFields(f.Type()) {
			if pField.Name == fField.Name {
				found = true
				break
			}
		}
		require.True(t, found, "field %s not found in types.Flow", pField.Name)
	}
}
