package mentions

import (
	"testing"

	"chat-switchboard/models"
)

func roster() []models.ChannelModel {
	return []models.ChannelModel{
		{ID: "1", ChannelID: "ch1", ModelID: "anthropic/claude-3-opus", DisplayName: "Claude-3-Opus", IsDefault: true},
		{ID: "2", ChannelID: "ch1", ModelID: "openai/gpt-4", DisplayName: "GPT-4"},
		{ID: "3", ChannelID: "ch1", ModelID: "anthropic/claude-3-haiku", DisplayName: "Claude-3-Haiku"},
	}
}

func TestParse_NoMentions(t *testing.T) {
	m := Parse("Hello, how are you?", roster())
	if len(m) != 0 {
		t.Fatalf("expected 0 mentions, got %d", len(m))
	}
}

func TestParse_EmptyRoster(t *testing.T) {
	m := Parse("@gpt-4 hello", nil)
	if len(m) != 0 {
		t.Fatalf("expected 0 mentions with empty roster, got %d", len(m))
	}
}

func TestParse_SingleMention(t *testing.T) {
	m := Parse("@GPT-4 what do you think?", roster())
	if len(m) != 1 {
		t.Fatalf("expected 1 mention, got %d", len(m))
	}
	if m[0].Name != "GPT-4" {
		t.Errorf("expected Name='GPT-4', got %q", m[0].Name)
	}
	if m[0].Resolved == nil {
		t.Fatal("expected resolved, got nil")
	}
	if m[0].Resolved.ID != "2" {
		t.Errorf("expected resolved to GPT-4 (ID=2), got ID=%s", m[0].Resolved.ID)
	}
}

func TestParse_CaseInsensitive(t *testing.T) {
	m := Parse("@claude-3-opus please help", roster())
	if len(m) != 1 || m[0].Resolved == nil {
		t.Fatal("expected 1 resolved mention")
	}
	if m[0].Resolved.ID != "1" {
		t.Errorf("expected Claude-3-Opus (ID=1), got ID=%s", m[0].Resolved.ID)
	}
}

func TestParse_MultipleMentions(t *testing.T) {
	m := Parse("Hey @Claude-3-Opus and @GPT-4, compare your approaches.", roster())
	if len(m) != 2 {
		t.Fatalf("expected 2 mentions, got %d", len(m))
	}
	if m[0].Resolved == nil || m[0].Resolved.ID != "1" {
		t.Errorf("first mention: expected Claude-3-Opus")
	}
	if m[1].Resolved == nil || m[1].Resolved.ID != "2" {
		t.Errorf("second mention: expected GPT-4")
	}
}

func TestParse_UnresolvedMention(t *testing.T) {
	m := Parse("@nonexistent-model hello", roster())
	if len(m) != 1 {
		t.Fatalf("expected 1 mention, got %d", len(m))
	}
	if m[0].Resolved != nil {
		t.Errorf("expected unresolved mention, got resolved to %s", m[0].Resolved.DisplayName)
	}
}

func TestParse_MixedResolvedUnresolved(t *testing.T) {
	m := Parse("@GPT-4 and @fake-model what?", roster())
	if len(m) != 2 {
		t.Fatalf("expected 2 mentions, got %d", len(m))
	}
	if m[0].Resolved == nil {
		t.Error("first mention should resolve")
	}
	if m[1].Resolved != nil {
		t.Error("second mention should not resolve")
	}
}

func TestParse_MentionAtStart(t *testing.T) {
	m := Parse("@GPT-4", roster())
	if len(m) != 1 || m[0].Resolved == nil {
		t.Fatal("expected 1 resolved mention at start")
	}
}

func TestParse_MentionInMiddle(t *testing.T) {
	m := Parse("Ask @GPT-4 about this", roster())
	if len(m) != 1 || m[0].Resolved == nil {
		t.Fatal("expected 1 resolved mention in middle")
	}
}

func TestParse_NoSpaceBefore(t *testing.T) {
	// email@GPT-4 should NOT be parsed as a mention
	m := Parse("email@GPT-4 test", roster())
	if len(m) != 0 {
		t.Fatalf("expected 0 mentions (no space before @), got %d", len(m))
	}
}

func TestParse_BareAt(t *testing.T) {
	m := Parse("@ nothing", roster())
	if len(m) != 0 {
		t.Fatalf("expected 0 mentions for bare @, got %d", len(m))
	}
}

func TestParse_HyphenSpaceNormalization(t *testing.T) {
	// Display name "Claude-3-Opus" should match "@claude_3_opus" (underscore normalization)
	m := Parse("@claude_3_opus help", roster())
	if len(m) != 1 || m[0].Resolved == nil {
		t.Fatal("expected underscore-normalized match")
	}
	if m[0].Resolved.ID != "1" {
		t.Errorf("expected Claude-3-Opus, got %s", m[0].Resolved.DisplayName)
	}
}

func TestParse_ByteOffsets(t *testing.T) {
	content := "Hey @GPT-4 ok"
	m := Parse(content, roster())
	if len(m) != 1 {
		t.Fatal("expected 1 mention")
	}
	if m[0].Start != 4 || m[0].End != 10 {
		t.Errorf("expected offsets [4,10), got [%d,%d)", m[0].Start, m[0].End)
	}
	if content[m[0].Start:m[0].End] != "@GPT-4" {
		t.Errorf("offsets don't match: %q", content[m[0].Start:m[0].End])
	}
}

func TestParse_TrailingPunctuation(t *testing.T) {
	// Comma, period, exclamation should be stripped
	tests := []struct {
		input string
		want  int // expected resolved count
	}{
		{"@GPT-4, what?", 1},
		{"@GPT-4.", 1},
		{"@GPT-4!", 1},
		{"@GPT-4? help", 1},
		{"(@GPT-4)", 0},   // ( before @ is not whitespace — not a mention
		{"( @GPT-4)", 1},  // space before @ — valid mention, ) stripped
	}
	for _, tt := range tests {
		m := Parse(tt.input, roster())
		resolved := 0
		for _, mm := range m {
			if mm.Resolved != nil {
				resolved++
			}
		}
		if resolved != tt.want {
			t.Errorf("Parse(%q): expected %d resolved, got %d", tt.input, tt.want, resolved)
		}
	}
}

func TestResolvedModels_Dedup(t *testing.T) {
	m := Parse("@GPT-4 do this @GPT-4 and that", roster())
	resolved := ResolvedModels(m)
	if len(resolved) != 1 {
		t.Fatalf("expected 1 deduplicated model, got %d", len(resolved))
	}
}

func TestResolvedModels_NilOnNoResolve(t *testing.T) {
	m := Parse("@unknown-model test", roster())
	resolved := ResolvedModels(m)
	if resolved != nil {
		t.Fatalf("expected nil for unresolved only, got %d", len(resolved))
	}
}

func TestResolvedModels_MultipleDistinct(t *testing.T) {
	m := Parse("@GPT-4 and @Claude-3-Opus compare", roster())
	resolved := ResolvedModels(m)
	if len(resolved) != 2 {
		t.Fatalf("expected 2 distinct models, got %d", len(resolved))
	}
}

func TestNormalize(t *testing.T) {
	tests := []struct {
		in, want string
	}{
		{"Claude-3-Opus", "claude 3 opus"},
		{"GPT-4", "gpt 4"},
		{"gpt_4", "gpt 4"},
		{"  mixed--Case__Name  ", "mixed case name"},
	}
	for _, tt := range tests {
		got := normalize(tt.in)
		if got != tt.want {
			t.Errorf("normalize(%q) = %q, want %q", tt.in, got, tt.want)
		}
	}
}
