package runtime

import (
	"math/rand"
	"testing"

	"github.com/tidbcloud/AgentTeam/internal/apr/types"
)

// mockProvider is a minimal LLMProvider implementation for testing.
type mockProvider struct {
	name   string
	models []types.Model
}

func (m *mockProvider) Name() string          { return m.name }
func (m *mockProvider) Models() []types.Model { return m.models }

func newMockProvider(name string, modelIDs ...string) *mockProvider {
	models := make([]types.Model, len(modelIDs))
	for i, id := range modelIDs {
		models[i] = types.Model{ID: id}
	}
	return &mockProvider{name: name, models: models}
}

// ---------- Lookup ----------

func newTestDispatcher(providers []types.LLMProvider, routes []ModelRoute) *Dispatcher {
	d := &Dispatcher{
		providers:      make(map[string]types.LLMProvider),
		modelProviders: make(map[string][]string),
		routes:         routes,
		stopCh:         make(chan struct{}),
		syncDone:       make(chan struct{}),
		randomSource:   rand.New(rand.NewSource(42)),
	}
	// Don't start syncLoop; manually register providers.
	for _, p := range providers {
		d.providers[p.Name()] = p
		d.syncProviderModels(p.Name(), p)
	}
	close(d.stopCh)
	close(d.syncDone)
	return d
}

func TestLookup_ExactRoute(t *testing.T) {
	zhipu := newMockProvider("zhipu", "claude-sonnet-4-20250514", "claude-haiku-4.5")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"zhipu"}},
		},
	)

	p, model := d.Lookup("", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu provider, got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected model claude-sonnet-4-20250514, got %s", model)
	}

	// Model not in route should not match
	p, _ = d.Lookup("", "anthropic/claude-haiku-4.5")
	if p != nil {
		t.Fatalf("expected nil for claude-haiku-4.5 not in route, got %s", p.Name())
	}
}

func TestLookup_WildcardRoute(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514", "claude-haiku-4.5", "gpt-4o")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514", "claude-haiku-4.5"}, Providers: []string{"ark"}},
		},
	)

	// Both exact model names should match
	for _, model := range []string{"claude-sonnet-4-20250514", "claude-haiku-4.5"} {
		p, m := d.Lookup("", "anthropic/"+model)
		if p == nil || p.Name() != "ark" {
			t.Errorf("Lookup(anthropic/%s): expected ark, got %v", model, p)
		}
		if m != model {
			t.Errorf("Lookup(anthropic/%s): model = %q", model, m)
		}
	}

	// gpt-4o should not match (not in route models)
	p, _ := d.Lookup("", "anthropic/gpt-4o")
	if p != nil {
		t.Errorf("expected nil for gpt-4o not in route models, got %s", p.Name())
	}

	// Wildcard pattern should NOT match (exact match only)
	p, _ = d.Lookup("", "anthropic/claude-")
	if p != nil {
		t.Errorf("expected nil for prefix match, got %s", p.Name())
	}
}

func TestLookup_EmptyModelsAllowsAll(t *testing.T) {
	zhipu := newMockProvider("zhipu", "any-model")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{}, Providers: []string{"zhipu"}},
		},
	)

	p, _ := d.Lookup("", "anthropic/anything-goes")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu when route.Models is empty, got %v", p)
	}
}

func TestLookup_DirectModel(t *testing.T) {
	zhipu := newMockProvider("zhipu", "claude-sonnet-4-20250514")
	d := newTestDispatcher([]types.LLMProvider{zhipu}, nil)

	p, model := d.Lookup("", "claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu for direct lookup, got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected model claude-sonnet-4-20250514, got %s", model)
	}
}

func TestLookup_ProviderRedirect(t *testing.T) {
	zhipu := newMockProvider("zhipu", "claude-sonnet-4-20250514")
	d := newTestDispatcher([]types.LLMProvider{zhipu}, nil)

	// "glm" redirects to "zhipu"
	p, model := d.Lookup("", "glm/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu via glm redirect, got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected model claude-sonnet-4-20250514, got %s", model)
	}
}

func TestLookup_NotFound(t *testing.T) {
	d := newTestDispatcher(nil, nil)

	p, _ := d.Lookup("", "nonexistent/model")
	if p != nil {
		t.Fatalf("expected nil for unknown vendor, got %s", p.Name())
	}
}

// ---------- Models ----------

func TestModels_ExactFilter(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514", "claude-haiku-4.5", "gpt-4o")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	models := d.Models()
	if len(models["anthropic"]) != 1 {
		t.Fatalf("expected 1 model, got %d: %v", len(models["anthropic"]), models["anthropic"])
	}
	if models["anthropic"][0].ID != "claude-sonnet-4-20250514" {
		t.Fatalf("expected claude-sonnet-4-20250514, got %s", models["anthropic"][0].ID)
	}
}

func TestModels_WildcardFilter(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514", "claude-haiku-4.5", "gpt-4o")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514", "claude-haiku-4.5"}, Providers: []string{"ark"}},
		},
	)

	models := d.Models()
	if len(models["anthropic"]) != 2 {
		t.Fatalf("expected 2 anthropic models, got %d: %v", len(models["anthropic"]), models["anthropic"])
	}

	ids := map[string]bool{}
	for _, m := range models["anthropic"] {
		ids[m.ID] = true
	}
	if !ids["claude-sonnet-4-20250514"] || !ids["claude-haiku-4.5"] {
		t.Fatalf("expected both claude models, got %v", ids)
	}
	if ids["gpt-4o"] {
		t.Fatal("gpt-4o should not be in anthropic models")
	}
}

func TestModels_EmptyModelsSkipped(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{}, Providers: []string{"ark"}},
		},
	)

	models := d.Models()
	if len(models["anthropic"]) != 0 {
		t.Fatalf("expected 0 models when route.Models is empty, got %d", len(models["anthropic"]))
	}
}

// ---------- Model Replacement ----------

func TestReplacement_Probability1(t *testing.T) {
	zhipu := newMockProvider("zhipu", "glm-4.7")
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu, ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
			{Vendor: "zhipu", Models: []string{"glm-4.7"}, Providers: []string{"zhipu"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu provider (replaced), got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected model glm-4.7 (replaced), got %s", model)
	}

	stats := d.ReplacementStats()
	if len(stats) != 1 {
		t.Fatalf("expected 1 stat entry, got %d", len(stats))
	}
	if stats[0].RequestCount != 1 {
		t.Errorf("expected RequestCount=1, got %d", stats[0].RequestCount)
	}
	if stats[0].HitCount != 1 {
		t.Errorf("expected HitCount=1, got %d", stats[0].HitCount)
	}
}

func TestReplacement_Probability0(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "ark" {
		t.Fatalf("expected ark provider (not replaced), got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected original model (not replaced), got %s", model)
	}

	stats := d.ReplacementStats()
	if stats[0].RequestCount != 0 {
		t.Errorf("expected RequestCount=0 for prob=0, got %d", stats[0].RequestCount)
	}
	if stats[0].HitCount != 0 {
		t.Errorf("expected HitCount=0 for prob=0, got %d", stats[0].HitCount)
	}
}

func TestReplacement_WrongKey(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("other-key", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "ark" {
		t.Fatalf("expected ark provider (no matching key), got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected original model (no matching key), got %s", model)
	}

	stats := d.ReplacementStats()
	if stats[0].RequestCount != 0 {
		t.Errorf("expected RequestCount=0 for wrong key, got %d", stats[0].RequestCount)
	}
}

func TestReplacement_EmptyKey(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "ark" {
		t.Fatalf("expected ark provider (empty key), got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected original model (empty key), got %s", model)
	}
}

func TestReplacement_MultipleRules(t *testing.T) {
	zhipu := newMockProvider("zhipu", "glm-4.7", "glm-5")
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu, ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
			{Vendor: "zhipu", Models: []string{"glm-4.7", "glm-5"}, Providers: []string{"zhipu"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
		{APIKey: "key1", Model: "zhipu/glm-4.7", TargetModel: "zhipu/glm-5", Probability: 1.0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu provider, got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected model glm-4.7, got %s", model)
	}

	p, model = d.Lookup("key1", "zhipu/glm-4.7")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu provider, got %v", p)
	}
	if model != "glm-5" {
		t.Fatalf("expected model glm-5, got %s", model)
	}

	stats := d.ReplacementStats()
	if len(stats) != 2 {
		t.Fatalf("expected 2 stat entries, got %d", len(stats))
	}
	if stats[0].RequestCount != 1 || stats[0].HitCount != 1 {
		t.Errorf("first rule: expected RequestCount=1 HitCount=1, got %d/%d", stats[0].RequestCount, stats[0].HitCount)
	}
	if stats[1].RequestCount != 1 || stats[1].HitCount != 1 {
		t.Errorf("second rule: expected RequestCount=1 HitCount=1, got %d/%d", stats[1].RequestCount, stats[1].HitCount)
	}
}

func TestReplacement_CaseInsensitive(t *testing.T) {
	zhipu := newMockProvider("zhipu", "glm-4.7")
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu, ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
			{Vendor: "zhipu", Models: []string{"glm-4.7"}, Providers: []string{"zhipu"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "Anthropic/Claude-Sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected replacement with case-insensitive match, got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected glm-4.7, got %s", model)
	}
}

func TestReplacement_SetPreservesCounters(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
		{APIKey: "key2", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-5", Probability: 1.0},
	})

	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	d.Lookup("key2", "anthropic/claude-sonnet-4-20250514")

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	stats := d.ReplacementStats()
	if len(stats) != 1 {
		t.Fatalf("expected 1 stat entry, got %d", len(stats))
	}
	if stats[0].RequestCount != 2 {
		t.Errorf("expected preserved RequestCount=2, got %d", stats[0].RequestCount)
	}
	if stats[0].HitCount != 2 {
		t.Errorf("expected preserved HitCount=2, got %d", stats[0].HitCount)
	}
}

func TestReplacement_SetResetsRemovedRuleCounters(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})
	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")

	d.SetModelReplacements(nil)

	stats := d.ReplacementStats()
	if len(stats) != 0 {
		t.Fatalf("expected 0 stat entries after clearing, got %d", len(stats))
	}

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "ark" {
		t.Fatalf("expected ark provider after clearing rules, got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected original model after clearing rules, got %s", model)
	}
}

func TestReplacement_StatsReturnsRawKey(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "sk-longapikey1234567890", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	stats := d.ReplacementStats()
	if stats[0].APIKey != "sk-longapikey1234567890" {
		t.Errorf("expected raw key, got '%s'", stats[0].APIKey)
	}
}

func TestReplacement_RequestCountIncrementedOnMiss(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 0},
	})

	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")

	stats := d.ReplacementStats()
	if stats[0].RequestCount != 0 {
		t.Errorf("expected RequestCount=0 for prob=0 (skip), got %d", stats[0].RequestCount)
	}
	if stats[0].HitCount != 0 {
		t.Errorf("expected HitCount=0, got %d", stats[0].HitCount)
	}
}

func TestReplacement_GlobWildcard(t *testing.T) {
	zhipu := newMockProvider("zhipu", "glm-4.7")
	ark := newMockProvider("ark", "claude-sonnet-4-20250514", "claude-haiku-4.5")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu, ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514", "claude-haiku-4.5"}, Providers: []string{"ark"}},
			{Vendor: "zhipu", Models: []string{"glm-4.7"}, Providers: []string{"zhipu"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/*", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu (glob match), got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected glm-4.7, got %s", model)
	}

	p, model = d.Lookup("key1", "anthropic/claude-haiku-4.5")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu (glob match second model), got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected glm-4.7, got %s", model)
	}

	p, model = d.Lookup("key1", "zhipu/glm-4.7")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu (no glob match), got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected glm-4.7 (unchanged), got %s", model)
	}

	stats := d.ReplacementStats()
	if stats[0].RequestCount != 2 {
		t.Errorf("expected RequestCount=2, got %d", stats[0].RequestCount)
	}
	if stats[0].HitCount != 2 {
		t.Errorf("expected HitCount=2, got %d", stats[0].HitCount)
	}
}

func TestReplacement_GlobPrefix(t *testing.T) {
	zhipu := newMockProvider("zhipu", "glm-4.7")
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu, ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
			{Vendor: "zhipu", Models: []string{"glm-4.7"}, Providers: []string{"zhipu"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "*/claude-sonnet*", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu (glob prefix match), got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected glm-4.7, got %s", model)
	}
}

func TestReplacement_GlobMatchAll(t *testing.T) {
	zhipu := newMockProvider("zhipu", "glm-4.7")
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{zhipu, ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
			{Vendor: "zhipu", Models: []string{"glm-4.7"}, Providers: []string{"zhipu"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "*", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu (match all), got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected glm-4.7, got %s", model)
	}

	p, model = d.Lookup("key1", "zhipu/glm-4.7")
	if p == nil || p.Name() != "zhipu" {
		t.Fatalf("expected zhipu (match all second), got %v", p)
	}
	if model != "glm-4.7" {
		t.Fatalf("expected glm-4.7, got %s", model)
	}
}

func TestReplacement_GlobNoMatch(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "openai/*", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	p, model := d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	if p == nil || p.Name() != "ark" {
		t.Fatalf("expected ark (no glob match), got %v", p)
	}
	if model != "claude-sonnet-4-20250514" {
		t.Fatalf("expected original model (no glob match), got %s", model)
	}
}

func TestReplacement_IDComputedFromFields(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	stats := d.ReplacementStats()
	if len(stats) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(stats))
	}
	if stats[0].ID == "" {
		t.Fatal("expected non-empty ID")
	}

	// Same fields must produce same ID
	d2 := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)
	d2.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})
	stats2 := d2.ReplacementStats()
	if stats[0].ID != stats2[0].ID {
		t.Errorf("same fields should produce same ID: %q vs %q", stats[0].ID, stats2[0].ID)
	}
}

func TestReplacement_IDDifferentForDifferentFields(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-5", Probability: 1.0},
	})

	stats := d.ReplacementStats()
	if stats[0].ID == stats[1].ID {
		t.Errorf("different fields should produce different IDs, both got %q", stats[0].ID)
	}
}

func TestReplacement_IDPreservesCounters(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})
	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")
	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")

	// Re-set with same fields (ID auto-computed will match)
	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})

	stats := d.ReplacementStats()
	if stats[0].RequestCount != 3 {
		t.Errorf("expected preserved RequestCount=3, got %d", stats[0].RequestCount)
	}
	if stats[0].HitCount != 3 {
		t.Errorf("expected preserved HitCount=3, got %d", stats[0].HitCount)
	}
}

func TestReplacement_IDResetsOnFieldChange(t *testing.T) {
	ark := newMockProvider("ark", "claude-sonnet-4-20250514")
	d := newTestDispatcher(
		[]types.LLMProvider{ark},
		[]ModelRoute{
			{Vendor: "anthropic", Models: []string{"claude-sonnet-4-20250514"}, Providers: []string{"ark"}},
		},
	)

	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-4.7", Probability: 1.0},
	})
	d.Lookup("key1", "anthropic/claude-sonnet-4-20250514")

	// Change target_model -> different ID -> counters reset
	d.SetModelReplacements([]ModelReplacement{
		{APIKey: "key1", Model: "anthropic/claude-sonnet-4-20250514", TargetModel: "zhipu/glm-5", Probability: 1.0},
	})

	stats := d.ReplacementStats()
	if stats[0].RequestCount != 0 {
		t.Errorf("expected reset RequestCount=0 after field change, got %d", stats[0].RequestCount)
	}
	if stats[0].HitCount != 0 {
		t.Errorf("expected reset HitCount=0 after field change, got %d", stats[0].HitCount)
	}
}
