package agentstate

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestTombstonesEvictByCapacity(t *testing.T) {
	now := time.Date(2026, 8, 19, 5, 0, 0, 0, time.UTC)
	set := NewTombstones(24*time.Hour, 512)
	for index := 0; index < 513; index++ {
		state := tombstoneWake(
			fmt.Sprintf("wake-%03d", index),
			now.Add(time.Duration(index)*time.Second),
		)
		require.NoError(t, set.Add(state))
	}

	require.Equal(t, []string{"wake-000"}, set.Evict(now.Add(513*time.Second)))
	require.Equal(t, 512, set.Len())
}

func TestTombstonesEvictByAge(t *testing.T) {
	now := time.Date(2026, 8, 19, 5, 0, 0, 0, time.UTC)
	set := NewTombstones(24*time.Hour, 512)
	require.NoError(t, set.Add(tombstoneWake("wake-old", now)))
	require.NoError(t, set.Add(tombstoneWake("wake-fresh", now.Add(time.Second))))

	require.Equal(
		t,
		[]string{"wake-old"},
		set.Evict(now.Add(24*time.Hour)),
	)
	require.Equal(t, []string{"wake-fresh"}, tombstoneIDs(set.Snapshot()))
}

func TestTombstonesRejectOpenWakes(t *testing.T) {
	set := NewTombstones(0, 0)
	open := tombstoneWake("wake-open", time.Now().UTC())
	open.Phase = WakePhaseReceived
	open.ReplyObjectIDs = nil

	require.ErrorContains(t, set.Add(open), "terminal")
	require.Zero(t, set.Len())
}

func TestTombstonesRetainEveryTerminalShape(t *testing.T) {
	now := time.Date(2026, 8, 19, 5, 0, 0, 0, time.UTC)
	set := NewTombstones(0, 0)

	confirmed := tombstoneWake("wake-confirmed", now)
	noReply := tombstoneWake("wake-no-reply", now.Add(time.Second))
	noReply.Phase = WakePhaseNoReply
	noReply.ReplyObjectIDs = nil
	noReply.NoReplyReasonCode = WakeNoReplyDecided
	failed := tombstoneWake("wake-failed", now.Add(2*time.Second))
	failed.Phase = WakePhaseFailed
	failed.ReplyObjectIDs = nil
	failed.FailureReasonCode = WakeFailureRuntimeFailed

	require.NoError(t, set.Add(confirmed))
	require.NoError(t, set.Add(noReply))
	require.NoError(t, set.Add(failed))
	require.Equal(
		t,
		[]string{"wake-failed", "wake-no-reply", "wake-confirmed"},
		tombstoneIDs(set.Snapshot()),
	)
}

func TestTombstonesSnapshotReturnsDeepCopy(t *testing.T) {
	now := time.Date(2026, 8, 19, 5, 0, 0, 0, time.UTC)
	set := NewTombstones(0, 0)
	require.NoError(t, set.Add(tombstoneWake("wake-1", now)))

	first := set.Snapshot()
	first[0].ReplyObjectIDs[0] = "mutated"
	second := set.Snapshot()
	require.Equal(t, "reply-wake-1", second[0].ReplyObjectIDs[0])
}

func TestTombstonesIdempotentReplacementPreservesTerminalImmutability(t *testing.T) {
	now := time.Date(2026, 8, 19, 5, 0, 0, 0, time.UTC)
	set := NewTombstones(0, 0)
	state := tombstoneWake("wake-1", now)
	require.NoError(t, set.Add(state))
	require.NoError(t, set.Add(state))

	changed := state
	changed.ReplyObjectIDs = []string{"different"}
	require.ErrorIs(t, set.Add(changed), ErrTerminalWake)
	require.Equal(t, []string{"reply-wake-1"}, set.Snapshot()[0].ReplyObjectIDs)
}

func TestTombstonesOrderingUsesStableIDTieBreaker(t *testing.T) {
	now := time.Date(2026, 8, 19, 5, 0, 0, 0, time.UTC)
	set := NewTombstones(0, 0)
	require.NoError(t, set.Add(tombstoneWake("wake-b", now)))
	require.NoError(t, set.Add(tombstoneWake("wake-a", now)))

	require.Equal(t, []string{"wake-a", "wake-b"}, tombstoneIDs(set.Snapshot()))
}

func tombstoneWake(localWakeID string, changedAt time.Time) WakeDeliveryState {
	return WakeDeliveryState{
		LocalWakeID:     localWakeID,
		SourceMessageID: "source-" + localWakeID,
		TargetKey:       "#ops",
		AckedAt:         changedAt.Add(-time.Second),
		ChangedAt:       changedAt,
		Phase:           WakePhaseReplyConfirmed,
		ReplyObjectIDs:  []string{"reply-" + localWakeID},
	}
}

func tombstoneIDs(states []WakeDeliveryState) []string {
	result := make([]string, 0, len(states))
	for _, state := range states {
		result = append(result, state.LocalWakeID)
	}
	return result
}
