package daemon

import (
	"context"
	"encoding/json"
	"sync"
	"testing"

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

// terminalLifecycleFenceStore records status/activity/session mutations so the
// tests can prove a stale terminal frame is dropped BEFORE any store mutation
// while a matching current-generation frame still applies.
type terminalLifecycleFenceStore struct {
	recordingStartStore
	mu           sync.Mutex
	statuses     []string // agentID:status per accepted mutation
	activities   []string // agentID:activity per accepted mutation
	sessionDelta []string // agentID:sessionID per accepted mutation
}

func (store *terminalLifecycleFenceStore) UpdateAgentStatusFromMachine(_ context.Context, _ string, agentID string, status string) (bool, error) {
	store.mu.Lock()
	defer store.mu.Unlock()
	store.statuses = append(store.statuses, agentID+":"+status)
	return true, nil
}

func (store *terminalLifecycleFenceStore) UpdateAgentActivityFromMachine(_ context.Context, _ string, agentID string, activity string, detail string) (bool, error) {
	store.mu.Lock()
	defer store.mu.Unlock()
	store.activities = append(store.activities, agentID+":"+activity)
	return true, nil
}

func (store *terminalLifecycleFenceStore) UpdateAgentSessionFromMachine(_ context.Context, _ string, agentID string, sessionID string, persistLegacySession bool) (bool, error) {
	store.mu.Lock()
	defer store.mu.Unlock()
	store.sessionDelta = append(store.sessionDelta, agentID+":"+sessionID)
	return true, nil
}

func (store *terminalLifecycleFenceStore) statusSnapshot() []string {
	store.mu.Lock()
	defer store.mu.Unlock()
	return append([]string(nil), store.statuses...)
}

func (store *terminalLifecycleFenceStore) activitySnapshot() []string {
	store.mu.Lock()
	defer store.mu.Unlock()
	return append([]string(nil), store.activities...)
}

func (store *terminalLifecycleFenceStore) sessionSnapshot() []string {
	store.mu.Lock()
	defer store.mu.Unlock()
	return append([]string(nil), store.sessionDelta...)
}

// newTerminalLifecycleFenceClient builds a client whose current agent session is
// the given value with the agent running.
func newTerminalLifecycleFenceClient(currentSession string) *clientState {
	client := &clientState{
		machineID:             "mac_1",
		workspaceID:           "wsp_1",
		createdByUserID:       "usr_1",
		agentSessions:         map[string]string{},
		runningAgents:         map[string]bool{},
		startingAgents:        map[string]bool{},
		stoppedAgents:         map[string]bool{},
		agentStatuses:         map[string]string{},
		agentActivities:       map[string]agentActivityState{},
		agentTurnStateRecords: map[string]agentTurnStateRecord{},
	}
	if currentSession != "" {
		client.agentSessions["agt_1"] = currentSession
		client.runningAgents["agt_1"] = true
		client.agentStatuses["agt_1"] = "active"
	}
	return client
}

// routeTerminalStatus sends one agent:status frame through the real daemon
// protocol router into the given client.
func routeTerminalStatus(t *testing.T, hub *Hub, client *clientState, status, expectedSessionID string) {
	t.Helper()
	payload := map[string]any{"type": "agent:status", "agentId": "agt_1", "status": status}
	if expectedSessionID != "" {
		payload["expectedSessionId"] = expectedSessionID
	}
	raw, err := json.Marshal(payload)
	require.NoError(t, err)
	require.NoError(t, hub.handleMessage(context.Background(), client, raw))
}

// routeTerminalActivity sends one agent:activity frame through the real daemon
// protocol router into the given client.
func routeTerminalActivity(t *testing.T, hub *Hub, client *clientState, activity, detail, expectedSessionID string) {
	t.Helper()
	payload := map[string]any{"type": "agent:activity", "agentId": "agt_1", "activity": activity, "detail": detail}
	if expectedSessionID != "" {
		payload["expectedSessionId"] = expectedSessionID
	}
	raw, err := json.Marshal(payload)
	require.NoError(t, err)
	require.NoError(t, hub.handleMessage(context.Background(), client, raw))
}

// TestTerminalLifecycleFenceMatchingStatusApplies proves a terminal inactive
// status frame carrying the CURRENT session identity still applies: store
// mutation, running set cleared, status updated.
func TestTerminalLifecycleFenceMatchingStatusApplies(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalStatus(t, hub, client, "inactive", "session-2")
	require.Equal(t, []string{"agt_1:inactive"}, store.statusSnapshot(),
		"a matching terminal status must reach the store exactly once")
	require.False(t, client.isAgentRunning("agt_1"), "a matching terminal status must mark the agent not running")
	require.Equal(t, "inactive", client.agentStatus("agt_1"))
}

// TestTerminalLifecycleFenceStaleStatusDropped proves a delayed stop from a
// replaced generation — a terminal status carrying an expected session that is
// NOT the current session — is dropped ENTIRELY: no store mutation, no realtime/
// current status or running mutation, the replacement session/status preserved.
func TestTerminalLifecycleFenceStaleStatusDropped(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalStatus(t, hub, client, "inactive", "session-1")
	require.Empty(t, store.statusSnapshot(), "a stale terminal status must be dropped before any store mutation")
	require.True(t, client.isAgentRunning("agt_1"), "a stale terminal status must not clear the running state")
	require.Equal(t, "active", client.agentStatus("agt_1"), "a stale terminal status must preserve the replacement status")
}

// TestTerminalLifecycleFenceLegacyStatusUnconditional proves a legacy status
// frame WITHOUT the optional expected-session fence keeps the unconditional
// behavior.
func TestTerminalLifecycleFenceLegacyStatusUnconditional(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalStatus(t, hub, client, "inactive", "")
	require.Equal(t, []string{"agt_1:inactive"}, store.statusSnapshot(), "a legacy status must reach the store once")
	require.False(t, client.isAgentRunning("agt_1"))
	require.Equal(t, "inactive", client.agentStatus("agt_1"))
}

// TestTerminalLifecycleFenceMatchingActivityApplies proves a terminal offline
// activity frame carrying the CURRENT session identity still applies: store
// mutation and current activity updated.
func TestTerminalLifecycleFenceMatchingActivityApplies(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalActivity(t, hub, client, "offline", "process exited normally", "session-2")
	require.Equal(t, []string{"agt_1:offline"}, store.activitySnapshot(),
		"a matching terminal activity must reach the store exactly once")
	activity, detail := client.agentActivity("agt_1")
	require.Equal(t, "offline", activity)
	require.Equal(t, "process exited normally", detail)
}

// TestTerminalLifecycleFenceStaleActivityDropped proves a delayed stop from a
// replaced generation — a terminal offline activity carrying an expected session
// that is NOT the current session — is dropped ENTIRELY: no store mutation, no
// current activity mutation.
func TestTerminalLifecycleFenceStaleActivityDropped(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalActivity(t, hub, client, "offline", "process exited normally", "session-1")
	require.Empty(t, store.activitySnapshot(), "a stale terminal activity must be dropped before any store mutation")
	activity, _ := client.agentActivity("agt_1")
	require.Equal(t, "", activity, "a stale terminal activity must preserve the replacement activity")
}

// TestTerminalLifecycleFenceLegacyActivityUnconditional proves a legacy activity
// frame WITHOUT the optional expected-session fence keeps the unconditional
// behavior.
func TestTerminalLifecycleFenceLegacyActivityUnconditional(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalActivity(t, hub, client, "offline", "process exited normally", "")
	require.Equal(t, []string{"agt_1:offline"}, store.activitySnapshot(), "a legacy activity must reach the store once")
	activity, _ := client.agentActivity("agt_1")
	require.Equal(t, "offline", activity)
}

// TestTerminalLifecycleFullStopOrder proves a full matching current-generation
// stop (status + activity + session clear) applies in the expected order through
// the real Hub: the terminal status and activity land, then the session clears.
func TestTerminalLifecycleFullStopOrder(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalStatus(t, hub, client, "inactive", "session-2")
	routeTerminalActivity(t, hub, client, "offline", "process exited normally", "session-2")
	routeSession(t, hub, client, "", "session-2")
	require.Equal(t, []string{"agt_1:inactive"}, store.statusSnapshot())
	require.Equal(t, []string{"agt_1:offline"}, store.activitySnapshot())
	require.Equal(t, []string{"agt_1:"}, store.sessionSnapshot(), "a matching full stop must clear the session exactly once")
	require.Empty(t, client.agentSession("agt_1"), "a matching full stop must remove the current session")
	require.False(t, client.isAgentRunning("agt_1"))
}

// TestTerminalLifecycleStaleFullStopRejected proves a full stale stop from a
// replaced generation — every terminal frame carrying the dying (non-current)
// session — is rejected wholesale: no store mutation, no running/status/activity/
// session mutation, the replacement session-2 preserved as active.
func TestTerminalLifecycleStaleFullStopRejected(t *testing.T) {
	store := &terminalLifecycleFenceStore{}
	hub := &Hub{store: store}
	client := newTerminalLifecycleFenceClient("session-2")
	routeTerminalStatus(t, hub, client, "inactive", "session-1")
	routeTerminalActivity(t, hub, client, "offline", "process exited normally", "session-1")
	routeSession(t, hub, client, "", "session-1")
	require.Empty(t, store.statusSnapshot(), "a stale full stop must drop the terminal status before any store mutation")
	require.Empty(t, store.activitySnapshot(), "a stale full stop must drop the terminal activity before any store mutation")
	require.Empty(t, store.sessionSnapshot(), "a stale full stop must drop the session clear before any store mutation")
	require.Equal(t, "session-2", client.agentSession("agt_1"), "a stale full stop must preserve the replacement session")
	require.True(t, client.isAgentRunning("agt_1"), "a stale full stop must preserve the replacement running state")
	require.Equal(t, "active", client.agentStatus("agt_1"), "a stale full stop must preserve the replacement status")
}
