// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0

package utils_test

import (
	"reflect"
	"testing"

	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"

	"github.com/projectcapsule/capsule/pkg/utils"
)

func TestMapMergeNoOverrite_AddsNonOverlapping(t *testing.T) {
	dst := map[string]string{"a": "1"}
	src := map[string]string{"b": "2"}

	utils.MapMergeNoOverrite(dst, src)

	if got, want := dst["a"], "1"; got != want {
		t.Fatalf("dst[a] = %q, want %q", got, want)
	}
	if got, want := dst["b"], "2"; got != want {
		t.Fatalf("dst[b] = %q, want %q", got, want)
	}
	if len(dst) != 2 {
		t.Fatalf("len(dst) = %d, want 2", len(dst))
	}
}

func TestMapMergeNoOverrite_DoesNotOverwriteExisting(t *testing.T) {
	dst := map[string]string{"a": "1"}
	src := map[string]string{"a": "X"} // overlapping key

	utils.MapMergeNoOverrite(dst, src)

	if got, want := dst["a"], "1"; got != want {
		t.Fatalf("dst[a] overwritten: got %q, want %q", got, want)
	}
}

func TestMapMergeNoOverrite_EmptySrc_NoChange(t *testing.T) {
	dst := map[string]string{"a": "1"}
	src := map[string]string{} // empty

	before := make(map[string]string, len(dst))
	for k, v := range dst {
		before[k] = v
	}

	utils.MapMergeNoOverrite(dst, src)

	if !reflect.DeepEqual(dst, before) {
		t.Fatalf("dst changed with empty src: got %#v, want %#v", dst, before)
	}
}

func TestMapMergeNoOverrite_NilSrc_NoChange(t *testing.T) {
	dst := map[string]string{"a": "1"}
	var src map[string]string // nil

	before := make(map[string]string, len(dst))
	for k, v := range dst {
		before[k] = v
	}

	utils.MapMergeNoOverrite(dst, src)

	if !reflect.DeepEqual(dst, before) {
		t.Fatalf("dst changed with nil src: got %#v, want %#v", dst, before)
	}
}

func TestMapMergeNoOverrite_Idempotent(t *testing.T) {
	dst := map[string]string{"a": "1"}
	src := map[string]string{"b": "2"}

	utils.MapMergeNoOverrite(dst, src)
	first := map[string]string{}
	for k, v := range dst {
		first[k] = v
	}

	// Call again; result should be identical
	utils.MapMergeNoOverrite(dst, src)

	if !reflect.DeepEqual(dst, first) {
		t.Fatalf("non-idempotent merge: after second merge got %#v, want %#v", dst, first)
	}
}

func TestMapMergeNoOverrite_NilDst_Panics(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("expected panic when dst is nil, but did not panic")
		}
	}()

	var dst map[string]string // nil destination map
	src := map[string]string{"a": "1"}

	// Writing to a nil map panics; document current behavior via this test.
	utils.MapMergeNoOverrite(dst, src)
}

func TestMapEqual(t *testing.T) {
	tests := []struct {
		name string
		a    map[string]string
		b    map[string]string
		want bool
	}{
		{
			name: "both nil => equal",
			a:    nil,
			b:    nil,
			want: true,
		},
		{
			name: "nil vs empty => equal (len==0 in both)",
			a:    nil,
			b:    map[string]string{},
			want: true,
		},
		{
			name: "empty vs nil => equal (len==0 in both)",
			a:    map[string]string{},
			b:    nil,
			want: true,
		},
		{
			name: "different lengths => not equal",
			a:    map[string]string{"a": "1"},
			b:    map[string]string{},
			want: false,
		},
		{
			name: "same length but missing key => not equal",
			a:    map[string]string{"a": "1"},
			b:    map[string]string{"b": "1"},
			want: false,
		},
		{
			name: "same keys but different value => not equal",
			a:    map[string]string{"a": "1"},
			b:    map[string]string{"a": "2"},
			want: false,
		},
		{
			name: "same keys and values => equal",
			a:    map[string]string{"a": "1", "b": "2"},
			b:    map[string]string{"b": "2", "a": "1"},
			want: true,
		},
		{
			name: "both empty => equal",
			a:    map[string]string{},
			b:    map[string]string{},
			want: true,
		},
		{
			name: "extra key on b (length differs) => not equal",
			a:    map[string]string{"a": "1"},
			b:    map[string]string{"a": "1", "b": "2"},
			want: false,
		},
		{
			name: "same length but b has different key (no overlap) => not equal",
			a:    map[string]string{"a": "1", "b": "2"},
			b:    map[string]string{"c": "1", "d": "2"},
			want: false,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)
			g.Expect(utils.MapEqual(tt.a, tt.b)).To(Equal(tt.want))
		})
	}
}

func TestToUnstructuredMap(t *testing.T) {
	t.Parallel()

	got, err := utils.ToUnstructuredMap(&corev1.ConfigMap{
		Data: map[string]string{"key": "value"},
	})
	if err != nil {
		t.Fatalf("ToUnstructuredMap() unexpected error: %v", err)
	}
	if got["data"].(map[string]any)["key"] != "value" {
		t.Fatalf("ToUnstructuredMap() data = %#v", got["data"])
	}
}

func TestMapify(t *testing.T) {
	t.Parallel()

	type nested struct {
		Value string
	}
	type sample struct {
		Name    string
		Nested  nested
		Pointer *nested
		Items   []nested
		Labels  map[string]string
		hidden  string
	}

	got := utils.Mapify(sample{
		Name:    "example",
		Nested:  nested{Value: "nested"},
		Pointer: &nested{Value: "pointer"},
		Items:   []nested{{Value: "one"}},
		Labels:  map[string]string{"app": "api"},
		hidden:  "ignored",
	})

	if got["Name"] != "example" {
		t.Fatalf("Mapify() Name = %#v", got["Name"])
	}
	if _, ok := got["hidden"]; ok {
		t.Fatalf("Mapify() included unexported field")
	}
	if got["Nested"].(map[string]any)["Value"] != "nested" {
		t.Fatalf("Mapify() nested = %#v", got["Nested"])
	}
	if got["Pointer"].(map[string]any)["Value"] != "pointer" {
		t.Fatalf("Mapify() pointer = %#v", got["Pointer"])
	}
	if got["Items"].([]any)[0].(map[string]any)["Value"] != "one" {
		t.Fatalf("Mapify() slice = %#v", got["Items"])
	}
	if got["Labels"].(map[string]any)["app"] != "api" {
		t.Fatalf("Mapify() map = %#v", got["Labels"])
	}

	var nilSample *sample
	if got := utils.Mapify(nilSample); len(got) != 0 {
		t.Fatalf("Mapify(nil pointer) = %#v, want empty map", got)
	}
	if got := utils.Mapify("not-a-struct"); len(got) != 0 {
		t.Fatalf("Mapify(non-struct) = %#v, want empty map", got)
	}
}
