// Package session provides session lifecycle management for claude-mnemonic.
package session

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

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
)

// ManagerSuite is a test suite for Manager operations.
type ManagerSuite struct {
	suite.Suite
	manager *Manager
}

func (s *ManagerSuite) SetupTest() {
	// Create manager without real session store (use nil for unit tests)
	s.manager = &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	// Initialize context for manager
	ctx, cancel := context.WithCancel(context.Background())
	s.manager.ctx = ctx
	s.manager.cancel = cancel
}

func (s *ManagerSuite) TearDownTest() {
	if s.manager != nil && s.manager.cancel != nil {
		s.manager.cancel()
	}
}

func TestManagerSuite(t *testing.T) {
	suite.Run(t, new(ManagerSuite))
}

// TestActiveSession tests ActiveSession creation and basic operations.
func (s *ManagerSuite) TestActiveSession() {
	session := &ActiveSession{
		SessionDBID:     1,
		ClaudeSessionID: "claude-123",
		SDKSessionID:    "sdk-123",
		Project:         "test-project",
		UserPrompt:      "Hello",
		StartTime:       time.Now(),
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
	}

	s.Equal(int64(1), session.SessionDBID)
	s.Equal("claude-123", session.ClaudeSessionID)
	s.Equal("sdk-123", session.SDKSessionID)
	s.Equal("test-project", session.Project)
	s.Equal("Hello", session.UserPrompt)
}

// TestGetActiveSessionCount tests session counting.
func (s *ManagerSuite) TestGetActiveSessionCount() {
	// Initially 0
	s.Equal(0, s.manager.GetActiveSessionCount())

	// Add sessions directly for testing
	s.manager.sessions[1] = &ActiveSession{SessionDBID: 1}
	s.manager.sessions[2] = &ActiveSession{SessionDBID: 2}

	s.Equal(2, s.manager.GetActiveSessionCount())
}

// TestGetTotalQueueDepth tests queue depth calculation.
func (s *ManagerSuite) TestGetTotalQueueDepth() {
	// Initially 0
	s.Equal(0, s.manager.GetTotalQueueDepth())

	// Add sessions with pending messages
	s.manager.sessions[1] = &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 3),
	}
	s.manager.sessions[2] = &ActiveSession{
		SessionDBID:     2,
		pendingMessages: make([]PendingMessage, 5),
	}

	s.Equal(8, s.manager.GetTotalQueueDepth())
}

// TestIsAnySessionProcessing tests processing status detection.
func (s *ManagerSuite) TestIsAnySessionProcessing() {
	// No sessions - not processing
	s.False(s.manager.IsAnySessionProcessing())

	// Session with no pending - not processing
	s.manager.sessions[1] = &ActiveSession{
		SessionDBID:     1,
		pendingMessages: []PendingMessage{},
	}
	s.False(s.manager.IsAnySessionProcessing())

	// Session with pending - processing
	s.manager.sessions[1].pendingMessages = []PendingMessage{{Type: MessageTypeObservation}}
	s.True(s.manager.IsAnySessionProcessing())

	// Clear pending but set generator active
	s.manager.sessions[1].pendingMessages = []PendingMessage{}
	s.manager.sessions[1].generatorActive.Store(true)
	s.True(s.manager.IsAnySessionProcessing())
}

// TestGetAllSessions tests retrieving all sessions.
func (s *ManagerSuite) TestGetAllSessions() {
	// Empty
	sessions := s.manager.GetAllSessions()
	s.Empty(sessions)

	// Add sessions
	session1 := &ActiveSession{SessionDBID: 1, Project: "project-a"}
	session2 := &ActiveSession{SessionDBID: 2, Project: "project-b"}
	s.manager.sessions[1] = session1
	s.manager.sessions[2] = session2

	sessions = s.manager.GetAllSessions()
	s.Len(sessions, 2)
}

// TestDeleteSession tests session deletion.
func (s *ManagerSuite) TestDeleteSession() {
	// Create session with context
	ctx, cancel := context.WithCancel(context.Background())
	session := &ActiveSession{
		SessionDBID:     1,
		Project:         "test-project",
		StartTime:       time.Now(),
		pendingMessages: []PendingMessage{},
		ctx:             ctx,
		cancel:          cancel,
	}
	s.manager.sessions[1] = session

	// Track callback
	var deletedID int64
	s.manager.SetOnSessionDeleted(func(id int64) {
		deletedID = id
	})

	s.Equal(1, s.manager.GetActiveSessionCount())

	// Delete
	s.manager.DeleteSession(1)

	s.Equal(0, s.manager.GetActiveSessionCount())
	s.Equal(int64(1), deletedID)

	// Double delete should be safe
	s.manager.DeleteSession(1)
}

// TestDrainMessages tests message draining.
func (s *ManagerSuite) TestDrainMessages() {
	// No session - nil
	messages := s.manager.DrainMessages(999)
	s.Nil(messages)

	// Session with messages
	session := &ActiveSession{
		SessionDBID: 1,
		pendingMessages: []PendingMessage{
			{Type: MessageTypeObservation},
			{Type: MessageTypeSummarize},
		},
	}
	s.manager.sessions[1] = session

	messages = s.manager.DrainMessages(1)
	s.Len(messages, 2)

	// Queue should be empty now
	s.Empty(session.pendingMessages)

	// Drain again - empty
	messages = s.manager.DrainMessages(1)
	s.Empty(messages)
}

// TestSetOnSessionCreated tests callback setting.
func (s *ManagerSuite) TestSetOnSessionCreated() {
	var calledWith int64
	callback := func(id int64) {
		calledWith = id
	}

	s.manager.SetOnSessionCreated(callback)
	s.NotNil(s.manager.onCreated)

	// Simulate callback
	if s.manager.onCreated != nil {
		s.manager.onCreated(42)
	}
	s.Equal(int64(42), calledWith)
}

// TestSetOnSessionDeleted tests callback setting.
func (s *ManagerSuite) TestSetOnSessionDeleted() {
	var calledWith int64
	callback := func(id int64) {
		calledWith = id
	}

	s.manager.SetOnSessionDeleted(callback)
	s.NotNil(s.manager.onDeleted)

	// Simulate callback
	if s.manager.onDeleted != nil {
		s.manager.onDeleted(42)
	}
	s.Equal(int64(42), calledWith)
}

// TestMessageTypes tests message type constants.
func TestMessageTypes(t *testing.T) {
	assert.Equal(t, MessageType(0), MessageTypeObservation)
	assert.Equal(t, MessageType(1), MessageTypeSummarize)
}

// TestTimeoutConstants tests timeout constants.
func TestTimeoutConstants(t *testing.T) {
	assert.Equal(t, 30*time.Minute, SessionTimeout)
	assert.Equal(t, 5*time.Minute, CleanupInterval)
}

// TestObservationData tests observation data structure.
func TestObservationData(t *testing.T) {
	data := ObservationData{
		ToolName:     "Read",
		ToolInput:    map[string]string{"path": "/test/file.go"},
		ToolResponse: "file content",
		PromptNumber: 1,
		CWD:          "/test",
	}

	assert.Equal(t, "Read", data.ToolName)
	assert.Equal(t, 1, data.PromptNumber)
	assert.Equal(t, "/test", data.CWD)
}

// TestSummarizeData tests summarize data structure.
func TestSummarizeData(t *testing.T) {
	data := SummarizeData{
		LastUserMessage:      "What did you do?",
		LastAssistantMessage: "I completed the task.",
	}

	assert.Equal(t, "What did you do?", data.LastUserMessage)
	assert.Equal(t, "I completed the task.", data.LastAssistantMessage)
}

// TestPendingMessage tests pending message structure.
func TestPendingMessage(t *testing.T) {
	obsData := &ObservationData{ToolName: "Read"}
	msg := PendingMessage{
		Type:        MessageTypeObservation,
		Observation: obsData,
	}

	assert.Equal(t, MessageTypeObservation, msg.Type)
	assert.NotNil(t, msg.Observation)
	assert.Nil(t, msg.Summarize)

	sumData := &SummarizeData{LastUserMessage: "Test"}
	msg2 := PendingMessage{
		Type:      MessageTypeSummarize,
		Summarize: sumData,
	}

	assert.Equal(t, MessageTypeSummarize, msg2.Type)
	assert.Nil(t, msg2.Observation)
	assert.NotNil(t, msg2.Summarize)
}

// TestConcurrentSessionAccess tests thread-safe session operations.
func TestConcurrentSessionAccess(t *testing.T) {
	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	var wg sync.WaitGroup
	numGoroutines := 100

	// Concurrent session operations
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(id int64) {
			defer wg.Done()

			// Add session
			ctx, cancel := context.WithCancel(context.Background())
			manager.mu.Lock()
			manager.sessions[id] = &ActiveSession{
				SessionDBID: id,
				Project:     "test",
				StartTime:   time.Now(),
				ctx:         ctx,
				cancel:      cancel,
			}
			manager.mu.Unlock()

			// Read operations
			_ = manager.GetActiveSessionCount()
			_ = manager.GetTotalQueueDepth()
			_ = manager.IsAnySessionProcessing()
			_ = manager.GetAllSessions()

			// Delete session
			manager.DeleteSession(id)
		}(int64(i))
	}

	wg.Wait()

	// All sessions should be deleted
	assert.Equal(t, 0, manager.GetActiveSessionCount())
}

// TestProcessNotifyChannel tests the process notification channel.
func TestProcessNotifyChannel(t *testing.T) {
	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}

	// Non-blocking send should work
	select {
	case manager.ProcessNotify <- struct{}{}:
		// Success
	default:
		t.Error("ProcessNotify channel should accept first message")
	}

	// Second send should not block (channel is buffered with size 1)
	select {
	case manager.ProcessNotify <- struct{}{}:
		// Full buffer, this is expected behavior
	default:
		// This is fine - channel is full
	}

	// Drain the channel
	select {
	case <-manager.ProcessNotify:
		// Drained
	default:
		t.Error("Should be able to receive from ProcessNotify")
	}
}

// TestActiveSessionContext tests session context handling.
func TestActiveSessionContext(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())

	session := &ActiveSession{
		SessionDBID: 1,
		ctx:         ctx,
		cancel:      cancel,
	}

	// Context should not be done
	select {
	case <-session.ctx.Done():
		t.Error("Context should not be done yet")
	default:
		// Expected
	}

	// Cancel context
	session.cancel()

	// Context should be done
	select {
	case <-session.ctx.Done():
		// Expected
	default:
		t.Error("Context should be done after cancel")
	}
}

// TestGeneratorActive tests the atomic generator active flag.
func TestGeneratorActive(t *testing.T) {
	session := &ActiveSession{}

	// Initially false
	assert.False(t, session.generatorActive.Load())

	// Set to true
	session.generatorActive.Store(true)
	assert.True(t, session.generatorActive.Load())

	// Set back to false
	session.generatorActive.Store(false)
	assert.False(t, session.generatorActive.Load())
}

// TestTokenAccumulation tests token accumulation fields.
func TestTokenAccumulation(t *testing.T) {
	session := &ActiveSession{
		CumulativeInputTokens:  0,
		CumulativeOutputTokens: 0,
	}

	// Accumulate tokens
	session.CumulativeInputTokens += 100
	session.CumulativeOutputTokens += 50

	assert.Equal(t, int64(100), session.CumulativeInputTokens)
	assert.Equal(t, int64(50), session.CumulativeOutputTokens)

	// Add more
	session.CumulativeInputTokens += 200
	session.CumulativeOutputTokens += 100

	assert.Equal(t, int64(300), session.CumulativeInputTokens)
	assert.Equal(t, int64(150), session.CumulativeOutputTokens)
}

// TestShutdownAll tests graceful shutdown of all sessions.
func (s *ManagerSuite) TestShutdownAll() {
	// Create multiple sessions
	for i := int64(1); i <= 3; i++ {
		ctx, cancel := context.WithCancel(context.Background())
		s.manager.sessions[i] = &ActiveSession{
			SessionDBID:     i,
			Project:         "test-project",
			StartTime:       time.Now(),
			pendingMessages: []PendingMessage{},
			ctx:             ctx,
			cancel:          cancel,
		}
	}

	s.Equal(3, s.manager.GetActiveSessionCount())

	// Track deleted sessions
	var deletedIDs []int64
	s.manager.SetOnSessionDeleted(func(id int64) {
		deletedIDs = append(deletedIDs, id)
	})

	// Shutdown all
	s.manager.ShutdownAll(context.Background())

	// All sessions should be deleted
	s.Equal(0, s.manager.GetActiveSessionCount())
	s.Len(deletedIDs, 3)
}

// TestDeleteNonExistentSession tests deleting a session that doesn't exist.
func (s *ManagerSuite) TestDeleteNonExistentSession() {
	// Track callback
	callbackCalled := false
	s.manager.SetOnSessionDeleted(func(id int64) {
		callbackCalled = true
	})

	// Delete non-existent session
	s.manager.DeleteSession(999)

	// Callback should not be called
	s.False(callbackCalled)
}

// TestLastPromptNumber tests prompt number tracking.
func TestLastPromptNumber(t *testing.T) {
	session := &ActiveSession{
		SessionDBID:      1,
		LastPromptNumber: 0,
	}

	assert.Equal(t, 0, session.LastPromptNumber)

	session.LastPromptNumber = 5
	assert.Equal(t, 5, session.LastPromptNumber)

	session.LastPromptNumber++
	assert.Equal(t, 6, session.LastPromptNumber)
}

// TestActiveSessionNotifyChannel tests session notification channel.
func TestActiveSessionNotifyChannel(t *testing.T) {
	session := &ActiveSession{
		notify: make(chan struct{}, 1),
	}

	// Non-blocking send
	select {
	case session.notify <- struct{}{}:
		// Success
	default:
		t.Error("Should accept first notification")
	}

	// Second send should not block
	select {
	case session.notify <- struct{}{}:
		// Full buffer
	default:
		// Expected - buffer is full
	}

	// Drain
	select {
	case <-session.notify:
		// Drained
	default:
		t.Error("Should receive notification")
	}
}

// TestMessageMutex tests message mutex operations.
func TestMessageMutex(t *testing.T) {
	session := &ActiveSession{
		pendingMessages: make([]PendingMessage, 0),
	}

	var wg sync.WaitGroup

	// Concurrent message operations
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()

			session.messageMu.Lock()
			session.pendingMessages = append(session.pendingMessages, PendingMessage{
				Type: MessageTypeObservation,
			})
			session.messageMu.Unlock()
		}()
	}

	wg.Wait()

	assert.Len(t, session.pendingMessages, 50)
}

// TestQueueDepthMultipleSessions tests queue depth with multiple sessions.
func (s *ManagerSuite) TestQueueDepthMultipleSessions() {
	// Add sessions with varying queue depths
	s.manager.sessions[1] = &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 10),
	}
	s.manager.sessions[2] = &ActiveSession{
		SessionDBID:     2,
		pendingMessages: make([]PendingMessage, 0),
	}
	s.manager.sessions[3] = &ActiveSession{
		SessionDBID:     3,
		pendingMessages: make([]PendingMessage, 5),
	}

	s.Equal(15, s.manager.GetTotalQueueDepth())
}

// TestIsAnySessionProcessing_GeneratorOnly tests processing status with only generator active.
func (s *ManagerSuite) TestIsAnySessionProcessingGeneratorOnly() {
	session := &ActiveSession{
		SessionDBID:     1,
		pendingMessages: []PendingMessage{},
	}
	s.manager.sessions[1] = session

	// No processing initially
	s.False(s.manager.IsAnySessionProcessing())

	// Set generator active
	session.generatorActive.Store(true)
	s.True(s.manager.IsAnySessionProcessing())

	// Clear generator
	session.generatorActive.Store(false)
	s.False(s.manager.IsAnySessionProcessing())
}

// TestPendingMessageWithBothTypes tests pending messages with both types.
func TestPendingMessageWithBothTypes(t *testing.T) {
	messages := []PendingMessage{
		{
			Type:        MessageTypeObservation,
			Observation: &ObservationData{ToolName: "Read"},
		},
		{
			Type:      MessageTypeSummarize,
			Summarize: &SummarizeData{LastUserMessage: "Test"},
		},
		{
			Type:        MessageTypeObservation,
			Observation: &ObservationData{ToolName: "Write"},
		},
	}

	assert.Len(t, messages, 3)

	// Verify types
	assert.Equal(t, MessageTypeObservation, messages[0].Type)
	assert.Equal(t, MessageTypeSummarize, messages[1].Type)
	assert.Equal(t, MessageTypeObservation, messages[2].Type)

	// Verify data
	assert.Equal(t, "Read", messages[0].Observation.ToolName)
	assert.Nil(t, messages[0].Summarize)

	assert.Equal(t, "Test", messages[1].Summarize.LastUserMessage)
	assert.Nil(t, messages[1].Observation)

	assert.Equal(t, "Write", messages[2].Observation.ToolName)
}

// TestDrainMessagesPreservesOrder tests that draining preserves message order.
func (s *ManagerSuite) TestDrainMessagesPreservesOrder() {
	session := &ActiveSession{
		SessionDBID: 1,
		pendingMessages: []PendingMessage{
			{Type: MessageTypeObservation, Observation: &ObservationData{ToolName: "Tool1"}},
			{Type: MessageTypeSummarize, Summarize: &SummarizeData{LastUserMessage: "Msg1"}},
			{Type: MessageTypeObservation, Observation: &ObservationData{ToolName: "Tool2"}},
		},
	}
	s.manager.sessions[1] = session

	messages := s.manager.DrainMessages(1)

	s.Len(messages, 3)
	s.Equal("Tool1", messages[0].Observation.ToolName)
	s.Equal("Msg1", messages[1].Summarize.LastUserMessage)
	s.Equal("Tool2", messages[2].Observation.ToolName)
}

// TestActiveSessionCWD tests CWD field in ObservationData.
func TestActiveSessionCWD(t *testing.T) {
	tests := []struct {
		name string
		cwd  string
	}{
		{"empty_cwd", ""},
		{"absolute_path", "/home/user/project"},
		{"windows_path", "C:\\Users\\test\\project"},
		{"path_with_spaces", "/home/user/my project"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			data := ObservationData{
				ToolName: "Test",
				CWD:      tt.cwd,
			}
			assert.Equal(t, tt.cwd, data.CWD)
		})
	}
}

// TestToolInputResponse tests various tool input/response types.
func TestToolInputResponse(t *testing.T) {
	tests := []struct {
		input    interface{}
		response interface{}
		name     string
	}{
		{name: "nil_values", input: nil, response: nil},
		{name: "string_values", input: "input string", response: "response string"},
		{name: "map_values", input: map[string]string{"key": "value"}, response: map[string]interface{}{"result": true}},
		{name: "slice_values", input: []string{"a", "b"}, response: []int{1, 2, 3}},
		{name: "int_values", input: 42, response: 100},
		{name: "bool_values", input: true, response: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			data := ObservationData{
				ToolName:     "TestTool",
				ToolInput:    tt.input,
				ToolResponse: tt.response,
			}
			assert.Equal(t, tt.input, data.ToolInput)
			assert.Equal(t, tt.response, data.ToolResponse)
		})
	}
}

// =============================================================================
// TESTS FOR NewManager AND CLEANUP
// =============================================================================

// TestNewManager tests the NewManager function.
func TestNewManager(t *testing.T) {
	t.Parallel()

	// Test with nil session store (valid for testing)
	manager := NewManager(nil)

	assert.NotNil(t, manager)
	assert.NotNil(t, manager.sessions)
	assert.NotNil(t, manager.ProcessNotify)
	assert.NotNil(t, manager.ctx)
	assert.NotNil(t, manager.cancel)
	assert.Equal(t, 0, manager.GetActiveSessionCount())

	// Clean up - cancel context to stop cleanup goroutine
	manager.cancel()
}

// TestNewManager_CleanupGoroutineStops tests that cleanup goroutine stops on cancel.
func TestNewManager_CleanupGoroutineStops(t *testing.T) {
	t.Parallel()

	manager := NewManager(nil)

	// Give goroutine time to start
	time.Sleep(10 * time.Millisecond)

	// Cancel should stop the cleanup goroutine
	manager.cancel()

	// Context should be done
	select {
	case <-manager.ctx.Done():
		// Expected
	case <-time.After(100 * time.Millisecond):
		t.Error("Context should be done after cancel")
	}
}

// TestCleanupStaleSessions_NoSessions tests cleanup with no sessions.
func TestCleanupStaleSessions_NoSessions(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Should not panic with empty sessions
	manager.cleanupStaleSessions()
	assert.Equal(t, 0, manager.GetActiveSessionCount())
}

// TestCleanupStaleSessions_FreshSession tests that fresh sessions are not cleaned.
func TestCleanupStaleSessions_FreshSession(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Add a fresh session
	sessionCtx, sessionCancel := context.WithCancel(context.Background())
	manager.sessions[1] = &ActiveSession{
		SessionDBID:     1,
		StartTime:       time.Now(), // Fresh
		pendingMessages: []PendingMessage{},
		ctx:             sessionCtx,
		cancel:          sessionCancel,
	}

	manager.cleanupStaleSessions()

	// Session should still exist (not stale)
	assert.Equal(t, 1, manager.GetActiveSessionCount())
	sessionCancel()
}

// TestCleanupStaleSessions_StaleSession tests that stale sessions are cleaned.
func TestCleanupStaleSessions_StaleSession(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Add a stale session
	sessionCtx, sessionCancel := context.WithCancel(context.Background())
	manager.sessions[1] = &ActiveSession{
		SessionDBID:     1,
		StartTime:       time.Now().Add(-SessionTimeout - time.Minute), // Stale
		pendingMessages: []PendingMessage{},
		ctx:             sessionCtx,
		cancel:          sessionCancel,
	}

	manager.cleanupStaleSessions()

	// Session should be deleted
	assert.Equal(t, 0, manager.GetActiveSessionCount())
}

// TestCleanupStaleSessions_StaleWithPending tests stale sessions with pending messages are not cleaned.
func TestCleanupStaleSessions_StaleWithPending(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Add a stale session with pending messages
	sessionCtx, sessionCancel := context.WithCancel(context.Background())
	defer sessionCancel()
	manager.sessions[1] = &ActiveSession{
		SessionDBID:     1,
		StartTime:       time.Now().Add(-SessionTimeout - time.Minute), // Stale
		pendingMessages: []PendingMessage{{Type: MessageTypeObservation}},
		ctx:             sessionCtx,
		cancel:          sessionCancel,
	}

	manager.cleanupStaleSessions()

	// Session should NOT be deleted (has pending messages)
	assert.Equal(t, 1, manager.GetActiveSessionCount())
}

// TestCleanupStaleSessions_StaleWithActiveGenerator tests stale sessions with active generator are not cleaned.
func TestCleanupStaleSessions_StaleWithActiveGenerator(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Add a stale session with active generator
	sessionCtx, sessionCancel := context.WithCancel(context.Background())
	defer sessionCancel()
	session := &ActiveSession{
		SessionDBID:     1,
		StartTime:       time.Now().Add(-SessionTimeout - time.Minute), // Stale
		pendingMessages: []PendingMessage{},
		ctx:             sessionCtx,
		cancel:          sessionCancel,
	}
	session.generatorActive.Store(true)
	manager.sessions[1] = session

	manager.cleanupStaleSessions()

	// Session should NOT be deleted (generator is active)
	assert.Equal(t, 1, manager.GetActiveSessionCount())
}

// TestCleanupStaleSessions_MixedSessions tests cleanup with mixed fresh and stale sessions.
func TestCleanupStaleSessions_MixedSessions(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Fresh session
	ctx1, cancel1 := context.WithCancel(context.Background())
	defer cancel1()
	manager.sessions[1] = &ActiveSession{
		SessionDBID:     1,
		StartTime:       time.Now(),
		pendingMessages: []PendingMessage{},
		ctx:             ctx1,
		cancel:          cancel1,
	}

	// Stale session (should be deleted)
	ctx2, cancel2 := context.WithCancel(context.Background())
	manager.sessions[2] = &ActiveSession{
		SessionDBID:     2,
		StartTime:       time.Now().Add(-SessionTimeout - time.Minute),
		pendingMessages: []PendingMessage{},
		ctx:             ctx2,
		cancel:          cancel2,
	}

	// Stale session with pending (should NOT be deleted)
	ctx3, cancel3 := context.WithCancel(context.Background())
	defer cancel3()
	manager.sessions[3] = &ActiveSession{
		SessionDBID:     3,
		StartTime:       time.Now().Add(-SessionTimeout - time.Minute),
		pendingMessages: []PendingMessage{{Type: MessageTypeObservation}},
		ctx:             ctx3,
		cancel:          cancel3,
	}

	manager.cleanupStaleSessions()

	// Should have 2 sessions left (1 fresh, 1 stale with pending)
	assert.Equal(t, 2, manager.GetActiveSessionCount())

	// Verify which sessions remain
	manager.mu.RLock()
	_, has1 := manager.sessions[1]
	_, has2 := manager.sessions[2]
	_, has3 := manager.sessions[3]
	manager.mu.RUnlock()

	assert.True(t, has1, "Fresh session should remain")
	assert.False(t, has2, "Stale session should be deleted")
	assert.True(t, has3, "Stale session with pending should remain")
}

// TestCleanupLoop_ExitsOnCancel tests that cleanup loop exits when context is cancelled.
func TestCleanupLoop_ExitsOnCancel(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	manager.ctx = ctx
	manager.cancel = cancel

	// Start cleanup loop in goroutine
	done := make(chan struct{})
	manager.wg.Add(1)
	go func() {
		manager.cleanupLoop()
		close(done)
	}()

	// Cancel immediately
	cancel()

	// Should exit quickly
	select {
	case <-done:
		// Success - loop exited
	case <-time.After(100 * time.Millisecond):
		t.Error("Cleanup loop should exit when context is cancelled")
	}
}

// =============================================================================
// TESTS FOR InitializeSession (without DB)
// =============================================================================

// TestInitializeSession_AlreadyActive tests reusing an already active session.
func TestInitializeSession_AlreadyActive(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add an active session
	existingSession := &ActiveSession{
		SessionDBID:      42,
		ClaudeSessionID:  "claude-existing",
		Project:          "test-project",
		UserPrompt:       "original prompt",
		LastPromptNumber: 1,
		StartTime:        time.Now(),
		pendingMessages:  make([]PendingMessage, 0),
	}
	manager.sessions[42] = existingSession

	// Initialize same session - should reuse
	session, err := manager.InitializeSession(context.Background(), 42, "new prompt", 5)

	assert.NoError(t, err)
	assert.NotNil(t, session)
	assert.Same(t, existingSession, session)
	assert.Equal(t, "new prompt", session.UserPrompt)
	assert.Equal(t, 5, session.LastPromptNumber)
}

// TestInitializeSession_AlreadyActive_EmptyPrompt tests reusing session with empty prompt.
func TestInitializeSession_AlreadyActive_EmptyPrompt(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add an active session
	existingSession := &ActiveSession{
		SessionDBID:      42,
		UserPrompt:       "original prompt",
		LastPromptNumber: 1,
	}
	manager.sessions[42] = existingSession

	// Initialize with empty prompt - should NOT update
	session, err := manager.InitializeSession(context.Background(), 42, "", 0)

	assert.NoError(t, err)
	assert.NotNil(t, session)
	assert.Equal(t, "original prompt", session.UserPrompt) // Unchanged
	assert.Equal(t, 1, session.LastPromptNumber)           // Unchanged
}

// TestInitializeSession_NoStore tests initialization without session store.
func TestInitializeSession_NoStore(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessionStore:  nil, // No store
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Should fail gracefully with nil store (panic recovery not expected)
	// This tests the guard against nil sessionStore
	defer func() {
		if r := recover(); r != nil {
			_ = r // Expected panic when calling nil store - intentionally ignored
		}
	}()

	_, _ = manager.InitializeSession(context.Background(), 999, "prompt", 1)
}

// TestInitializeSession_CallbackTriggered tests that created callback is triggered.
func TestInitializeSession_CallbackTriggered(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	var calledWithID int64
	manager.SetOnSessionCreated(func(id int64) {
		calledWithID = id
	})

	// Add session directly (simulating what would happen after DB fetch)
	sessionCtx, sessionCancel := context.WithCancel(context.Background())
	defer sessionCancel()
	session := &ActiveSession{
		SessionDBID:     100,
		ClaudeSessionID: "test",
		Project:         "project",
		StartTime:       time.Now(),
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
		ctx:             sessionCtx,
		cancel:          sessionCancel,
	}

	manager.mu.Lock()
	manager.sessions[100] = session
	onCreated := manager.onCreated
	manager.mu.Unlock()

	// Trigger callback
	if onCreated != nil {
		onCreated(100)
	}

	assert.Equal(t, int64(100), calledWithID)
}

// =============================================================================
// TESTS FOR QueueObservation AND QueueSummarize (without DB)
// =============================================================================

// TestQueueObservation_ToExistingSession tests queuing to an existing session.
func TestQueueObservation_ToExistingSession(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add session
	session := &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
	}
	manager.sessions[1] = session

	// Queue observation
	err := manager.QueueObservation(context.Background(), 1, ObservationData{
		ToolName:     "Read",
		ToolInput:    map[string]string{"path": "/test"},
		ToolResponse: "content",
		PromptNumber: 1,
		CWD:          "/project",
	})

	assert.NoError(t, err)
	assert.Equal(t, 1, manager.GetTotalQueueDepth())

	// Verify message
	messages := manager.DrainMessages(1)
	assert.Len(t, messages, 1)
	assert.Equal(t, MessageTypeObservation, messages[0].Type)
	assert.Equal(t, "Read", messages[0].Observation.ToolName)
	assert.Equal(t, "/project", messages[0].Observation.CWD)
}

// TestQueueObservation_NotifiesSession tests that notification is sent to session.
func TestQueueObservation_NotifiesSession(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add session with notify channel
	session := &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
	}
	manager.sessions[1] = session

	// Queue observation
	err := manager.QueueObservation(context.Background(), 1, ObservationData{ToolName: "Test"})
	assert.NoError(t, err)

	// Should receive notification on session channel
	select {
	case <-session.notify:
		// Success
	default:
		t.Error("Session should receive notification")
	}

	// Should receive notification on process channel
	select {
	case <-manager.ProcessNotify:
		// Success
	default:
		t.Error("Manager ProcessNotify should receive notification")
	}
}

// TestQueueSummarize_ToExistingSession tests queuing summarize to an existing session.
func TestQueueSummarize_ToExistingSession(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add session
	session := &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
	}
	manager.sessions[1] = session

	// Queue summarize
	err := manager.QueueSummarize(context.Background(), 1, "User asked question", "Assistant answered")
	assert.NoError(t, err)
	assert.Equal(t, 1, manager.GetTotalQueueDepth())

	// Verify message
	messages := manager.DrainMessages(1)
	assert.Len(t, messages, 1)
	assert.Equal(t, MessageTypeSummarize, messages[0].Type)
	assert.Equal(t, "User asked question", messages[0].Summarize.LastUserMessage)
	assert.Equal(t, "Assistant answered", messages[0].Summarize.LastAssistantMessage)
}

// TestQueueSummarize_NotifiesSession tests that notification is sent to session.
func TestQueueSummarize_NotifiesSession(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add session with notify channel
	session := &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
	}
	manager.sessions[1] = session

	// Queue summarize
	err := manager.QueueSummarize(context.Background(), 1, "user", "assistant")
	assert.NoError(t, err)

	// Should receive notification on session channel
	select {
	case <-session.notify:
		// Success
	default:
		t.Error("Session should receive notification")
	}

	// Should receive notification on process channel
	select {
	case <-manager.ProcessNotify:
		// Success
	default:
		t.Error("Manager ProcessNotify should receive notification")
	}
}

// TestQueueOperations_MultipleMessages tests queuing multiple messages.
func TestQueueOperations_MultipleMessages(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add session
	session := &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
	}
	manager.sessions[1] = session

	// Queue multiple messages
	for i := 0; i < 10; i++ {
		if i%2 == 0 {
			err := manager.QueueObservation(context.Background(), 1, ObservationData{
				ToolName: "Tool" + string(rune('A'+i)),
			})
			assert.NoError(t, err)
		} else {
			err := manager.QueueSummarize(context.Background(), 1, "user", "assistant")
			assert.NoError(t, err)
		}
	}

	assert.Equal(t, 10, manager.GetTotalQueueDepth())

	// Drain and verify
	messages := manager.DrainMessages(1)
	assert.Len(t, messages, 10)
}

// TestQueueOperations_NonBlockingNotification tests non-blocking notification behavior.
func TestQueueOperations_NonBlockingNotification(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add session with full notify channel
	session := &ActiveSession{
		SessionDBID:     1,
		pendingMessages: make([]PendingMessage, 0),
		notify:          make(chan struct{}, 1),
	}
	// Fill the notify channel
	session.notify <- struct{}{}
	manager.sessions[1] = session

	// Fill ProcessNotify channel
	manager.ProcessNotify <- struct{}{}

	// Queue should NOT block even with full channels
	done := make(chan bool)
	go func() {
		err := manager.QueueObservation(context.Background(), 1, ObservationData{ToolName: "Test"})
		assert.NoError(t, err)
		done <- true
	}()

	select {
	case <-done:
		// Success - didn't block
	case <-time.After(100 * time.Millisecond):
		t.Error("Queue operation should not block even with full notification channels")
	}
}

// TestConcurrentQueueAndCleanup tests concurrent queue operations and cleanup.
func TestConcurrentQueueAndCleanup(t *testing.T) {
	t.Parallel()

	manager := &Manager{
		sessions:      make(map[int64]*ActiveSession),
		ProcessNotify: make(chan struct{}, 1),
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	manager.ctx = ctx
	manager.cancel = cancel

	// Pre-add multiple sessions
	for i := int64(1); i <= 5; i++ {
		sessionCtx, sessionCancel := context.WithCancel(context.Background())
		manager.sessions[i] = &ActiveSession{
			SessionDBID:     i,
			StartTime:       time.Now(),
			pendingMessages: make([]PendingMessage, 0),
			notify:          make(chan struct{}, 1),
			ctx:             sessionCtx,
			cancel:          sessionCancel,
		}
	}

	var wg sync.WaitGroup

	// Concurrent queue operations
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			sessionID := int64((idx % 5) + 1)
			if idx%2 == 0 {
				_ = manager.QueueObservation(context.Background(), sessionID, ObservationData{ToolName: "Test"})
			} else {
				_ = manager.QueueSummarize(context.Background(), sessionID, "user", "assistant")
			}
		}(i)
	}

	// Concurrent cleanup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			manager.cleanupStaleSessions()
		}()
	}

	// Concurrent reads
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_ = manager.GetActiveSessionCount()
			_ = manager.GetTotalQueueDepth()
			_ = manager.IsAnySessionProcessing()
			_ = manager.GetAllSessions()
		}()
	}

	wg.Wait()

	// Should have all sessions (none are stale)
	assert.Equal(t, 5, manager.GetActiveSessionCount())
	// Should have 50 messages total
	assert.Equal(t, 50, manager.GetTotalQueueDepth())
}
