package services

// Pure-function tests for DashboardLayoutSpec + WidgetCatalog.
// No DB; safe to run in any environment.

import (
	"encoding/json"
	"errors"
	"strings"
	"testing"
)

func TestFactoryDefaultLayout_AllKnownWidgetsPresent(t *testing.T) {
	def := FactoryDefaultLayout()
	if def.Version != LayoutSpecVersion {
		t.Errorf("FactoryDefaultLayout version=%d; want %d", def.Version, LayoutSpecVersion)
	}
	if len(def.Widgets) != len(KnownWidgetKeys) {
		t.Fatalf("FactoryDefaultLayout has %d widgets; want %d", len(def.Widgets), len(KnownWidgetKeys))
	}
	for i, k := range KnownWidgetKeys {
		if def.Widgets[i].Key != k {
			t.Errorf("widgets[%d].Key = %q; want %q", i, def.Widgets[i].Key, k)
		}
		if !def.Widgets[i].Visible {
			t.Errorf("widgets[%d].Visible = false; factory default should be all-visible", i)
		}
	}
}

func TestFactoryDefaultLayout_SettingsDefaultsPresent(t *testing.T) {
	def := FactoryDefaultLayout()
	for _, w := range def.Widgets {
		catalogDef, ok := LookupWidgetDef(w.Key)
		if !ok {
			t.Errorf("factory widget %q is not in catalog", w.Key)
			continue
		}
		hasDefaults := catalogDef.DefaultCount != nil || catalogDef.DefaultHorizon != nil
		if hasDefaults && len(w.Settings) == 0 {
			t.Errorf("widget %q has catalog defaults but factory layout has empty settings", w.Key)
		}
		if !hasDefaults && len(w.Settings) > 0 {
			t.Errorf("widget %q has no catalog defaults but factory layout has settings %s", w.Key, string(w.Settings))
		}
	}
}

func TestFactoryDefaultLayout_PassesValidation(t *testing.T) {
	def := FactoryDefaultLayout()
	if err := def.Validate(); err != nil {
		t.Fatalf("factory default failed Validate(): %v", err)
	}
}

func TestDashboardLayoutSpec_Validate_WrongVersion(t *testing.T) {
	s := DashboardLayoutSpec{Version: 99, Widgets: []DashboardWidgetRef{{Key: WidgetDeadlineSummary, Visible: true}}}
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("Validate returned %v; want ErrInvalidInput", err)
	}
	if !strings.Contains(err.Error(), "version") {
		t.Errorf("error %q should mention 'version'", err.Error())
	}
}

func TestDashboardLayoutSpec_Validate_TooManyWidgets(t *testing.T) {
	widgets := make([]DashboardWidgetRef, LayoutWidgetCap+1)
	for i := range widgets {
		widgets[i] = DashboardWidgetRef{Key: WidgetDeadlineSummary, Visible: true}
	}
	s := DashboardLayoutSpec{Version: 1, Widgets: widgets}
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("Validate returned %v; want ErrInvalidInput", err)
	}
}

func TestDashboardLayoutSpec_Validate_UnknownKey(t *testing.T) {
	s := DashboardLayoutSpec{Version: 1, Widgets: []DashboardWidgetRef{
		{Key: "not-a-real-widget", Visible: true},
	}}
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("Validate returned %v; want ErrInvalidInput", err)
	}
}

func TestDashboardLayoutSpec_Validate_DuplicateKey(t *testing.T) {
	s := DashboardLayoutSpec{Version: 1, Widgets: []DashboardWidgetRef{
		{Key: WidgetDeadlineSummary, Visible: true},
		{Key: WidgetDeadlineSummary, Visible: false},
	}}
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("Validate returned %v; want ErrInvalidInput", err)
	}
	if !strings.Contains(err.Error(), "duplicate") {
		t.Errorf("error %q should mention 'duplicate'", err.Error())
	}
}

func TestDashboardLayoutSpec_Validate_BadSettings(t *testing.T) {
	// count not in CountOptions for upcoming-deadlines (legal: 1,3,5,10,20)
	s := DashboardLayoutSpec{Version: 1, Widgets: []DashboardWidgetRef{
		{Key: WidgetUpcomingDeadlines, Visible: true, Settings: json.RawMessage(`{"count": 7}`)},
	}}
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("Validate returned %v; want ErrInvalidInput", err)
	}
}

func TestDashboardLayoutSpec_Validate_AcceptsValidSettings(t *testing.T) {
	s := DashboardLayoutSpec{Version: 1, Widgets: []DashboardWidgetRef{
		{Key: WidgetUpcomingDeadlines, Visible: true, Settings: json.RawMessage(`{"count": 5, "horizon_days": 14}`)},
		{Key: WidgetInlineAgenda, Visible: true, Settings: json.RawMessage(`{"horizon_days": 60}`)},
		{Key: WidgetRecentActivity, Visible: false},
	}}
	if err := s.Validate(); err != nil {
		t.Fatalf("Validate returned %v; want nil", err)
	}
}

func TestDashboardLayoutSpec_Validate_SettingsOnNoSettingsWidget(t *testing.T) {
	// deadline-summary has no Settings schema.
	s := DashboardLayoutSpec{Version: 1, Widgets: []DashboardWidgetRef{
		{Key: WidgetDeadlineSummary, Visible: true, Settings: json.RawMessage(`{"count": 5}`)},
	}}
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("Validate returned %v; want ErrInvalidInput", err)
	}
}

func TestDashboardLayoutSpec_SanitizeForRead_DropsUnknownKeys(t *testing.T) {
	s := DashboardLayoutSpec{Version: 1, Widgets: []DashboardWidgetRef{
		{Key: WidgetDeadlineSummary, Visible: true},
		{Key: "deprecated-widget", Visible: true},
		{Key: WidgetInlineAgenda, Visible: true},
	}}
	changed := s.SanitizeForRead()
	if !changed {
		t.Errorf("SanitizeForRead returned false; expected true (one entry dropped)")
	}
	if len(s.Widgets) != 2 {
		t.Errorf("after sanitize: %d widgets; want 2", len(s.Widgets))
	}
	if s.Widgets[0].Key != WidgetDeadlineSummary || s.Widgets[1].Key != WidgetInlineAgenda {
		t.Errorf("after sanitize: keys = %v %v; want %v %v",
			s.Widgets[0].Key, s.Widgets[1].Key, WidgetDeadlineSummary, WidgetInlineAgenda)
	}
}

func TestDashboardLayoutSpec_SanitizeForRead_NoopOnClean(t *testing.T) {
	s := FactoryDefaultLayout()
	if s.SanitizeForRead() {
		t.Errorf("SanitizeForRead on factory default returned true; want false (already clean)")
	}
}

func TestDashboardLayoutSpec_SanitizeForRead_BumpsVersion(t *testing.T) {
	s := DashboardLayoutSpec{Version: 0, Widgets: []DashboardWidgetRef{{Key: WidgetDeadlineSummary, Visible: true}}}
	if !s.SanitizeForRead() {
		t.Errorf("SanitizeForRead returned false; expected version bump")
	}
	if s.Version != LayoutSpecVersion {
		t.Errorf("after sanitize: Version=%d; want %d", s.Version, LayoutSpecVersion)
	}
}

func TestParseDashboardLayoutSpec_RoundTrip(t *testing.T) {
	def := FactoryDefaultLayout()
	bytes, err := json.Marshal(def)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	parsed, err := ParseDashboardLayoutSpec(bytes)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if parsed.Version != def.Version {
		t.Errorf("version mismatch: %d vs %d", parsed.Version, def.Version)
	}
	if len(parsed.Widgets) != len(def.Widgets) {
		t.Errorf("widget count mismatch: %d vs %d", len(parsed.Widgets), len(def.Widgets))
	}
}

func TestParseDashboardLayoutSpec_InvalidJSON(t *testing.T) {
	_, err := ParseDashboardLayoutSpec([]byte(`{not-json}`))
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("ParseDashboardLayoutSpec returned %v; want ErrInvalidInput", err)
	}
}

func TestWidgetCatalog_AllKnownKeysHaveDef(t *testing.T) {
	for _, k := range KnownWidgetKeys {
		def, ok := LookupWidgetDef(k)
		if !ok {
			t.Errorf("KnownWidgetKeys entry %q has no WidgetDef", k)
			continue
		}
		if def.TitleDE == "" || def.TitleEN == "" {
			t.Errorf("widget %q missing title (de=%q en=%q)", k, def.TitleDE, def.TitleEN)
		}
		if def.DescriptionDE == "" || def.DescriptionEN == "" {
			t.Errorf("widget %q missing description", k)
		}
	}
}

func TestWidgetCatalog_NoOrphanDefs(t *testing.T) {
	known := make(map[WidgetKey]bool, len(KnownWidgetKeys))
	for _, k := range KnownWidgetKeys {
		known[k] = true
	}
	for _, def := range WidgetCatalog() {
		if !known[def.Key] {
			// Orphans are allowed (forward-compat: pinned-projects const
			// exists in widget_catalog.go before its widget module ships).
			// But verify the catalog entry is internally coherent.
			if def.TitleDE == "" || def.TitleEN == "" {
				t.Errorf("orphan catalog entry %q must still have titles", def.Key)
			}
		}
	}
}

func TestWidgetSettingsSchema_NilRejectsNonEmpty(t *testing.T) {
	var sch *WidgetSettingsSchema
	if err := sch.Validate(json.RawMessage(`{"count": 5}`)); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("nil schema accepted settings; got %v", err)
	}
	if err := sch.Validate(nil); err != nil {
		t.Errorf("nil schema rejected empty settings: %v", err)
	}
	if err := sch.Validate(json.RawMessage(`null`)); err != nil {
		t.Errorf("nil schema rejected 'null' settings: %v", err)
	}
}
