//go:build unit

package service

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

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

type duplicateGroupRepoStub struct {
	GroupRepository
	nextID             int64
	groups             map[int64]*Group
	names              map[string]struct{}
	byOperation        map[string]int64
	sourceBindings     map[int64][]AccountGroup
	createdBindings    map[int64][]AccountGroup
	createdFromSources []int64
	atomicCreateErr    error
}

func newDuplicateGroupRepoStub(source *Group) *duplicateGroupRepoStub {
	repo := &duplicateGroupRepoStub{
		nextID:          100,
		groups:          make(map[int64]*Group),
		names:           make(map[string]struct{}),
		byOperation:     make(map[string]int64),
		sourceBindings:  make(map[int64][]AccountGroup),
		createdBindings: make(map[int64][]AccountGroup),
	}
	if source != nil {
		repo.groups[source.ID] = source
		repo.names[source.Name] = struct{}{}
	}
	return repo
}

func cloneGroupForDuplicateTest(group *Group) *Group {
	if group == nil {
		return nil
	}
	cloned := *group
	cloned.DailyLimitUSD = cloneGroupValuePointer(group.DailyLimitUSD)
	cloned.WeeklyLimitUSD = cloneGroupValuePointer(group.WeeklyLimitUSD)
	cloned.MonthlyLimitUSD = cloneGroupValuePointer(group.MonthlyLimitUSD)
	cloned.ImagePrice1K = cloneGroupValuePointer(group.ImagePrice1K)
	cloned.ImagePrice2K = cloneGroupValuePointer(group.ImagePrice2K)
	cloned.ImagePrice4K = cloneGroupValuePointer(group.ImagePrice4K)
	cloned.VideoPrice480P = cloneGroupValuePointer(group.VideoPrice480P)
	cloned.VideoPrice720P = cloneGroupValuePointer(group.VideoPrice720P)
	cloned.VideoPrice1080P = cloneGroupValuePointer(group.VideoPrice1080P)
	cloned.WebSearchPricePerCall = cloneGroupValuePointer(group.WebSearchPricePerCall)
	cloned.FallbackGroupID = cloneGroupValuePointer(group.FallbackGroupID)
	cloned.FallbackGroupIDOnInvalidRequest = cloneGroupValuePointer(group.FallbackGroupIDOnInvalidRequest)
	cloned.ModelRouting = cloneGroupModelRouting(group.ModelRouting)
	cloned.SupportedModelScopes = append([]string(nil), group.SupportedModelScopes...)
	cloned.MessagesDispatchModelConfig = cloneGroupMessagesDispatchModelConfig(group.MessagesDispatchModelConfig)
	cloned.ModelsListConfig.Models = append([]string(nil), group.ModelsListConfig.Models...)
	cloned.AccountGroups = append([]AccountGroup(nil), group.AccountGroups...)
	return &cloned
}

func (r *duplicateGroupRepoStub) GetByID(_ context.Context, id int64) (*Group, error) {
	group := r.groups[id]
	if group == nil {
		return nil, ErrGroupNotFound
	}
	cloned := cloneGroupForDuplicateTest(group)
	cloned.Hydrated = true
	return cloned, nil
}

func (r *duplicateGroupRepoStub) FindByDuplicateOperationID(_ context.Context, operationID string) (*Group, error) {
	id := r.byOperation[operationID]
	if id == 0 {
		return nil, nil
	}
	return cloneGroupForDuplicateTest(r.groups[id]), nil
}

func (r *duplicateGroupRepoStub) CreateFromSource(_ context.Context, group *Group, sourceGroupID int64) error {
	if r.atomicCreateErr != nil {
		return r.atomicCreateErr
	}
	if group.DuplicateOperationID != "" {
		if _, exists := r.byOperation[group.DuplicateOperationID]; exists {
			return ErrGroupExists
		}
	}
	if _, exists := r.names[group.Name]; exists {
		return ErrGroupExists
	}
	r.nextID++
	group.ID = r.nextID
	group.CreatedAt = time.Now().UTC()
	group.UpdatedAt = group.CreatedAt
	bindings := append([]AccountGroup(nil), r.sourceBindings[sourceGroupID]...)
	for i := range bindings {
		bindings[i].GroupID = group.ID
	}
	group.AccountCount = int64(len(bindings))
	group.ActiveAccountCount = int64(len(bindings))
	r.createdBindings[group.ID] = bindings
	r.createdFromSources = append(r.createdFromSources, sourceGroupID)
	r.names[group.Name] = struct{}{}
	r.groups[group.ID] = cloneGroupForDuplicateTest(group)
	if group.DuplicateOperationID != "" {
		r.byOperation[group.DuplicateOperationID] = group.ID
	}
	return nil
}

func groupDuplicateTestPointer[T any](value T) *T { return &value }

func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing.T) {
	createdAt := time.Date(2026, time.July, 1, 2, 3, 4, 0, time.UTC)
	source := &Group{
		ID:                           41,
		Name:                         "高级订阅",
		Description:                  "configuration",
		Platform:                     PlatformOpenAI,
		RateMultiplier:               1.75,
		PeakRateEnabled:              true,
		PeakStart:                    "09:00",
		PeakEnd:                      "18:00",
		PeakRateMultiplier:           1.2,
		IsExclusive:                  true,
		Status:                       StatusActive,
		Hydrated:                     true,
		SubscriptionType:             SubscriptionTypeSubscription,
		DailyLimitUSD:                groupDuplicateTestPointer(11.0),
		WeeklyLimitUSD:               groupDuplicateTestPointer(22.0),
		MonthlyLimitUSD:              groupDuplicateTestPointer(33.0),
		DefaultValidityDays:          91,
		AllowImageGeneration:         true,
		AllowBatchImageGeneration:    true,
		ImageRateIndependent:         true,
		ImageRateMultiplier:          1.4,
		ImagePrice1K:                 groupDuplicateTestPointer(0.01),
		ImagePrice2K:                 groupDuplicateTestPointer(0.02),
		ImagePrice4K:                 groupDuplicateTestPointer(0.04),
		BatchImageDiscountMultiplier: 0.4,
		BatchImageHoldMultiplier:     0.7,
		VideoRateIndependent:         true,
		VideoRateMultiplier:          2.1,
		VideoPrice480P:               groupDuplicateTestPointer(0.1),
		VideoPrice720P:               groupDuplicateTestPointer(0.2),
		VideoPrice1080P:              groupDuplicateTestPointer(0.3),
		VideoModelPrices: map[string]map[string]float64{
			VideoPriceFamilyGrokImagineVideo15: {VideoBillingResolution720P: 0.14},
		},
		WebSearchPricePerCall:           groupDuplicateTestPointer(0.005),
		ClaudeCodeOnly:                  true,
		FallbackGroupID:                 groupDuplicateTestPointer(int64(7)),
		FallbackGroupIDOnInvalidRequest: groupDuplicateTestPointer(int64(8)),
		ModelRouting:                    map[string][]int64{"gpt-*": {13, 17}},
		ModelRoutingEnabled:             true,
		MCPXMLInject:                    true,
		SupportedModelScopes:            []string{"claude", "gemini_text"},
		SortOrder:                       9,
		AllowMessagesDispatch:           true,
		AllowLive:                       true,
		RequireOAuthOnly:                true,
		RequirePrivacySet:               true,
		DefaultMappedModel:              "gpt-5.4",
		MessagesDispatchModelConfig: OpenAIMessagesDispatchModelConfig{
			OpusMappedModel:    "gpt-5.4",
			SonnetMappedModel:  "gpt-5.3",
			HaikuMappedModel:   "gpt-5-mini",
			ExactModelMappings: map[string]string{"claude-special": "gpt-special"},
		},
		ModelsListConfig:        GroupModelsListConfig{Enabled: true, Models: []string{"gpt-5.4", "gpt-5-mini"}},
		RPMLimit:                99,
		MaxReasoningEffort:      "medium",
		ReasoningEffortMappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}},
		CreatedAt:               createdAt,
		UpdatedAt:               createdAt,
		AccountCount:            12,
		ActiveAccountCount:      8,
		RateLimitedAccountCount: 2,
		DuplicateOperationID:    "old-operation-must-not-copy",
		AccountGroups:           []AccountGroup{{AccountID: 13, GroupID: 41, Priority: 37}},
	}
	repo := newDuplicateGroupRepoStub(source)
	repo.sourceBindings[source.ID] = []AccountGroup{
		{AccountID: 13, GroupID: source.ID, Priority: 37},
		{AccountID: 17, GroupID: source.ID, Priority: 8},
	}
	svc := &adminServiceImpl{groupRepo: repo, groupDuplicateRepo: repo}

	duplicate, err := svc.DuplicateGroup(context.Background(), source.ID, "admin:7", "stable-key")

	require.NoError(t, err)
	require.NotEqual(t, source.ID, duplicate.ID)
	require.Equal(t, "高级订阅 (Copy)", duplicate.Name)
	require.Equal(t, duplicateGroupInactiveStatus, duplicate.Status)
	require.True(t, duplicate.Hydrated, "the duplicate response is reloaded with derived counts")
	require.Equal(t, source.Description, duplicate.Description)
	require.Equal(t, source.Platform, duplicate.Platform)
	require.Equal(t, source.RateMultiplier, duplicate.RateMultiplier)
	require.Equal(t, source.PeakRateMultiplier, duplicate.PeakRateMultiplier)
	require.Equal(t, source.DefaultValidityDays, duplicate.DefaultValidityDays)
	require.Equal(t, source.ImagePrice4K, duplicate.ImagePrice4K)
	require.Equal(t, source.VideoModelPrices, duplicate.VideoModelPrices)
	require.Equal(t, source.WebSearchPricePerCall, duplicate.WebSearchPricePerCall)
	require.Equal(t, source.FallbackGroupID, duplicate.FallbackGroupID)
	require.Equal(t, source.ModelRouting, duplicate.ModelRouting)
	require.Equal(t, source.MessagesDispatchModelConfig, duplicate.MessagesDispatchModelConfig)
	require.Equal(t, source.ModelsListConfig, duplicate.ModelsListConfig)
	require.Equal(t, source.RPMLimit, duplicate.RPMLimit)
	require.Equal(t, source.MaxReasoningEffort, duplicate.MaxReasoningEffort)
	require.Equal(t, source.ReasoningEffortMappings, duplicate.ReasoningEffortMappings)
	require.EqualValues(t, 2, duplicate.AccountCount)
	require.EqualValues(t, 2, duplicate.ActiveAccountCount)
	require.NotEmpty(t, duplicate.DuplicateOperationID)
	require.Equal(t, []int64{source.ID}, repo.createdFromSources)
	require.Equal(t, []AccountGroup{
		{AccountID: 13, GroupID: duplicate.ID, Priority: 37},
		{AccountID: 17, GroupID: duplicate.ID, Priority: 8},
	}, repo.createdBindings[duplicate.ID])

	duplicate.ModelRouting["gpt-*"][0] = 999
	duplicate.VideoModelPrices[VideoPriceFamilyGrokImagineVideo15][VideoBillingResolution720P] = 999
	duplicate.SupportedModelScopes[0] = "changed"
	duplicate.MessagesDispatchModelConfig.ExactModelMappings["claude-special"] = "changed"
	duplicate.ModelsListConfig.Models[0] = "changed"
	duplicate.ReasoningEffortMappings[0].To = "changed"
	*duplicate.DailyLimitUSD = 999
	require.Equal(t, int64(13), source.ModelRouting["gpt-*"][0])
	require.Equal(t, 0.14, source.VideoModelPrices[VideoPriceFamilyGrokImagineVideo15][VideoBillingResolution720P])
	require.Equal(t, "claude", source.SupportedModelScopes[0])
	require.Equal(t, "gpt-special", source.MessagesDispatchModelConfig.ExactModelMappings["claude-special"])
	require.Equal(t, "gpt-5.4", source.ModelsListConfig.Models[0])
	require.Equal(t, "xhigh", source.ReasoningEffortMappings[0].To)
	require.Equal(t, 11.0, *source.DailyLimitUSD)
}

func TestDuplicateGroupRecoversSameOperationAndScopesByAdmin(t *testing.T) {
	source := &Group{ID: 9, Name: "team", Platform: PlatformAnthropic, Status: StatusActive}
	repo := newDuplicateGroupRepoStub(source)
	svc := &adminServiceImpl{groupRepo: repo, groupDuplicateRepo: repo}
	ctx := context.Background()

	first, err := svc.DuplicateGroup(ctx, source.ID, "admin:7", "same-key")
	require.NoError(t, err)
	retry, err := svc.DuplicateGroup(ctx, source.ID, "admin:7", "same-key")
	require.NoError(t, err)
	recovered, err := svc.RecoverDuplicateGroup(ctx, source.ID, "admin:7", "same-key")
	require.NoError(t, err)
	otherAdmin, err := svc.DuplicateGroup(ctx, source.ID, "admin:8", "same-key")
	require.NoError(t, err)

	require.Equal(t, first.ID, retry.ID)
	require.Equal(t, first.ID, recovered.ID)
	require.NotEqual(t, first.ID, otherAdmin.ID)
	require.Equal(t, "team (Copy 2)", otherAdmin.Name)
}

func TestDuplicateGroupAdvancesNameAndTruncatesUnicodeByRunes(t *testing.T) {
	source := &Group{ID: 12, Name: "team", Platform: PlatformAnthropic, Status: StatusActive}
	repo := newDuplicateGroupRepoStub(source)
	repo.names["team (Copy)"] = struct{}{}
	svc := &adminServiceImpl{groupRepo: repo, groupDuplicateRepo: repo}

	duplicate, err := svc.DuplicateGroup(context.Background(), source.ID, "admin:1", "")
	require.NoError(t, err)
	require.Equal(t, "team (Copy 2)", duplicate.Name)

	unicodeName := duplicateGroupName(strings.Repeat("组", 100), 23)
	require.Equal(t, maxGroupNameRunes, utf8.RuneCountInString(unicodeName))
	require.True(t, strings.HasSuffix(unicodeName, " (Copy 23)"))
}

func TestDuplicateGroupAtomicCreateFailureReturnsNoCopy(t *testing.T) {
	source := &Group{ID: 15, Name: "team", Platform: PlatformAnthropic, Status: StatusActive}
	repo := newDuplicateGroupRepoStub(source)
	repo.atomicCreateErr = errors.New("binding insert failed")
	svc := &adminServiceImpl{groupRepo: repo, groupDuplicateRepo: repo}

	duplicate, err := svc.DuplicateGroup(context.Background(), source.ID, "admin:1", "key")

	require.ErrorContains(t, err, "binding insert failed")
	require.Nil(t, duplicate)
	require.Len(t, repo.groups, 1)
	require.Empty(t, repo.byOperation)
}
