package agentruntime

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
)

var errFakeResolver = errors.New("fake resolver failure")

// fakeResolver resolves every agent to one fixed machine unless overridden.
type fakeResolver struct {
	machineID string
	err       error
}

func (resolver fakeResolver) MachineIDForAgent(_ context.Context, _ string, _ string) (string, error) {
	if resolver.err != nil {
		return "", resolver.err
	}
	return resolver.machineID, nil
}

// fakeOverlay is a deterministic runtimeOverlay double exposing only the
// daemon-derived turn state and connection anchor. It has no concept of the
// Workflow capacity lease or callback FIFO, so the tests prove the reader never
// reads them.
type fakeOverlay struct {
	machineConnected bool
	connectedAt      time.Time
	turnState        string
	turnStateKnown   bool
	sessionGen       string
	guard            sync.RWMutex
	released         bool
}

func (overlay *fakeOverlay) MachineConnected(string) bool { return overlay.machineConnected }
func (overlay *fakeOverlay) MachineConnectedAt(string) (time.Time, bool) {
	if !overlay.machineConnected {
		return time.Time{}, false
	}
	return overlay.connectedAt, true
}
func (overlay *fakeOverlay) AgentRuntimeTurnState(string, string) (string, bool) {
	return overlay.turnState, overlay.turnStateKnown
}

func (overlay *fakeOverlay) AcquireObservationGuard(machineID string) (func(), bool) {
	if !overlay.machineConnected {
		return nil, false
	}
	overlay.guard.RLock()
	var once sync.Once
	return func() {
		once.Do(func() {
			overlay.released = true
			overlay.guard.RUnlock()
		})
	}, true
}

func (overlay *fakeOverlay) ObserveAgentRuntime(machineID, agentID string) (run.RecoveryObservation, bool) {
	if !overlay.machineConnected {
		return run.RecoveryObservation{}, false
	}
	return run.RecoveryObservation{
		MachineID:         machineID,
		SessionGeneration: overlay.sessionGen,
		Connected:         true,
		ConnectedAt:       overlay.connectedAt,
		State:             overlay.turnState,
		StateKnown:        overlay.turnStateKnown,
	}, true
}

// fixedClock builds a reader with a deterministic clock and grace.
func fixedClockReader(resolver MachineResolver, overlay runtimeOverlay, now time.Time, grace time.Duration) *HubWorkingReader {
	return NewHubWorkingReaderWithOptions(resolver, overlay, HubWorkingReaderOptions{Now: func() time.Time { return now }, Grace: grace})
}

// P4: idle -> working=false/known=true (fires); active/queued -> working=true;
// unknown/absent -> no send, no timing.
func TestHubWorkingReaderIdleIsRecoverable(t *testing.T) {
	// A live idle session: process/session up (machine connected) but no active or
	// queued turn. The old presence inference read this as working; the turn-state
	// reader must report idle (recoverable).
	reader := NewHubWorkingReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		turnState:        AgentTurnStateIdle,
		turnStateKnown:   true,
	})
	working, known := reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known)
	require.False(t, working, "a live idle session must be idle and recoverable")
}

func TestHubWorkingReaderActiveTurnNeverRecovers(t *testing.T) {
	// A real executing turn persists beyond any minutes: active, never recovered.
	reader := NewHubWorkingReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		turnState:        AgentTurnStateActive,
		turnStateKnown:   true,
	})
	working, known := reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known)
	require.True(t, working, "an active turn must never be recovered")
}

func TestHubWorkingReaderRealQueuedTurnIsWorking(t *testing.T) {
	// A real delivered message waiting for/inside the runtime execution slot:
	// queued, not recoverable. This is the daemon's real execution queue, not the
	// Workflow callback FIFO.
	reader := NewHubWorkingReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		turnState:        AgentTurnStateQueued,
		turnStateKnown:   true,
	})
	working, known := reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known)
	require.True(t, working, "a real queued turn must be working")
}

// TestHubWorkingReaderOnlineNoSnapshotDefersWithinGrace proves an online
// responder whose turn state is not yet known is deferred (no send, no timing)
// for a bounded grace anchored at the connection, not forever.
func TestHubWorkingReaderOnlineNoSnapshotDefersWithinGrace(t *testing.T) {
	now := time.Date(2026, 8, 16, 12, 0, 0, 0, time.UTC)
	grace := 60 * time.Second
	reader := fixedClockReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		connectedAt:      now.Add(-30 * time.Second), // 30s into a 60s grace
		turnStateKnown:   false,
	}, now, grace)
	working, known := reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.False(t, known, "within grace the state is not determinable (deferred)")
	require.False(t, working, "within grace must never be treated as working")
}

// TestHubWorkingReaderOnlineNoSnapshotRecoverableAfterGrace proves an online
// responder with no current snapshot becomes recoverable once the bounded grace
// elapses, so recovery can never stall on an empty Hub after a daemon restart.
func TestHubWorkingReaderOnlineNoSnapshotRecoverableAfterGrace(t *testing.T) {
	now := time.Date(2026, 8, 16, 12, 0, 0, 0, time.UTC)
	grace := 60 * time.Second
	reader := fixedClockReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		connectedAt:      now.Add(-grace).Add(-time.Second), // just past grace
		turnStateKnown:   false,
	}, now, grace)
	working, known := reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known, "after grace the responder becomes recoverable")
	require.False(t, working, "no active/queued turn means recoverable, not working")
}

// TestHubWorkingReaderAgentMissingFromSnapshotRecoverableAfterGrace proves a
// responder absent from an otherwise-received snapshot follows the same bounded
// grace: within grace deferred, after grace recoverable.
func TestHubWorkingReaderAgentMissingFromSnapshotRecoverableAfterGrace(t *testing.T) {
	now := time.Date(2026, 8, 16, 12, 0, 0, 0, time.UTC)
	grace := 60 * time.Second
	within := fixedClockReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		connectedAt:      now.Add(-10 * time.Second),
		turnStateKnown:   false,
	}, now, grace)
	working, known := within.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.False(t, known, "missing agent within grace defers")
	require.False(t, working)

	after := fixedClockReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		connectedAt:      now.Add(-grace).Add(-time.Second),
		turnStateKnown:   false,
	}, now, grace)
	working, known = after.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known, "missing agent after grace becomes recoverable")
	require.False(t, working)
}

// TestHubWorkingReaderGraceAnchorIsConnectionFixed proves the grace anchor is
// the connection timestamp and never moves: a duplicate ready or a stale event
// (here modeled by an overlay whose snapshotAt would move but connectedAt stays)
// cannot extend the window.
func TestHubWorkingReaderGraceAnchorIsConnectionFixed(t *testing.T) {
	now := time.Date(2026, 8, 16, 12, 0, 0, 0, time.UTC)
	grace := 60 * time.Second
	// The connection is long past grace (connectedAt never moves), so even though
	// the agent turn state is still unknown the responder is already recoverable.
	reader := fixedClockReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		connectedAt:      now.Add(-5 * time.Minute),
		turnStateKnown:   false,
	}, now, grace)
	working, known := reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known, "a long-connected machine with no snapshot is recoverable regardless of duplicate ready")
	require.False(t, working)
}

// TestHubWorkingReaderOfflineDefersButResumesAfterReconnect proves offline is a
// deferral (no send, no timing) and the same responder becomes recoverable once
// the machine reconnects and reports an idle snapshot.
func TestHubWorkingReaderOfflineDefersButResumesAfterReconnect(t *testing.T) {
	now := time.Date(2026, 8, 16, 12, 0, 0, 0, time.UTC)
	grace := 60 * time.Second

	offline := fixedClockReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: false,
		turnStateKnown:   false,
	}, now, grace)
	working, known := offline.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.False(t, known, "offline defers (unknown, no timing)")
	require.False(t, working)

	online := fixedClockReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		connectedAt:      now.Add(-2 * time.Minute),
		turnState:        AgentTurnStateIdle,
		turnStateKnown:   true,
	}, now, grace)
	working, known = online.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known, "after reconnect + idle snapshot the responder is recoverable")
	require.False(t, working)
}

func TestHubWorkingReaderUnknownDoesNotSend(t *testing.T) {
	// Leader seq 68: after server/worker restart the Hub has not reconnected (or
	// the responder's machine cannot be resolved), so the state is unknown and the
	// tick must NOT send nor start timing. An online machine without a snapshot is
	// the bounded-grace case covered separately above.
	cases := []struct {
		name   string
		reader *HubWorkingReader
	}{
		{"unresolvable machine", NewHubWorkingReader(fakeResolver{machineID: ""}, &fakeOverlay{machineConnected: true, turnStateKnown: true})},
		{"resolver error", NewHubWorkingReader(fakeResolver{err: errFakeResolver}, &fakeOverlay{machineConnected: true, turnStateKnown: true})},
		{"machine not connected", NewHubWorkingReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{machineConnected: false, turnStateKnown: true})},
		{"nil hub", NewHubWorkingReader(fakeResolver{machineID: "mac_1"}, nil)},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			working, known := tc.reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
			require.False(t, known, "unknown state must never be treated as determinable")
			require.False(t, working, "unknown state must never be treated as working (never fall back to lease/FIFO/presence)")
		})
	}
}

// TestHubWorkingReaderFifoBacklogDoesNotMaskStoppedHead proves the reader is
// turn-state-only: a same-responder Workflow callback FIFO tail (which the reader
// cannot even see) never masks an idle head.
func TestHubWorkingReaderFifoBacklogDoesNotMaskStoppedHead(t *testing.T) {
	reader := NewHubWorkingReader(fakeResolver{machineID: "mac_1"}, &fakeOverlay{
		machineConnected: true,
		turnState:        AgentTurnStateIdle,
		turnStateKnown:   true,
	})
	working, known := reader.RuntimeWorking(context.Background(), "wsp_1", "agt_1")
	require.True(t, known)
	require.False(t, working, "Workflow callback FIFO backlog must never mask an idle head")
}
