package daemon

import (
	"context"
	"testing"

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

// P3: clientState stores the daemon-derived turn state and reports unknown for
// absent/cleared states; replaceReadyAgentTurnStates replays the signal across a
// reconnect and drops states outside the running set.
func TestClientAgentTurnStateTracksConnectionScopedSignal(t *testing.T) {
	client := &clientState{
		runningAgents:         map[string]bool{},
		startingAgents:        map[string]bool{},
		stoppedAgents:         map[string]bool{},
		agentStatuses:         map[string]string{},
		agentActivities:       map[string]agentActivityState{},
		agentSessions:         map[string]string{},
		agentTurnStateRecords: map[string]agentTurnStateRecord{},
	}

	// Unknown before any report.
	state, ok := client.agentTurnState("agt_x")
	require.False(t, ok)
	require.Empty(t, state)

	// idle live session is reported as idle (recoverable), not working. No
	// sessionId fence applies when the daemon omits it (old daemon / plain event).
	client.setAgentTurnState("agt_x", "idle", "", 0, "")
	state, ok = client.agentTurnState("agt_x")
	require.True(t, ok)
	require.Equal(t, "idle", state)

	// active / queued are working.
	client.setAgentTurnState("agt_x", "active", "", 0, "")
	state, _ = client.agentTurnState("agt_x")
	require.Equal(t, "active", state)
	client.setAgentTurnState("agt_x", "queued", "", 0, "")
	state, _ = client.agentTurnState("agt_x")
	require.Equal(t, "queued", state)

	// Empty clears to unknown.
	client.setAgentTurnState("agt_x", "", "", 0, "")
	_, ok = client.agentTurnState("agt_x")
	require.False(t, ok)

	// Stale-session fence: a turn_state whose sessionId does not match the
	// current agent session is DROPPED and the current known state is preserved
	// (never cleared to unknown); only a matching sessionId is accepted.
	client.setAgentSession("agt_x", "sess_new")
	client.setAgentTurnState("agt_x", "queued", "sess_new", 0, "")
	state, ok = client.agentTurnState("agt_x")
	require.True(t, ok)
	require.Equal(t, "queued", state)
	client.setAgentTurnState("agt_x", "idle", "sess_stale", 0, "")
	state, ok = client.agentTurnState("agt_x")
	require.True(t, ok, "a stale turn_state must be dropped, preserving the current known state")
	require.Equal(t, "queued", state, "the current session's known state must not be overwritten by a stale event")

	// Reconnect replay keeps only states inside the running agent set.
	client.setAgentTurnState("agt_live", "idle", "", 0, "")
	client.setAgentTurnState("agt_gone", "active", "", 0, "")
	client.replaceReadyAgentTurnStates([]string{"agt_live"}, map[string]string{
		"agt_live": "idle",
		"agt_gone": "active",
		"agt_new":  "queued",
	}, nil)
	state, ok = client.agentTurnState("agt_live")
	require.True(t, ok)
	require.Equal(t, "idle", state)
	_, ok = client.agentTurnState("agt_gone")
	require.False(t, ok, "states outside the running set must be dropped")
	_, ok = client.agentTurnState("agt_new")
	require.False(t, ok, "states outside the running set must be dropped")
}

// P3: Hub.AgentRuntimeTurnState returns unknown when the machine is not connected
// and the reported state when it is.
func TestHubAgentRuntimeTurnState(t *testing.T) {
	hub := &Hub{clients: map[string]*clientState{}}
	_, ok := hub.AgentRuntimeTurnState("mac_missing", "agt_x")
	require.False(t, ok, "a disconnected machine must report unknown")

	client := &clientState{
		runningAgents:         map[string]bool{},
		startingAgents:        map[string]bool{},
		stoppedAgents:         map[string]bool{},
		agentStatuses:         map[string]string{},
		agentActivities:       map[string]agentActivityState{},
		agentSessions:         map[string]string{},
		agentTurnStateRecords: map[string]agentTurnStateRecord{"agt_x": {State: "idle"}},
	}
	hub.clients["mac_1"] = client
	state, ok := hub.AgentRuntimeTurnState("mac_1", "agt_x")
	require.True(t, ok)
	require.Equal(t, "idle", state)

	// An old daemon never reported a state -> unknown (fail-safe).
	client.setAgentTurnState("agt_old", "", "", 0, "")
	_, ok = hub.AgentRuntimeTurnState("mac_1", "agt_old")
	require.False(t, ok)
}

// P3: handleSessionLinkAgentTurnStateCommand stores a valid turn state. With a
// nil store the handler skips the machine-ownership validation and stores the
// state directly (the ownership fence is covered by the store-backed path).
func TestHandleSessionLinkAgentTurnStateCommand(t *testing.T) {
	hub := &Hub{}
	client := &clientState{agentTurnStateRecords: map[string]agentTurnStateRecord{}}

	err := handleSessionLinkAgentTurnStateCommand(hub, context.Background(), client, []byte(`{"type":"agent:turn_state","agentId":"agt_1","turnState":"idle","sessionId":"sess_1"}`))
	require.NoError(t, err)
	state, ok := client.agentTurnState("agt_1")
	require.False(t, ok, "a turn_state with a sessionId that does not match the current session is dropped")

	// With the current session established, a matching turn_state is accepted.
	client.setAgentSession("agt_1", "sess_1")
	err = handleSessionLinkAgentTurnStateCommand(hub, context.Background(), client, []byte(`{"type":"agent:turn_state","agentId":"agt_1","turnState":"idle","sessionId":"sess_1"}`))
	require.NoError(t, err)
	state, ok = client.agentTurnState("agt_1")
	require.True(t, ok)
	require.Equal(t, "idle", state)

	// Empty state clears to unknown.
	err = handleSessionLinkAgentTurnStateCommand(hub, context.Background(), client, []byte(`{"type":"agent:turn_state","agentId":"agt_1","turnState":"","sessionId":"sess_1"}`))
	require.NoError(t, err)
	_, ok = client.agentTurnState("agt_1")
	require.False(t, ok)

	// Missing agentId is a protocol violation.
	err = handleSessionLinkAgentTurnStateCommand(hub, context.Background(), client, []byte(`{"type":"agent:turn_state","turnState":"idle"}`))
	require.Error(t, err)
}
