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

package meta_test

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/projectcapsule/capsule/pkg/api/meta"
)

// helper
func makeCond(tpe, status, reason, msg string, gen int64) meta.Condition {
	return meta.Condition{
		Type:               tpe,
		Status:             metav1.ConditionStatus(status),
		Reason:             reason,
		Message:            msg,
		ObservedGeneration: gen,
		LastTransitionTime: metav1.NewTime(time.Unix(0, 0)),
	}
}

func TestConditionList_GetConditionByType(t *testing.T) {
	t.Run("returns matching condition", func(t *testing.T) {
		list := meta.ConditionList{
			makeCond("Ready", "False", "Init", "starting", 1),
			makeCond("Synced", "True", "Ok", "done", 2),
		}

		got := list.GetConditionByType("Synced")
		assert.NotNil(t, got)
		assert.Equal(t, "Synced", got.Type)
		assert.Equal(t, metav1.ConditionTrue, got.Status)
		assert.Equal(t, "Ok", got.Reason)
		assert.Equal(t, "done", got.Message)
	})

	t.Run("returns nil when not found", func(t *testing.T) {
		list := meta.ConditionList{
			makeCond("Ready", "False", "Init", "starting", 1),
		}
		assert.Nil(t, list.GetConditionByType("Missing"))
	})

	t.Run("returned pointer refers to slice element (not copy)", func(t *testing.T) {
		list := meta.ConditionList{
			makeCond("Ready", "False", "Init", "starting", 1),
			makeCond("Synced", "True", "Ok", "done", 2),
		}
		ptr := list.GetConditionByType("Ready")
		assert.NotNil(t, ptr)

		ptr.Message = "mutated"
		// This asserts GetConditionByType returns &list[i] (via index),
		// not &cond where cond is the range variable copy.
		assert.Equal(t, "mutated", list[0].Message)
	})
}

func TestConditionList_UpdateConditionByType(t *testing.T) {
	now := metav1.Now()

	t.Run("updates existing condition in place", func(t *testing.T) {
		list := meta.ConditionList{
			makeCond("Ready", "False", "Init", "starting", 1),
			makeCond("Synced", "True", "Ok", "done", 2),
		}
		beforeLen := len(list)

		list.UpdateConditionByType(meta.Condition{
			Type:               "Ready",
			Status:             metav1.ConditionTrue,
			Reason:             "Reconciled",
			Message:            "ready now",
			ObservedGeneration: 3,
			LastTransitionTime: now,
		})

		assert.Equal(t, beforeLen, len(list))
		got := list.GetConditionByType("Ready")
		assert.NotNil(t, got)
		assert.Equal(t, metav1.ConditionTrue, got.Status)
		assert.Equal(t, "Reconciled", got.Reason)
		assert.Equal(t, "ready now", got.Message)
		assert.Equal(t, int64(3), got.ObservedGeneration)
	})

	t.Run("appends when condition type not present", func(t *testing.T) {
		list := meta.ConditionList{
			makeCond("Ready", "True", "Ok", "ready", 1),
		}
		beforeLen := len(list)

		list.UpdateConditionByType(meta.Condition{
			Type:               "Synced",
			Status:             metav1.ConditionTrue,
			Reason:             "Done",
			Message:            "synced",
			ObservedGeneration: 2,
			LastTransitionTime: now,
		})

		assert.Equal(t, beforeLen+1, len(list))
		got := list.GetConditionByType("Synced")
		assert.NotNil(t, got)
		assert.Equal(t, metav1.ConditionTrue, got.Status)
		assert.Equal(t, "Done", got.Reason)
		assert.Equal(t, "synced", got.Message)
		assert.Equal(t, int64(2), got.ObservedGeneration)
	})
}

func TestConditionList_RemoveConditionByType(t *testing.T) {
	t.Run("removes all conditions with matching type", func(t *testing.T) {
		list := meta.ConditionList{
			makeCond("A", "True", "x", "m1", 1),
			makeCond("B", "True", "y", "m2", 1),
			makeCond("A", "False", "z", "m3", 2),
		}
		list.RemoveConditionByType("A")

		assert.Len(t, list, 1)
		assert.Equal(t, "B", list[0].Type)
	})

	t.Run("no-op when type not present", func(t *testing.T) {
		orig := meta.ConditionList{
			makeCond("A", "True", "x", "m1", 1),
		}
		list := append(meta.ConditionList{}, orig...) // copy

		list.RemoveConditionByType("Missing")

		assert.Equal(t, orig, list)
	})

	t.Run("nil receiver is safe", func(t *testing.T) {
		var list *meta.ConditionList // nil receiver
		assert.NotPanics(t, func() {
			list.RemoveConditionByType("X")
		})
	})
}

func TestUpdateCondition(t *testing.T) {
	now := metav1.Now()

	t.Run("no update when all relevant fields match", func(t *testing.T) {
		c := &meta.Condition{
			Type:    "Ready",
			Status:  "True",
			Reason:  "Success",
			Message: "All good",
		}

		updated := c.UpdateCondition(meta.Condition{
			Type:               "Ready",
			Status:             "True",
			Reason:             "Success",
			Message:            "All good",
			LastTransitionTime: now,
		})

		assert.False(t, updated)
	})

	t.Run("update occurs on message change", func(t *testing.T) {
		c := &meta.Condition{
			Type:    "Ready",
			Status:  "True",
			Reason:  "Success",
			Message: "Old message",
		}

		updated := c.UpdateCondition(meta.Condition{
			Type:               "Ready",
			Status:             "True",
			Reason:             "Success",
			Message:            "New message",
			LastTransitionTime: now,
		})

		assert.True(t, updated)
		assert.Equal(t, "New message", c.Message)
	})

	t.Run("update occurs on status change", func(t *testing.T) {
		c := &meta.Condition{
			Type:    "Ready",
			Status:  "False",
			Reason:  "Pending",
			Message: "Not ready yet",
		}

		updated := c.UpdateCondition(meta.Condition{
			Type:               "Ready",
			Status:             "True",
			Reason:             "Success",
			Message:            "Ready",
			LastTransitionTime: now,
		})

		assert.True(t, updated)
		assert.Equal(t, "True", string(c.Status))
		assert.Equal(t, "Success", c.Reason)
		assert.Equal(t, "Ready", c.Message)
	})
}
