package agentstate

import (
	"testing"
	"time"

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

func TestConsumerRequiresResyncAfterRevisionGap(t *testing.T) {
	consumer := NewConsumer()
	require.Equal(t, ApplyAccepted, consumer.Apply(consumerSnapshot(3, 1)))
	require.Equal(t, ApplyNeedsResync, consumer.Apply(consumerDelta(3, 2, 3)))
	require.Equal(t, ApplyIgnoredUntilSnapshot, consumer.Apply(consumerDelta(3, 3, 4)))
	require.Equal(t, ApplyAccepted, consumer.Apply(consumerSnapshot(3, 7)))
}

func TestConsumerDeltaForUnknownEpochNeedsResync(t *testing.T) {
	consumer := NewConsumer()
	require.Equal(t, ApplyNeedsResync, consumer.Apply(consumerDelta(4, 1, 2)))
	require.True(t, consumer.AwaitingSnapshot())
}

func TestConsumerIgnoresDuplicateStaleAndOlderEpoch(t *testing.T) {
	consumer := NewConsumer()
	require.Equal(t, ApplyAccepted, consumer.Apply(consumerSnapshot(5, 4)))
	require.Equal(t, ApplyIgnored, consumer.Apply(consumerSnapshot(5, 4)))
	require.Equal(t, ApplyIgnored, consumer.Apply(consumerSnapshot(5, 3)))
	require.Equal(t, ApplyIgnored, consumer.Apply(consumerSnapshot(4, 99)))
}

func TestConsumerHigherEpochDeltaCannotReplaceState(t *testing.T) {
	consumer := NewConsumer()
	require.Equal(t, ApplyAccepted, consumer.Apply(consumerSnapshot(5, 4)))

	require.Equal(t, ApplyNeedsResync, consumer.Apply(consumerDelta(6, 1, 2)))
	state, ok := consumer.State()
	require.True(t, ok)
	require.Equal(t, uint64(5), state.SessionEpoch)
	require.Equal(t, uint64(4), state.Revision)
}

func TestConsumerHigherEpochSnapshotAtomicallyReplacesState(t *testing.T) {
	consumer := NewConsumer()
	first := consumerSnapshot(5, 4)
	first.Snapshot.Deliveries = []WakeDeliveryState{consumerWake("wake-old")}
	first.Snapshot.Diagnostics = []AgentDiagnostic{consumerDiagnostic("diag-old")}
	require.Equal(t, ApplyAccepted, consumer.Apply(first))

	next := consumerSnapshot(6, 1)
	next.Snapshot.Deliveries = []WakeDeliveryState{consumerWake("wake-new")}
	next.Snapshot.Diagnostics = []AgentDiagnostic{consumerDiagnostic("diag-new")}
	require.Equal(t, ApplyAccepted, consumer.Apply(next))

	state, ok := consumer.State()
	require.True(t, ok)
	require.Equal(t, uint64(6), state.SessionEpoch)
	require.Equal(t, uint64(1), state.Revision)
	require.NotContains(t, state.Deliveries, "wake-old")
	require.Contains(t, state.Deliveries, "wake-new")
	require.NotContains(t, state.Diagnostics, "diag-old")
	require.Contains(t, state.Diagnostics, "diag-new")
}

func TestConsumerAppliesDeltaUpsertsAndRemovals(t *testing.T) {
	consumer := NewConsumer()
	snapshot := consumerSnapshot(3, 1)
	snapshot.Snapshot.Deliveries = []WakeDeliveryState{
		consumerWake("wake-keep"),
		consumerWake("wake-remove"),
	}
	snapshot.Snapshot.Diagnostics = []AgentDiagnostic{
		consumerDiagnostic("diag-keep"),
		consumerDiagnostic("diag-remove"),
	}
	require.Equal(t, ApplyAccepted, consumer.Apply(snapshot))

	delta := consumerDelta(3, 1, 2)
	running := AgentExecutionState{
		Phase:        ExecutionRunning,
		RunningKind:  RunningThinking,
		AttemptToken: "attempt-1",
		ChangedAt:    consumerTime(),
	}
	delta.Delta.Execution = &running
	delta.Delta.DeliveryUpserts = []WakeDeliveryState{consumerWake("wake-new")}
	delta.Delta.DeliveryRemovals = []string{"wake-remove"}
	delta.Delta.DiagnosticUpserts = []AgentDiagnostic{consumerDiagnostic("diag-new")}
	delta.Delta.DiagnosticRemovals = []string{"diag-remove"}

	require.Equal(t, ApplyAccepted, consumer.Apply(delta))
	state, ok := consumer.State()
	require.True(t, ok)
	require.Equal(t, ExecutionRunning, state.Execution.Phase)
	require.Contains(t, state.Deliveries, "wake-keep")
	require.Contains(t, state.Deliveries, "wake-new")
	require.NotContains(t, state.Deliveries, "wake-remove")
	require.Contains(t, state.Diagnostics, "diag-keep")
	require.Contains(t, state.Diagnostics, "diag-new")
	require.NotContains(t, state.Diagnostics, "diag-remove")
}

func TestConsumerStateReturnsDeepCopy(t *testing.T) {
	consumer := NewConsumer()
	snapshot := consumerSnapshot(3, 1)
	snapshot.Snapshot.Deliveries = []WakeDeliveryState{consumerWake("wake-1")}
	snapshot.Snapshot.Diagnostics = []AgentDiagnostic{consumerDiagnostic("diag-1")}
	require.Equal(t, ApplyAccepted, consumer.Apply(snapshot))

	first, ok := consumer.State()
	require.True(t, ok)
	first.Deliveries["wake-1"] = consumerWake("mutated")
	first.Diagnostics["diag-1"] = consumerDiagnostic("mutated")

	second, ok := consumer.State()
	require.True(t, ok)
	require.Equal(t, "wake-1", second.Deliveries["wake-1"].LocalWakeID)
	require.Equal(t, "diag-1", second.Diagnostics["diag-1"].DiagnosticID)
}

func consumerSnapshot(epoch, revision uint64) AgentStateEnvelopeV2 {
	snapshot := AgentStateSnapshotV2{
		EnvelopeBaseV2: EnvelopeBaseV2{
			Version:      ProtocolVersion,
			AgentID:      "agt-1",
			SessionEpoch: epoch,
			Revision:     revision,
			EmittedAt:    consumerTime(),
		},
		Mode: SnapshotMode,
		Execution: AgentExecutionState{
			Phase:     ExecutionIdle,
			ChangedAt: consumerTime(),
		},
		Deliveries:  []WakeDeliveryState{},
		Diagnostics: []AgentDiagnostic{},
	}
	return SnapshotEnvelope(snapshot)
}

func consumerDelta(epoch, baseRevision, revision uint64) AgentStateEnvelopeV2 {
	delta := AgentStateDeltaV2{
		EnvelopeBaseV2: EnvelopeBaseV2{
			Version:      ProtocolVersion,
			AgentID:      "agt-1",
			SessionEpoch: epoch,
			Revision:     revision,
			EmittedAt:    consumerTime(),
		},
		Mode:         DeltaMode,
		BaseRevision: baseRevision,
		DeliveryRemovals: []string{
			"wake-absent",
		},
	}
	return DeltaEnvelope(delta)
}

func consumerWake(localWakeID string) WakeDeliveryState {
	return WakeDeliveryState{
		LocalWakeID:     localWakeID,
		SourceMessageID: "msg-" + localWakeID,
		TargetKey:       "#ops",
		AckedAt:         consumerTime(),
		ChangedAt:       consumerTime(),
		Phase:           WakePhaseReceived,
	}
}

func consumerDiagnostic(diagnosticID string) AgentDiagnostic {
	return AgentDiagnostic{
		DiagnosticID:  diagnosticID,
		TargetKey:     "#ops",
		LocalWakeIDs:  []string{"wake-1"},
		CreatedAt:     consumerTime(),
		Category:      DiagnosticDelivery,
		Terminal:      true,
		FailureReason: WakeFailureRuntimeFailed,
	}
}

func consumerTime() time.Time {
	return time.Date(2026, 8, 19, 4, 0, 0, 0, time.UTC)
}
