package daemon

import (
	"testing"

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

// ---------------------------------------------------------------------------
// Exact Agent-session identity (#t2667 cycle-6).
//
// A stored turn-state record is observable as current ONLY when its transport
// generation AND Agent session identity EXACTLY match the current guarded
// generation/session. A legacy no-session record (empty SessionID) is not a
// wildcard: it is observable only while the current Agent session is also empty.
// A non-empty event epoch must equal the current generation even when the
// current generation is empty (a foreign-epoch event is a zero-write). On every
// Agent session replacement or clear (empty→non-empty and non-empty→empty) the
// prior record is atomically retired if not bound to the exact new session.
// ---------------------------------------------------------------------------

func newIdentityClient() *clientState {
	return &clientState{
		machineID:                 "mac_1",
		sessionGeneration:         "g1",
		agentSessions:             map[string]string{},
		agentTurnStateRecords:     map[string]agentTurnStateRecord{},
		runningAgents:             map[string]bool{},
		startingAgents:            map[string]bool{},
		stoppedAgents:             map[string]bool{},
		agentStatuses:             map[string]string{},
		agentActivities:           map[string]agentActivityState{},
		turnStateSnapshotReceived: false,
	}
}

// TestLegacyNoSessionRecordBoundToCurrentEmptySession proves a legacy event
// without a session id is bound to the current (empty) Agent session at atomic
// application and is observable while the session stays empty.
func TestLegacyNoSessionRecordBoundToCurrentEmptySession(t *testing.T) {
	client := newIdentityClient()
	// Legacy event: no epoch, no session. It binds to the current generation g1
	// and the current (empty) session.
	client.setAgentTurnState("agt_x", "idle", "", 0, "")
	obs, ok := client.hub().ObserveAgentRuntime("mac_1", "agt_x")
	_ = obs
	_ = ok
	// Direct record read: bound to g1 and empty session.
	record, known := client.agentTurnStateRecord("agt_x")
	require.True(t, known)
	require.Equal(t, "g1", record.Generation)
	require.Empty(t, record.SessionID)
}

// TestLegacyNoSessionRecordRetiredOnSessionSet proves a legacy no-session
// record is atomically retired when a non-empty Agent session is set: it must
// never survive/authorize the later session identity.
func TestLegacyNoSessionRecordRetiredOnSessionSet(t *testing.T) {
	client := newIdentityClient()
	client.setAgentTurnState("agt_x", "idle", "", 0, "")
	record, known := client.agentTurnStateRecord("agt_x")
	require.True(t, known)
	require.Empty(t, record.SessionID)

	// Setting a session must retire the no-session record.
	client.setAgentSession("agt_x", "sess_new")
	_, known = client.agentTurnStateRecord("agt_x")
	require.False(t, known, "a legacy no-session record must be retired when a session is set")
}

// TestPriorSessionRecordRetiredOnSessionClear proves a prior-session record is
// retired when the session is cleared (non-empty→empty): stale active/idle must
// become unobservable, never drive park/clear.
func TestPriorSessionRecordRetiredOnSessionClear(t *testing.T) {
	client := newIdentityClient()
	client.setAgentSession("agt_x", "sess_old")
	client.setAgentTurnState("agt_x", "active", "sess_old", 0, "")
	_, known := client.agentTurnStateRecord("agt_x")
	require.True(t, known)

	client.setAgentSession("agt_x", "")
	_, known = client.agentTurnStateRecord("agt_x")
	require.False(t, known, "a prior-session record must be retired when the session is cleared")
}

// TestPriorSessionRecordRetiredOnSessionReplace proves a prior-session record is
// retired when the session is replaced (sess_old → sess_new).
func TestPriorSessionRecordRetiredOnSessionReplace(t *testing.T) {
	client := newIdentityClient()
	client.setAgentSession("agt_x", "sess_old")
	client.setAgentTurnState("agt_x", "active", "sess_old", 0, "")
	_, known := client.agentTurnStateRecord("agt_x")
	require.True(t, known)

	client.setAgentSession("agt_x", "sess_new")
	_, known = client.agentTurnStateRecord("agt_x")
	require.False(t, known, "a prior-session record must be retired when the session is replaced")
}

// TestObserveExactSessionMatch proves ObserveAgentRuntime surfaces a record as
// known only when generation AND session match exactly; an empty-record session
// is never a wildcard.
func TestObserveExactSessionMatch(t *testing.T) {
	client := newIdentityClient()
	hub := client.hub()
	hub.clients["mac_1"] = client
	client.setAgentSession("agt_x", "sess_live")
	client.setAgentTurnState("agt_x", "idle", "sess_live", 0, "")
	obs, ok := hub.ObserveAgentRuntime("mac_1", "agt_x")
	require.True(t, ok)
	require.True(t, obs.StateKnown)
	require.Equal(t, "idle", obs.State)

	// A legacy no-session record bound while the session was empty becomes
	// unobservable once the session is set (record retired) and also never a
	// wildcard at observation.
	client2 := newIdentityClient()
	hub2 := client2.hub()
	hub2.clients["mac_2"] = client2
	client2.setAgentTurnState("agt_y", "active", "", 0, "")
	client2.setAgentSession("agt_y", "sess_now")
	obs2, ok := hub2.ObserveAgentRuntime("mac_2", "agt_y")
	require.True(t, ok)
	require.False(t, obs2.StateKnown, "a no-session legacy record must never authorize a later session")
}

// TestNonEmptyEventEpochRejectedWhenCurrentGenerationEmpty proves a non-empty
// event epoch is a zero-write when the current generation is empty (foreign
// connection event), never accepted.
func TestNonEmptyEventEpochRejectedWhenCurrentGenerationEmpty(t *testing.T) {
	client := newIdentityClient()
	client.sessionGeneration = ""
	client.setAgentTurnState("agt_x", "idle", "sess_live", 0, "g_foreign")
	_, known := client.agentTurnStateRecord("agt_x")
	require.False(t, known, "a non-empty event epoch against an empty-generation connection is a zero-write")
	require.Zero(t, client.turnStateHighWater, "a rejected foreign-epoch event must not advance the high-water")
}

// TestNonEmptyEventEpochExactMatch proves a non-empty event epoch exactly equal
// to the current generation applies; any other value is a zero-write.
func TestNonEmptyEventEpochExactMatch(t *testing.T) {
	client := newIdentityClient()
	client.setAgentSession("agt_x", "sess_live")
	client.setAgentTurnState("agt_x", "active", "sess_live", 0, "g1")
	record, known := client.agentTurnStateRecord("agt_x")
	require.True(t, known)
	require.Equal(t, "g1", record.Generation)

	// A different epoch is a zero-write that preserves the current state.
	client.setAgentTurnState("agt_x", "idle", "sess_live", 0, "g_other")
	record, known = client.agentTurnStateRecord("agt_x")
	require.True(t, known)
	require.Equal(t, "active", record.State, "a foreign-epoch event must not overwrite the current state")
}

// TestLowerEqualRevisionDropped proves a lower/equal revision is dropped and a
// matching-session fresh state applies; both transition orders are covered.
func TestLowerEqualRevisionDropped(t *testing.T) {
	client := newIdentityClient()
	client.setAgentSession("agt_x", "sess_live")
	client.setAgentTurnState("agt_x", "active", "sess_live", 5, "g1")
	record, known := client.agentTurnStateRecord("agt_x")
	require.True(t, known)
	require.Equal(t, int64(5), record.Revision)

	// Equal revision dropped.
	client.setAgentTurnState("agt_x", "idle", "sess_live", 5, "g1")
	record, _ = client.agentTurnStateRecord("agt_x")
	require.Equal(t, "active", record.State, "an equal-revision event must be dropped")

	// Lower revision dropped.
	client.setAgentTurnState("agt_x", "queued", "sess_live", 4, "g1")
	record, _ = client.agentTurnStateRecord("agt_x")
	require.Equal(t, "active", record.State, "a lower-revision event must be dropped")

	// Higher revision applies.
	client.setAgentTurnState("agt_x", "idle", "sess_live", 6, "g1")
	record, _ = client.agentTurnStateRecord("agt_x")
	require.Equal(t, "idle", record.State)
}

// TestSessionSetThenStateOldSessionDropped proves the transition order
// session-set-then-old-session-event: after setting sess_new, an event carrying
// the old session is dropped.
func TestSessionSetThenStateOldSessionDropped(t *testing.T) {
	client := newIdentityClient()
	client.setAgentSession("agt_x", "sess_new")
	client.setAgentTurnState("agt_x", "idle", "sess_old", 0, "g1")
	_, known := client.agentTurnStateRecord("agt_x")
	require.False(t, known, "an old-session event after the session was replaced must be dropped")
}

// TestStateThenSessionSetRetiresRecord proves the transition order
// state-then-session-set: a matching-session state applies, then the session
// changes and the record is retired.
func TestStateThenSessionSetRetiresRecord(t *testing.T) {
	client := newIdentityClient()
	client.setAgentSession("agt_x", "sess_old")
	client.setAgentTurnState("agt_x", "active", "sess_old", 0, "g1")
	_, known := client.agentTurnStateRecord("agt_x")
	require.True(t, known)
	client.setAgentSession("agt_x", "sess_new")
	_, known = client.agentTurnStateRecord("agt_x")
	require.False(t, known, "a session change must retire the prior-session record")
}

// agentTurnStateRecord reads the stored record for one agent.
func (client *clientState) agentTurnStateRecord(agentID string) (agentTurnStateRecord, bool) {
	client.mu.Lock()
	defer client.mu.Unlock()
	record, ok := client.agentTurnStateRecords[agentID]
	return record, ok
}

// hub wires a single-client Hub for observation tests.
func (client *clientState) hub() *Hub {
	return &Hub{clients: map[string]*clientState{client.machineID: client}}
}
