package mapping import ( "context" "encoding/json" "testing" "time" "qctextbuilder/internal/domain" "qctextbuilder/internal/qcclient" ) func TestSuggestFieldValues_FillsEmptyMappedFields(t *testing.T) { t.Parallel() fields := []domain.TemplateField{ {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}, {Path: "services.servicesTitle_r4830_8", KeyName: "servicesTitle_r4830_8", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionServices}, {Path: "services.servicesDescription_r4830_9", KeyName: "servicesDescription_r4830_9", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionServices}, {Path: "text.buttonText_c1165_1", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA}, } result := SuggestFieldValues(SuggestionRequest{ Fields: fields, GlobalData: map[string]any{ "companyName": "Muster AG", "businessType": "Solar", }, DraftContext: &domain.DraftContext{ LLM: domain.DraftLLMContext{ WebsiteSummary: "Wir planen und installieren Solaranlagen fuer KMU und Privatkunden.", StyleProfile: domain.DraftStyleProfile{ ContentTone: "professionell", }, }, }, Existing: map[string]string{}, }) if _, ok := result.ByFieldPath["text.textTitle_m1710_1"]; !ok { t.Fatalf("expected hero title suggestion") } if got := result.ByFieldPath["text.buttonText_c1165_1"].Value; got == "" { t.Fatalf("expected cta suggestion") } if got := result.ByFieldPath["services.servicesDescription_r4830_9"].Slot; got != "service_items[0].description" { t.Fatalf("unexpected slot: %q", got) } } func TestSuggestFieldValues_RespectsExistingValues(t *testing.T) { t.Parallel() fields := []domain.TemplateField{ {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}, } result := SuggestFieldValues(SuggestionRequest{ Fields: fields, GlobalData: map[string]any{ "companyName": "Muster AG", }, Existing: map[string]string{ "text.textTitle_m1710_1": "Schon gesetzt", }, }) if len(result.Suggestions) != 0 { t.Fatalf("expected no suggestions, got %d", len(result.Suggestions)) } } func TestGenerateAllSuggestions_IncludesFilledFields(t *testing.T) { t.Parallel() fields := []domain.TemplateField{ {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}, } state := GenerateAllSuggestions(context.Background(), nil, SuggestionRequest{ Fields: fields, GlobalData: map[string]any{ "companyName": "Muster AG", }, Existing: map[string]string{ "text.textTitle_m1710_1": "Bereits gesetzt", }, }, domain.DraftSuggestionState{}, time.Now().UTC()) if _, ok := state.ByFieldPath["text.textTitle_m1710_1"]; !ok { t.Fatalf("expected suggestion for filled field") } } func TestApplySuggestionsToEmptyFields_DoesNotOverwriteExisting(t *testing.T) { t.Parallel() now := time.Now().UTC() values, state := ApplySuggestionsToEmptyFields(map[string]string{ "field.hero": "Custom", }, domain.DraftSuggestionState{ ByFieldPath: map[string]domain.DraftSuggestion{ "field.hero": { FieldPath: "field.hero", Value: "Suggestion", Status: domain.DraftSuggestionStatusSuggested, }, "field.cta": { FieldPath: "field.cta", Value: "Jetzt anfragen", Status: domain.DraftSuggestionStatusSuggested, }, }, }, now) if got := values["field.hero"]; got != "Custom" { t.Fatalf("expected existing value unchanged, got %q", got) } if got := values["field.cta"]; got != "Jetzt anfragen" { t.Fatalf("expected empty value filled, got %q", got) } if got := state.ByFieldPath["field.hero"].Status; got != domain.DraftSuggestionStatusSuggested { t.Fatalf("expected hero status unchanged, got %q", got) } if got := state.ByFieldPath["field.cta"].Status; got != domain.DraftSuggestionStatusApplied { t.Fatalf("expected cta status applied, got %q", got) } } func TestApplyAllSuggestions_OverwritesExisting(t *testing.T) { t.Parallel() now := time.Now().UTC() values, state := ApplyAllSuggestions(map[string]string{ "field.hero": "Custom", }, domain.DraftSuggestionState{ ByFieldPath: map[string]domain.DraftSuggestion{ "field.hero": { FieldPath: "field.hero", Value: "Suggestion", Status: domain.DraftSuggestionStatusSuggested, }, "field.cta": { FieldPath: "field.cta", Value: "Jetzt anfragen", Status: domain.DraftSuggestionStatusSuggested, }, }, }, now) if got := values["field.hero"]; got != "Suggestion" { t.Fatalf("expected existing value overwritten, got %q", got) } if got := values["field.cta"]; got != "Jetzt anfragen" { t.Fatalf("expected cta applied, got %q", got) } if got := state.ByFieldPath["field.hero"].Status; got != domain.DraftSuggestionStatusApplied { t.Fatalf("expected hero status applied, got %q", got) } if got := state.ByFieldPath["field.cta"].Status; got != domain.DraftSuggestionStatusApplied { t.Fatalf("expected cta status applied, got %q", got) } } func TestRegenerateFieldSuggestion_OnlyChangesTargetField(t *testing.T) { t.Parallel() fields := []domain.TemplateField{ {Path: "text.textTitle_m1710_1", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}, {Path: "text.buttonText_c1165_1", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA}, } current := domain.DraftSuggestionState{ ByFieldPath: map[string]domain.DraftSuggestion{ "text.textTitle_m1710_1": {FieldPath: "text.textTitle_m1710_1", Value: "Old Hero"}, "text.buttonText_c1165_1": {FieldPath: "text.buttonText_c1165_1", Value: "Old CTA"}, }, } updated := RegenerateFieldSuggestion(context.Background(), nil, SuggestionRequest{ Fields: fields, GlobalData: map[string]any{ "companyName": "Muster AG", }, }, current, "text.buttonText_c1165_1", time.Now().UTC()) if got := updated.ByFieldPath["text.textTitle_m1710_1"].Value; got != "Old Hero" { t.Fatalf("expected untargeted field unchanged, got %q", got) } if got := updated.ByFieldPath["text.buttonText_c1165_1"].Value; got == "" || got == "Old CTA" { t.Fatalf("expected target field regenerated, got %q", got) } } func TestGenerateAllSuggestions_UsesGeneratorFallbackWhenPrimaryPartial(t *testing.T) { t.Parallel() fields := []domain.TemplateField{ {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}, {Path: "text.buttonText_c1165_1", Section: "text", KeyName: "buttonText_c1165_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionCTA}, } generator := NewCompositeSuggestionGenerator( NewLLMSuggestionGenerator(&stubQCClient{ generateContent: qcclient.GenerateContentData{ "text": { "textTitle_m1710_1": "LLM Hero", }, }, }), NewRuleBasedSuggestionGenerator(), ) state := GenerateAllSuggestions(context.Background(), generator, SuggestionRequest{ TemplateID: 101, Fields: fields, GlobalData: map[string]any{"companyName": "Muster AG"}, Existing: map[string]string{}, }, domain.DraftSuggestionState{}, time.Now().UTC()) hero := state.ByFieldPath["text.textTitle_m1710_1"] if hero.Source != domain.DraftSuggestionSourceLLM { t.Fatalf("expected hero source llm, got %q", hero.Source) } cta := state.ByFieldPath["text.buttonText_c1165_1"] if cta.Source != domain.DraftSuggestionSourceFallbackRuleBased { t.Fatalf("expected cta source fallback rule-based, got %q", cta.Source) } } func TestGenerateAllSuggestions_FallsBackWhenLLMReturnsInvalidValueType(t *testing.T) { t.Parallel() fields := []domain.TemplateField{ {Path: "text.textTitle_m1710_1", Section: "text", KeyName: "textTitle_m1710_1", FieldKind: "text", IsEnabled: true, WebsiteSection: domain.WebsiteSectionHero}, } generator := NewCompositeSuggestionGenerator( NewLLMSuggestionGenerator(&stubQCClient{ generateContent: qcclient.GenerateContentData{ "text": { "textTitle_m1710_1": true, }, }, }), NewRuleBasedSuggestionGenerator(), ) state := GenerateAllSuggestions(context.Background(), generator, SuggestionRequest{ TemplateID: 202, Fields: fields, GlobalData: map[string]any{"companyName": "Muster AG"}, Existing: map[string]string{}, }, domain.DraftSuggestionState{}, time.Now().UTC()) hero := state.ByFieldPath["text.textTitle_m1710_1"] if hero.Value == "" { t.Fatalf("expected fallback suggestion value") } if hero.Source != domain.DraftSuggestionSourceFallbackRuleBased { t.Fatalf("expected fallback source, got %q", hero.Source) } } type stubQCClient struct { generateContent qcclient.GenerateContentData generateErr error } func (s *stubQCClient) Health(context.Context) error { return nil } func (s *stubQCClient) ListAITemplates(context.Context) ([]qcclient.Template, error) { return nil, nil } func (s *stubQCClient) GetTemplate(context.Context, int64) (*qcclient.Template, error) { return nil, nil } func (s *stubQCClient) GetTemplateSchema(context.Context, int64) (json.RawMessage, error) { return nil, nil } func (s *stubQCClient) GenerateContent(context.Context, qcclient.GenerateContentRequest) (qcclient.GenerateContentData, json.RawMessage, error) { if s.generateErr != nil { return nil, nil, s.generateErr } return s.generateContent, nil, nil } func (s *stubQCClient) CreateSite(context.Context, qcclient.CreateSiteRequest) (*qcclient.CreateSiteResponseData, json.RawMessage, error) { return nil, nil, nil } func (s *stubQCClient) GetJob(context.Context, int64) (*qcclient.JobStatusData, json.RawMessage, error) { return nil, nil, nil } func (s *stubQCClient) GetEditorURL(context.Context, int64) (*qcclient.SiteEditorLoginData, json.RawMessage, error) { return nil, nil, nil }