package main
import (
	"gf-lt/config"
	"gf-lt/models"
	"reflect"
	"testing"
)
func TestConsolidateConsecutiveAssistantMessages(t *testing.T) {
	// Mock config for testing
	testCfg := &config.Config{
		AssistantRole:                 "assistant",
		WriteNextMsgAsCompletionAgent: "",
	}
	cfg = testCfg
	tests := []struct {
		name     string
		input    []models.RoleMsg
		expected []models.RoleMsg
	}{
		{
			name: "no consecutive assistant messages",
			input: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "Hi there"},
				{Role: "user", Content: "How are you?"},
			},
			expected: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "Hi there"},
				{Role: "user", Content: "How are you?"},
			},
		},
		{
			name: "consecutive assistant messages should be consolidated",
			input: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "First part"},
				{Role: "assistant", Content: "Second part"},
				{Role: "user", Content: "Thanks"},
			},
			expected: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "First part\nSecond part"},
				{Role: "user", Content: "Thanks"},
			},
		},
		{
			name: "multiple sets of consecutive assistant messages",
			input: []models.RoleMsg{
				{Role: "user", Content: "First question"},
				{Role: "assistant", Content: "First answer part 1"},
				{Role: "assistant", Content: "First answer part 2"},
				{Role: "user", Content: "Second question"},
				{Role: "assistant", Content: "Second answer part 1"},
				{Role: "assistant", Content: "Second answer part 2"},
				{Role: "assistant", Content: "Second answer part 3"},
			},
			expected: []models.RoleMsg{
				{Role: "user", Content: "First question"},
				{Role: "assistant", Content: "First answer part 1\nFirst answer part 2"},
				{Role: "user", Content: "Second question"},
				{Role: "assistant", Content: "Second answer part 1\nSecond answer part 2\nSecond answer part 3"},
			},
		},
		{
			name: "single assistant message (no consolidation needed)",
			input: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "Hi there"},
			},
			expected: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "Hi there"},
			},
		},
		{
			name: "only assistant messages",
			input: []models.RoleMsg{
				{Role: "assistant", Content: "First"},
				{Role: "assistant", Content: "Second"},
				{Role: "assistant", Content: "Third"},
			},
			expected: []models.RoleMsg{
				{Role: "assistant", Content: "First\nSecond\nThird"},
			},
		},
		{
			name: "user messages at the end are preserved",
			input: []models.RoleMsg{
				{Role: "assistant", Content: "First"},
				{Role: "assistant", Content: "Second"},
				{Role: "user", Content: "Final user message"},
			},
			expected: []models.RoleMsg{
				{Role: "assistant", Content: "First\nSecond"},
				{Role: "user", Content: "Final user message"},
			},
		},
		{
			name: "tool call ids preserved in consolidation",
			input: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "First part", ToolCallID: "call_123"},
				{Role: "assistant", Content: "Second part", ToolCallID: "call_123"}, // Same ID
				{Role: "user", Content: "Thanks"},
			},
			expected: []models.RoleMsg{
				{Role: "user", Content: "Hello"},
				{Role: "assistant", Content: "First part\nSecond part", ToolCallID: "call_123"},
				{Role: "user", Content: "Thanks"},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := consolidateAssistantMessages(tt.input)
			if len(result) != len(tt.expected) {
				t.Errorf("Expected %d messages, got %d", len(tt.expected), len(result))
				t.Logf("Result: %+v", result)
				t.Logf("Expected: %+v", tt.expected)
				return
			}
			for i, expectedMsg := range tt.expected {
				if i >= len(result) {
					t.Errorf("Result has fewer messages than expected at index %d", i)
					continue
				}
				actualMsg := result[i]
				if actualMsg.Role != expectedMsg.Role {
					t.Errorf("Message %d: expected role '%s', got '%s'", i, expectedMsg.Role, actualMsg.Role)
				}
				if actualMsg.Content != expectedMsg.Content {
					t.Errorf("Message %d: expected content '%s', got '%s'", i, expectedMsg.Content, actualMsg.Content)
				}
				if actualMsg.ToolCallID != expectedMsg.ToolCallID {
					t.Errorf("Message %d: expected ToolCallID '%s', got '%s'", i, expectedMsg.ToolCallID, actualMsg.ToolCallID)
				}
			}
			// Additional check: ensure no messages were lost
			if !reflect.DeepEqual(result, tt.expected) {
				t.Errorf("Result does not match expected:\nResult:   %+v\nExpected: %+v", result, tt.expected)
			}
		})
	}
}
func TestUnmarshalFuncCall(t *testing.T) {
	tests := []struct {
		name    string
		jsonStr string
		want    *models.FuncCall
		wantErr bool
	}{
		{
			name:    "simple websearch with numeric limit",
			jsonStr: `{"name": "websearch", "args": {"query": "current weather in London", "limit": 3}}`,
			want: &models.FuncCall{
				Name: "websearch",
				Args: map[string]string{"query": "current weather in London", "limit": "3"},
			},
			wantErr: false,
		},
		{
			name:    "string limit",
			jsonStr: `{"name": "websearch", "args": {"query": "test", "limit": "5"}}`,
			want: &models.FuncCall{
				Name: "websearch",
				Args: map[string]string{"query": "test", "limit": "5"},
			},
			wantErr: false,
		},
		{
			name:    "boolean arg",
			jsonStr: `{"name": "test", "args": {"flag": true}}`,
			want: &models.FuncCall{
				Name: "test",
				Args: map[string]string{"flag": "true"},
			},
			wantErr: false,
		},
		{
			name:    "null arg",
			jsonStr: `{"name": "test", "args": {"opt": null}}`,
			want: &models.FuncCall{
				Name: "test",
				Args: map[string]string{"opt": ""},
			},
			wantErr: false,
		},
		{
			name:    "float arg",
			jsonStr: `{"name": "test", "args": {"ratio": 0.5}}`,
			want: &models.FuncCall{
				Name: "test",
				Args: map[string]string{"ratio": "0.5"},
			},
			wantErr: false,
		},
		{
			name:    "invalid JSON",
			jsonStr: `{invalid}`,
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := unmarshalFuncCall(tt.jsonStr)
			if (err != nil) != tt.wantErr {
				t.Errorf("unmarshalFuncCall() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.wantErr {
				return
			}
			if got.Name != tt.want.Name {
				t.Errorf("unmarshalFuncCall() name = %v, want %v", got.Name, tt.want.Name)
			}
			if len(got.Args) != len(tt.want.Args) {
				t.Errorf("unmarshalFuncCall() args length = %v, want %v", len(got.Args), len(tt.want.Args))
			}
			for k, v := range tt.want.Args {
				if got.Args[k] != v {
					t.Errorf("unmarshalFuncCall() args[%v] = %v, want %v", k, got.Args[k], v)
				}
			}
		})
	}
}
func TestConvertJSONToMapStringString(t *testing.T) {
	tests := []struct {
		name    string
		jsonStr string
		want    map[string]string
		wantErr bool
	}{
		{
			name:    "simple map",
			jsonStr: `{"query": "weather", "limit": 5}`,
			want:    map[string]string{"query": "weather", "limit": "5"},
			wantErr: false,
		},
		{
			name:    "boolean and null",
			jsonStr: `{"flag": true, "opt": null}`,
			want:    map[string]string{"flag": "true", "opt": ""},
			wantErr: false,
		},
		{
			name:    "invalid JSON",
			jsonStr: `{invalid`,
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := convertJSONToMapStringString(tt.jsonStr)
			if (err != nil) != tt.wantErr {
				t.Errorf("convertJSONToMapStringString() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.wantErr {
				return
			}
			if len(got) != len(tt.want) {
				t.Errorf("convertJSONToMapStringString() length = %v, want %v", len(got), len(tt.want))
			}
			for k, v := range tt.want {
				if got[k] != v {
					t.Errorf("convertJSONToMapStringString()[%v] = %v, want %v", k, got[k], v)
				}
			}
		})
	}
}
func TestParseKnownToTag(t *testing.T) {
	tests := []struct {
		name        string
		content     string
		enabled     bool
		tag         string
		wantCleaned string
		wantKnownTo []string
	}{
		{
			name:        "feature disabled returns original",
			content:     "Hello @Alice@",
			enabled:     false,
			tag:         "@",
			wantCleaned: "Hello @Alice@",
			wantKnownTo: nil,
		},
		{
			name:        "no tag returns original",
			content:     "Hello Alice",
			enabled:     true,
			tag:         "@",
			wantCleaned: "Hello Alice",
			wantKnownTo: nil,
		},
		{
			name:        "single tag with one char",
			content:     "Hello @Alice@",
			enabled:     true,
			tag:         "@",
			wantCleaned: "Hello",
			wantKnownTo: []string{"Alice"},
		},
		{
			name:        "single tag with two chars",
			content:     "Secret @Alice,Bob@ message",
			enabled:     true,
			tag:         "@",
			wantCleaned: "Secret  message",
			wantKnownTo: []string{"Alice", "Bob"},
		},
		{
			name:        "tag at beginning",
			content:     "@Alice@ Hello",
			enabled:     true,
			tag:         "@",
			wantCleaned: "Hello",
			wantKnownTo: []string{"Alice"},
		},
		{
			name:        "tag at end",
			content:     "Hello @Alice@",
			enabled:     true,
			tag:         "@",
			wantCleaned: "Hello",
			wantKnownTo: []string{"Alice"},
		},
		{
			name:        "multiple tags",
			content:     "First @Alice@ then @Bob@",
			enabled:     true,
			tag:         "@",
			wantCleaned: "First  then",
			wantKnownTo: []string{"Alice", "Bob"},
		},
		{
			name:        "custom tag",
			content:     "Secret @Alice,Bob@ message",
			enabled:     true,
			tag:         "@",
			wantCleaned: "Secret  message",
			wantKnownTo: []string{"Alice", "Bob"},
		},
		{
			name:        "empty list",
			content:     "Secret @@@",
			enabled:     true,
			tag:         "@",
			wantCleaned: "Secret",
			wantKnownTo: nil,
		},
		{
			name:        "whitespace around commas",
			content:     "@ Alice , Bob , Carl @",
			enabled:     true,
			tag:         "@",
			wantCleaned: "",
			wantKnownTo: []string{"Alice", "Bob", "Carl"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Set up config
			testCfg := &config.Config{
				CharSpecificContextEnabled: tt.enabled,
				CharSpecificContextTag:     tt.tag,
			}
			cfg = testCfg
			knownTo := parseKnownToTag(tt.content)
			if len(knownTo) != len(tt.wantKnownTo) {
				t.Errorf("parseKnownToTag() knownTo length = %v, want %v", len(knownTo), len(tt.wantKnownTo))
				t.Logf("got: %v", knownTo)
				t.Logf("want: %v", tt.wantKnownTo)
			} else {
				for i, got := range knownTo {
					if got != tt.wantKnownTo[i] {
						t.Errorf("parseKnownToTag() knownTo[%d] = %q, want %q", i, got, tt.wantKnownTo[i])
					}
				}
			}
		})
	}
}
func TestProcessMessageTag(t *testing.T) {
	tests := []struct {
		name    string
		msg     models.RoleMsg
		enabled bool
		tag     string
		wantMsg models.RoleMsg
	}{
		{
			name: "feature disabled returns unchanged",
			msg: models.RoleMsg{
				Role:    "Alice",
				Content: "Secret @Bob@",
			},
			enabled: false,
			tag:     "@",
			wantMsg: models.RoleMsg{
				Role:    "Alice",
				Content: "Secret @Bob@",
				KnownTo: nil,
			},
		},
		{
			name: "no tag, no knownTo",
			msg: models.RoleMsg{
				Role:    "Alice",
				Content: "Hello everyone",
			},
			enabled: true,
			tag:     "@",
			wantMsg: models.RoleMsg{
				Role:    "Alice",
				Content: "Hello everyone",
				KnownTo: nil,
			},
		},
		{
			name: "tag with Bob, adds Alice automatically",
			msg: models.RoleMsg{
				Role:    "Alice",
				Content: "Secret @Bob@",
			},
			enabled: true,
			tag:     "@",
			wantMsg: models.RoleMsg{
				Role:    "Alice",
				Content: "Secret",
				KnownTo: []string{"Bob", "Alice"},
			},
		},
		{
			name: "tag already includes sender",
			msg: models.RoleMsg{
				Role:    "Alice",
				Content: "@Alice,Bob@",
			},
			enabled: true,
			tag:     "@",
			wantMsg: models.RoleMsg{
				Role:    "Alice",
				Content: "",
				KnownTo: []string{"Alice", "Bob"},
			},
		},
		{
			name: "knownTo already set (from DB), tag still processed",
			msg: models.RoleMsg{
				Role:    "Alice",
				Content: "Secret @Bob@",
				KnownTo: []string{"Alice"}, // from previous processing
			},
			enabled: true,
			tag:     "@",
			wantMsg: models.RoleMsg{
				Role:    "Alice",
				Content: "Secret",
				KnownTo: []string{"Bob", "Alice"},
			},
		},
		{
			name: "example from real use",
			msg: models.RoleMsg{
				Role:    "Alice",
				Content: "I'll start with a simple one! The word is 'banana'. (ooc: @Bob@)",
				KnownTo: []string{"Alice"}, // from previous processing
			},
			enabled: true,
			tag:     "@",
			wantMsg: models.RoleMsg{
				Role:    "Alice",
				Content: "I'll start with a simple one! The word is 'banana'. (ooc: @Bob@)",
				KnownTo: []string{"Bob", "Alice"},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testCfg := &config.Config{
				CharSpecificContextEnabled: tt.enabled,
				CharSpecificContextTag:     tt.tag,
			}
			cfg = testCfg
			got := processMessageTag(&tt.msg)
			if len(got.KnownTo) != len(tt.wantMsg.KnownTo) {
				t.Errorf("processMessageTag() KnownTo length = %v, want %v", len(got.KnownTo), len(tt.wantMsg.KnownTo))
				t.Logf("got: %v", got.KnownTo)
				t.Logf("want: %v", tt.wantMsg.KnownTo)
			} else {
				// order may differ; check membership
				for _, want := range tt.wantMsg.KnownTo {
					found := false
					for _, gotVal := range got.KnownTo {
						if gotVal == want {
							found = true
							break
						}
					}
					if !found {
						t.Errorf("processMessageTag() missing KnownTo entry %q, got %v", want, got.KnownTo)
					}
				}
			}
		})
	}
}
func TestFilterMessagesForCharacter(t *testing.T) {
	messages := []models.RoleMsg{
		{Role: "system", Content: "System message", KnownTo: nil}, // visible to all
		{Role: "Alice", Content: "Hello everyone", KnownTo: nil},  // visible to all
		{Role: "Alice", Content: "Secret for Bob", KnownTo: []string{"Alice", "Bob"}},
		{Role: "Bob", Content: "Reply to Alice", KnownTo: []string{"Alice", "Bob"}},
		{Role: "Alice", Content: "Private to Carl", KnownTo: []string{"Alice", "Carl"}},
		{Role: "Carl", Content: "Hi all", KnownTo: nil}, // visible to all
	}
	tests := []struct {
		name        string
		enabled     bool
		character   string
		wantIndices []int // indices from original messages that should be included
	}{
		{
			name:        "feature disabled returns all",
			enabled:     false,
			character:   "Alice",
			wantIndices: []int{0, 1, 2, 3, 4, 5},
		},
		{
			name:        "character empty returns all",
			enabled:     true,
			character:   "",
			wantIndices: []int{0, 1, 2, 3, 4, 5},
		},
		{
			name:        "Alice sees all including Carl-private",
			enabled:     true,
			character:   "Alice",
			wantIndices: []int{0, 1, 2, 3, 4, 5},
		},
		{
			name:        "Bob sees Alice-Bob secrets and all public",
			enabled:     true,
			character:   "Bob",
			wantIndices: []int{0, 1, 2, 3, 5},
		},
		{
			name:        "Carl sees Alice-Carl secret and public",
			enabled:     true,
			character:   "Carl",
			wantIndices: []int{0, 1, 4, 5},
		},
		{
			name:        "David sees only public messages",
			enabled:     true,
			character:   "David",
			wantIndices: []int{0, 1, 5},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testCfg := &config.Config{
				CharSpecificContextEnabled: tt.enabled,
				CharSpecificContextTag:     "@",
			}
			cfg = testCfg
			got := filterMessagesForCharacter(messages, tt.character)
			if len(got) != len(tt.wantIndices) {
				t.Errorf("filterMessagesForCharacter() returned %d messages, want %d", len(got), len(tt.wantIndices))
				t.Logf("got: %v", got)
				return
			}
			for i, idx := range tt.wantIndices {
				if got[i].Content != messages[idx].Content {
					t.Errorf("filterMessagesForCharacter() message %d content = %q, want %q", i, got[i].Content, messages[idx].Content)
				}
			}
		})
	}
}
func TestRoleMsgCopyPreservesKnownTo(t *testing.T) {
	// Test that the Copy() method preserves the KnownTo field
	originalMsg := models.RoleMsg{
		Role:    "Alice",
		Content: "Test message",
		KnownTo: []string{"Bob", "Charlie"},
	}
	copiedMsg := originalMsg.Copy()
	if copiedMsg.Role != originalMsg.Role {
		t.Errorf("Copy() failed to preserve Role: got %q, want %q", copiedMsg.Role, originalMsg.Role)
	}
	if copiedMsg.Content != originalMsg.Content {
		t.Errorf("Copy() failed to preserve Content: got %q, want %q", copiedMsg.Content, originalMsg.Content)
	}
	if !reflect.DeepEqual(copiedMsg.KnownTo, originalMsg.KnownTo) {
		t.Errorf("Copy() failed to preserve KnownTo: got %v, want %v", copiedMsg.KnownTo, originalMsg.KnownTo)
	}
	if copiedMsg.ToolCallID != originalMsg.ToolCallID {
		t.Errorf("Copy() failed to preserve ToolCallID: got %q, want %q", copiedMsg.ToolCallID, originalMsg.ToolCallID)
	}
	if copiedMsg.IsContentParts() != originalMsg.IsContentParts() {
		t.Errorf("Copy() failed to preserve hasContentParts flag")
	}
}
func TestKnownToFieldPreservationScenario(t *testing.T) {
	// Test the specific scenario from the log where KnownTo field was getting lost
	originalMsg := models.RoleMsg{
		Role:    "Alice",
		Content: `Alice: "Okay, Bob. The word is... **'Ephemeral'**. (ooc: @Bob@)"`,
		KnownTo: []string{"Bob"}, // This was detected in the log
	}
	t.Logf("Original message - Role: %s, Content: %s, KnownTo: %v",
		originalMsg.Role, originalMsg.Content, originalMsg.KnownTo)
	// Simulate what happens when the message gets copied during processing
	copiedMsg := originalMsg.Copy()
	t.Logf("Copied message - Role: %s, Content: %s, KnownTo: %v",
		copiedMsg.Role, copiedMsg.Content, copiedMsg.KnownTo)
	// Check if KnownTo field survived the copy
	if len(copiedMsg.KnownTo) == 0 {
		t.Error("ERROR: KnownTo field was lost during copy!")
	} else {
		t.Log("SUCCESS: KnownTo field was preserved during copy!")
	}
	// Verify the content is the same
	if copiedMsg.Content != originalMsg.Content {
		t.Errorf("Content was changed during copy: got %s, want %s", copiedMsg.Content, originalMsg.Content)
	}
	// Verify the KnownTo slice is properly copied
	if !reflect.DeepEqual(copiedMsg.KnownTo, originalMsg.KnownTo) {
		t.Errorf("KnownTo was not properly copied: got %v, want %v", copiedMsg.KnownTo, originalMsg.KnownTo)
	}
}
