package agentstate

import (
	"errors"
	"testing"
	"time"

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

func TestPublisherStartsEachEpochWithSnapshot(t *testing.T) {
	p := NewPublisher("agt-1", 41, publisherClock)
	snapshot, err := p.Snapshot(publisherState())
	require.NoError(t, err)
	require.Equal(t, uint64(42), snapshot.SessionEpoch)
	require.Equal(t, uint64(1), snapshot.Revision)
	require.Equal(t, SnapshotMode, snapshot.Mode)

	running := publisherExecution(ExecutionRunning)
	running.RunningKind = RunningThinking
	delta, err := p.Delta(ChangeSet{Execution: &running})
	require.NoError(t, err)
	require.Equal(t, uint64(1), delta.BaseRevision)
	require.Equal(t, uint64(2), delta.Revision)
	require.Equal(t, DeltaMode, delta.Mode)
}

func TestPublisherRejectsDeltaBeforeSnapshot(t *testing.T) {
	p := NewPublisher("agt-1", 0, publisherClock)
	running := publisherExecution(ExecutionRunning)
	running.RunningKind = RunningThinking

	_, err := p.Delta(ChangeSet{Execution: &running})
	require.ErrorIs(t, err, ErrSnapshotRequired)
}

func TestPublisherEmptyChangeDoesNotConsumeRevision(t *testing.T) {
	p := NewPublisher("agt-1", 0, publisherClock)
	_, err := p.Snapshot(publisherState())
	require.NoError(t, err)

	_, err = p.Delta(ChangeSet{})
	require.ErrorIs(t, err, ErrEmptyChangeSet)
	require.Equal(t, uint64(1), p.Revision())

	running := publisherExecution(ExecutionRunning)
	running.RunningKind = RunningThinking
	delta, err := p.Delta(ChangeSet{Execution: &running})
	require.NoError(t, err)
	require.Equal(t, uint64(2), delta.Revision)
}

func TestPublisherRequireSnapshotPreservesEpochAndAdvancesRevision(t *testing.T) {
	p := NewPublisher("agt-1", 5, publisherClock)
	first, err := p.Snapshot(publisherState())
	require.NoError(t, err)

	p.RequireSnapshot()
	running := publisherExecution(ExecutionRunning)
	running.RunningKind = RunningThinking
	_, err = p.Delta(ChangeSet{Execution: &running})
	require.ErrorIs(t, err, ErrSnapshotRequired)

	next, err := p.Snapshot(publisherState())
	require.NoError(t, err)
	require.Equal(t, first.SessionEpoch, next.SessionEpoch)
	require.Equal(t, first.Revision+1, next.Revision)
}

func TestPublisherRecreateStartsHigherEpochAtRevisionOne(t *testing.T) {
	p := NewPublisher("agt-1", 5, publisherClock)
	_, err := p.Snapshot(publisherState())
	require.NoError(t, err)

	p.NewEpoch(9)
	snapshot, err := p.Snapshot(publisherState())
	require.NoError(t, err)
	require.Equal(t, uint64(10), snapshot.SessionEpoch)
	require.Equal(t, uint64(1), snapshot.Revision)
}

func TestPublisherEmitsTerminalDeliveryAsUpsert(t *testing.T) {
	p := NewPublisher("agt-1", 0, publisherClock)
	_, err := p.Snapshot(publisherState())
	require.NoError(t, err)

	terminal := publisherWake(WakePhaseReplyConfirmed)
	terminal.ReplyObjectIDs = []string{"reply-1"}
	delta, err := p.Delta(ChangeSet{
		DeliveryUpserts: []WakeDeliveryState{terminal},
	})
	require.NoError(t, err)
	require.Len(t, delta.DeliveryUpserts, 1)
	require.Empty(t, delta.DeliveryRemovals)
}

func TestPublisherReturnsCopiesOfMutableInputs(t *testing.T) {
	state := publisherState()
	p := NewPublisher("agt-1", 0, publisherClock)
	snapshot, err := p.Snapshot(state)
	require.NoError(t, err)

	state.Deliveries[0].ReplyObjectIDs = []string{"mutated"}
	state.Diagnostics[0].LocalWakeIDs[0] = "mutated"
	require.Empty(t, snapshot.Deliveries[0].ReplyObjectIDs)
	require.Equal(t, "wake-1", snapshot.Diagnostics[0].LocalWakeIDs[0])
}

func TestPublisherRejectsInvalidClock(t *testing.T) {
	p := NewPublisher("agt-1", 0, func() time.Time { return time.Time{} })
	_, err := p.Snapshot(publisherState())
	require.True(t, errors.Is(err, ErrInvalidEnvelope))
}

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

func publisherExecution(phase ExecutionPhase) AgentExecutionState {
	state := AgentExecutionState{
		Phase:     phase,
		ChangedAt: publisherClock(),
	}
	switch phase {
	case ExecutionRunning:
		state.RunningKind = RunningWorking
	case ExecutionRetryWait:
		state.ReasonCode = ExecutionReasonRateLimited
	case ExecutionRecovering:
		state.ReasonCode = ExecutionReasonProcessInterrupted
	case ExecutionFailed:
		state.ReasonCode = ExecutionReasonRuntimeFailed
	}
	return state
}

func publisherWake(phase WakeDeliveryPhase) WakeDeliveryState {
	state := WakeDeliveryState{
		LocalWakeID:     "wake-1",
		SourceMessageID: "msg-1",
		TargetKey:       "#ops",
		AckedAt:         publisherClock(),
		ChangedAt:       publisherClock(),
		Phase:           phase,
	}
	switch phase {
	case WakePhaseReplyConfirmed:
		state.ReplyObjectIDs = []string{"reply-1"}
	case WakePhaseNoReply:
		state.NoReplyReasonCode = WakeNoReplyDecided
	case WakePhaseFailed:
		state.FailureReasonCode = WakeFailureRuntimeFailed
	}
	return state
}

func publisherState() State {
	return State{
		Execution:  publisherExecution(ExecutionIdle),
		Deliveries: []WakeDeliveryState{publisherWake(WakePhaseReceived)},
		Diagnostics: []AgentDiagnostic{{
			DiagnosticID:  "diag-1",
			TargetKey:     "#ops",
			LocalWakeIDs:  []string{"wake-1"},
			CreatedAt:     publisherClock(),
			Category:      DiagnosticDelivery,
			Terminal:      true,
			FailureReason: WakeFailureRuntimeFailed,
		}},
	}
}
