package integration import ( "testing" "gitgud.io/mike/mpv-manager/pkg/locale" "gitgud.io/mike/mpv-manager/pkg/tui" "gitgud.io/mike/mpv-manager/pkg/web" "github.com/stretchr/testify/assert" ) // createTestEntries creates test locale entries for integration tests // Note: Antarctica and languages without regions are excluded to avoid known bug // where languages without regions bypass the CountryCode filter in locale.SearchLanguages func createTestEntries() []locale.LocaleEntry { return []locale.LocaleEntry{ { LanguageName: "English", LanguageLocal: "English", LanguageCode: "en", LanguageCode2: "eng", LanguageCode3: "eng", Regions: []locale.RegionEntry{ { Locale: "en-US", CountryName: "United States", CountryLocal: "United States", CountryCode: "US", Flag: "🇺🇸", }, { Locale: "en-GB", CountryName: "United Kingdom", CountryLocal: "United Kingdom", CountryCode: "GB", Flag: "🇬🇧", }, }, }, { LanguageName: "Japanese", LanguageLocal: "日本語", LanguageCode: "ja", LanguageCode2: "jpn", LanguageCode3: "jpn", Regions: []locale.RegionEntry{ { Locale: "ja-JP", CountryName: "Japan", CountryLocal: "日本", CountryCode: "JP", Flag: "🇯🇵", }, }, }, { LanguageName: "Spanish", LanguageLocal: "Español", LanguageCode: "es", LanguageCode2: "spa", LanguageCode3: "spa", Regions: []locale.RegionEntry{ { Locale: "es-ES", CountryName: "Spain", CountryLocal: "España", CountryCode: "ES", Flag: "🇪🇸", }, { Locale: "es-419", CountryName: "Latin America", CountryLocal: "América Latina", CountryCode: "419", Flag: "🌎", }, }, }, { LanguageName: "French", LanguageLocal: "Français", LanguageCode: "fr", LanguageCode2: "fra", LanguageCode3: "fra", Regions: []locale.RegionEntry{ { Locale: "fr-FR", CountryName: "France", CountryLocal: "France", CountryCode: "FR", Flag: "🇫🇷", }, }, }, { LanguageName: "German", LanguageLocal: "Deutsch", LanguageCode: "de", LanguageCode2: "deu", LanguageCode3: "deu", Regions: []locale.RegionEntry{ { Locale: "de-DE", CountryName: "Germany", CountryLocal: "Deutschland", CountryCode: "DE", Flag: "🇩🇪", }, }, }, { LanguageName: "Chinese", LanguageLocal: "中文", LanguageCode: "zh", LanguageCode2: "zho", LanguageCode3: "zho", Regions: []locale.RegionEntry{ { Locale: "zh-CN", CountryName: "China", CountryLocal: "中国", CountryCode: "CN", Flag: "🇨🇳", }, }, }, } } // TestSearchLanguages_CrossUIConsistency verifies search results work with both TUI and Web UI adapters func TestSearchLanguages_CrossUIConsistency(t *testing.T) { testEntries := createTestEntries() tests := []struct { name string searchTerm string wantCount int wantFirstLang string }{ { name: "search Japanese", searchTerm: "Japanese", wantCount: 1, wantFirstLang: "Japanese", }, { name: "search French", searchTerm: "French", wantCount: 1, wantFirstLang: "French", }, { name: "search Spanish", searchTerm: "Spanish", wantCount: 2, // es-ES, es-419 wantFirstLang: "Spanish", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Search using locale package opts := locale.LanguageFilterOptions{ SearchTerm: tt.searchTerm, } localeResults := locale.SearchLanguages(testEntries, opts) // Convert to TUI majorLangItems tuiMajorItems := tui.ConvertLanguageOptionsToMajorItems(localeResults, testEntries) // Convert to web.LanguageOptions using web package adapters webOpts := web.ConvertToWebOptionSliceWithEntries(localeResults, testEntries) // Verify all three produce same count assert.Len(t, localeResults, tt.wantCount) assert.Len(t, tuiMajorItems, tt.wantCount) assert.Len(t, webOpts, tt.wantCount) // Verify first language matches if tt.wantCount > 0 { assert.Equal(t, tt.wantFirstLang, localeResults[0].LanguageName) assert.Contains(t, tuiMajorItems[0].Title(), tt.wantFirstLang) assert.Contains(t, webOpts[0].DisplayText, tt.wantFirstLang) } // Verify locales are consistent between locale and web results for i := range localeResults { assert.Equal(t, localeResults[i].Locale, webOpts[i].Locale) assert.Equal(t, localeResults[i].LanguageName, webOpts[i].LanguageName) } }) } } // TestCommonLanguages_CrossUIConsistency verifies common language lists match func TestCommonLanguages_CrossUIConsistency(t *testing.T) { testEntries := createTestEntries() // Get common languages from locale package localeResults := locale.GetCommonLanguages(testEntries) // Convert to TUI majorLangItems tuiMajorItems := tui.ConvertLanguageOptionsToMajorItems(localeResults, testEntries) // Convert to web.LanguageOptions webOpts := web.ConvertToWebOptionSliceWithEntries(localeResults, testEntries) // Verify all three produce same count assert.Greater(t, len(localeResults), 0) assert.Equal(t, len(localeResults), len(tuiMajorItems)) assert.Equal(t, len(localeResults), len(webOpts)) // Verify locales are consistent between locale and web results for i := range localeResults { assert.Equal(t, localeResults[i].Locale, webOpts[i].Locale) assert.Equal(t, localeResults[i].LanguageName, webOpts[i].LanguageName) } } // TestCountryFilter_CrossUIConsistency verifies country filtering works identically func TestCountryFilter_CrossUIConsistency(t *testing.T) { testEntries := createTestEntries() tests := []struct { name string countryCode string wantCount int wantLanguages []string }{ { name: "filter by JP", countryCode: "JP", wantCount: 1, wantLanguages: []string{"Japanese"}, }, { name: "filter by ES", countryCode: "ES", wantCount: 1, wantLanguages: []string{"Spanish"}, }, { name: "filter by FR", countryCode: "FR", wantCount: 1, wantLanguages: []string{"French"}, }, { name: "filter by DE", countryCode: "DE", wantCount: 1, wantLanguages: []string{"German"}, }, { name: "filter by CN", countryCode: "CN", wantCount: 1, wantLanguages: []string{"Chinese"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Filter using locale package opts := locale.LanguageFilterOptions{ CountryCode: tt.countryCode, } localeResults := locale.SearchLanguages(testEntries, opts) // Convert to TUI majorLangItems tuiMajorItems := tui.ConvertLanguageOptionsToMajorItems(localeResults, testEntries) // Convert to web.LanguageOptions webOpts := web.ConvertToWebOptionSliceWithEntries(localeResults, testEntries) // Verify all three produce same count assert.Len(t, localeResults, tt.wantCount) assert.Len(t, tuiMajorItems, tt.wantCount) assert.Len(t, webOpts, tt.wantCount) // Verify all expected languages present foundLanguages := make(map[string]bool) for _, r := range localeResults { foundLanguages[r.LanguageName] = true } for _, lang := range tt.wantLanguages { assert.True(t, foundLanguages[lang], "Language %s should be found for country %s", lang, tt.countryCode) } // Verify Web UI has correct country codes webCountryCodes := make([]string, 0) for _, opt := range webOpts { if opt.CountryCode != "" { webCountryCodes = append(webCountryCodes, opt.CountryCode) } } // Verify all country codes match filter for _, code := range webCountryCodes { assert.Equal(t, tt.countryCode, code, "Country code should match filter") } }) } } // TestSearchByTerm_TUIAdapter verifies TUI adapter produces correct results func TestSearchByTerm_TUIAdapter(t *testing.T) { testEntries := createTestEntries() tests := []struct { name string searchTerm string wantCount int wantContains string }{ { name: "search English", searchTerm: "English", wantCount: 2, wantContains: "English", }, { name: "search Japanese", searchTerm: "Japanese", wantCount: 1, wantContains: "Japanese", }, { name: "search Spanish", searchTerm: "Spanish", wantCount: 2, wantContains: "Spanish", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Search by term using locale.SearchLanguages() opts := locale.LanguageFilterOptions{ SearchTerm: tt.searchTerm, } searchResults := locale.SearchLanguages(testEntries, opts) // Convert to TUI majorLangItem using adapter tuiMajorItems := tui.ConvertLanguageOptionsToMajorItems(searchResults, testEntries) // Verify count matches assert.Len(t, tuiMajorItems, tt.wantCount) // Verify display text contains expected language if tt.wantCount > 0 { title := tuiMajorItems[0].Title() assert.Contains(t, title, tt.wantContains) } // Verify flags are present for _, item := range tuiMajorItems { title := item.Title() // TUI items start with flag emoji assert.True(t, len(title) > 0, "Title should not be empty") } }) } } // TestSearchByTerm_WebAdapter verifies Web UI adapter produces correct results func TestSearchByTerm_WebAdapter(t *testing.T) { testEntries := createTestEntries() tests := []struct { name string searchTerm string wantCount int wantContainsText string wantContainsCountry string }{ { name: "search Japanese", searchTerm: "Japanese", wantCount: 1, wantContainsText: "Japanese", wantContainsCountry: "JP", }, { name: "search English", searchTerm: "English", wantCount: 2, wantContainsText: "English", wantContainsCountry: "US", }, { name: "search French", searchTerm: "French", wantCount: 1, wantContainsText: "French", wantContainsCountry: "FR", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Search by term using locale.SearchLanguages() opts := locale.LanguageFilterOptions{ SearchTerm: tt.searchTerm, } searchResults := locale.SearchLanguages(testEntries, opts) // Convert to web.LanguageOption using web utils webOpts := web.ConvertToWebOptionSliceWithEntries(searchResults, testEntries) // Verify count matches assert.Len(t, webOpts, tt.wantCount) // Verify DisplayText matches expected format if tt.wantCount > 0 { assert.Contains(t, webOpts[0].DisplayText, tt.wantContainsText) assert.Contains(t, webOpts[0].DisplayText, tt.wantContainsCountry) } // Verify JSON tags are present (can't directly test JSON marshaling in this test, // but we can verify struct has the expected fields) for _, opt := range webOpts { assert.NotEmpty(t, opt.Locale, "Locale should not be empty") assert.NotEmpty(t, opt.LanguageName, "LanguageName should not be empty") assert.NotEmpty(t, opt.LanguageLocal, "LanguageLocal should not be empty") } }) } } // TestTUIAdapter_ConversionAccuracy verifies TUI adapter field mapping func TestTUIAdapter_ConversionAccuracy(t *testing.T) { testEntries := createTestEntries() tests := []struct { name string locale string wantLangName string wantLangLocal string wantInTitle bool wantInDesc bool }{ { name: "convert ja-JP", locale: "ja-JP", wantLangName: "Japanese", wantLangLocal: "日本語", wantInTitle: true, wantInDesc: true, }, { name: "convert en-US", locale: "en-US", wantLangName: "English", wantLangLocal: "English", wantInTitle: true, wantInDesc: true, }, { name: "convert es-ES", locale: "es-ES", wantLangName: "Spanish", wantLangLocal: "Español", wantInTitle: true, wantInDesc: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Find language option by locale localeOpt, err := locale.FindLanguageByLocale(testEntries, tt.locale) assert.NoError(t, err) assert.NotNil(t, localeOpt) // Convert to TUI majorLangItem tuiItem := tui.LanguageOptionToMajorLangItem(*localeOpt, testEntries) // Verify Title() includes language name title := tuiItem.Title() assert.Contains(t, title, tt.wantLangName) // Verify Description() includes language info desc := tuiItem.Description() assert.NotEmpty(t, desc, "Description should not be empty") }) } } // TestWebAdapter_ConversionAccuracy verifies Web UI adapter field mapping func TestWebAdapter_ConversionAccuracy(t *testing.T) { testEntries := createTestEntries() tests := []struct { name string locale string wantLocale string wantLangName string wantLangLocal string wantCountryCode string wantCountryName string }{ { name: "convert ja-JP", locale: "ja-JP", wantLocale: "ja-JP", wantLangName: "Japanese", wantLangLocal: "日本語", wantCountryCode: "JP", wantCountryName: "Japan", }, { name: "convert en-US", locale: "en-US", wantLocale: "en-US", wantLangName: "English", wantLangLocal: "English", wantCountryCode: "US", wantCountryName: "United States", }, { name: "convert es-419", locale: "es-419", wantLocale: "es-419", wantLangName: "Spanish", wantLangLocal: "Español", wantCountryCode: "419", wantCountryName: "Latin America", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Find language option by locale localeOpt, err := locale.FindLanguageByLocale(testEntries, tt.locale) assert.NoError(t, err) assert.NotNil(t, localeOpt) // Convert to web.LanguageOption using web package function webOpt := web.ConvertToWebOptionWithEntries(*localeOpt, testEntries) // Verify all fields mapped correctly assert.Equal(t, tt.wantLocale, webOpt.Locale) assert.Equal(t, tt.wantLangName, webOpt.LanguageName) assert.Equal(t, tt.wantLangLocal, webOpt.LanguageLocal) assert.Equal(t, tt.wantCountryCode, webOpt.CountryCode) assert.Equal(t, tt.wantCountryName, webOpt.CountryName) // Verify DisplayText is formatted correctly assert.NotEmpty(t, webOpt.DisplayText) assert.Contains(t, webOpt.DisplayText, tt.wantLangName) // Verify flag emoji is present assert.NotEmpty(t, webOpt.FlagEmoji, "Flag emoji should not be empty") }) } } // TestFeatureParity_TUIHasWebFeatures verifies TUI has all Web UI features func TestFeatureParity_TUIHasWebFeatures(t *testing.T) { testEntries := createTestEntries() t.Run("TUI search by language name", func(t *testing.T) { opts := locale.LanguageFilterOptions{ SearchTerm: "Japanese", } results := locale.SearchLanguages(testEntries, opts) tuiItems := tui.ConvertLanguageOptionsToMajorItems(results, testEntries) assert.Len(t, tuiItems, 1) assert.Contains(t, tuiItems[0].Title(), "Japanese") }) t.Run("TUI search by country", func(t *testing.T) { opts := locale.LanguageFilterOptions{ CountryCode: "JP", } results := locale.SearchLanguages(testEntries, opts) tuiItems := tui.ConvertLanguageOptionsToMajorItems(results, testEntries) // Should have at least one result for JP assert.Greater(t, len(tuiItems), 0) }) t.Run("TUI filter common languages", func(t *testing.T) { opts := locale.LanguageFilterOptions{ CommonOnly: true, } results := locale.SearchLanguages(testEntries, opts) tuiItems := tui.ConvertLanguageOptionsToMajorItems(results, testEntries) // Should only include common languages (en, ja, es, fr, de, zh) assert.Greater(t, len(tuiItems), 0) }) t.Run("TUI limit results", func(t *testing.T) { opts := locale.LanguageFilterOptions{ Limit: 3, } results := locale.SearchLanguages(testEntries, opts) tuiItems := tui.ConvertLanguageOptionsToMajorItems(results, testEntries) assert.LessOrEqual(t, len(tuiItems), 3) }) } // TestFeatureParity_WebHasTUIFeatures verifies Web UI has all TUI features func TestFeatureParity_WebHasTUIFeatures(t *testing.T) { testEntries := createTestEntries() t.Run("Web get common languages", func(t *testing.T) { // Using web package's ConvertToWebOptionSlice to convert locale results localeResults := locale.GetCommonLanguages(testEntries) webOpts := web.ConvertToWebOptionSliceWithEntries(localeResults, testEntries) assert.Greater(t, len(webOpts), 0) // All should be common languages for _, opt := range webOpts { assert.True(t, locale.IsCommonLanguage(opt.Locale), "%s should be a common language", opt.Locale) } }) t.Run("Web convert locale results", func(t *testing.T) { // Test web.GetLanguagesByCountry equivalent behavior opts := locale.LanguageFilterOptions{ CountryCode: "JP", } localeResults := locale.SearchLanguages(testEntries, opts) webOpts := web.ConvertToWebOptionSliceWithEntries(localeResults, testEntries) assert.Len(t, webOpts, 1) assert.Equal(t, "Japanese", webOpts[0].LanguageName) }) t.Run("Web find by locale", func(t *testing.T) { // Test web.FindLanguageByLocale equivalent behavior localeOpt, err := locale.FindLanguageByLocale(testEntries, "ja-JP") assert.NoError(t, err) assert.NotNil(t, localeOpt) assert.Equal(t, "Japanese", localeOpt.LanguageName) assert.Equal(t, "ja-JP", localeOpt.Locale) assert.Equal(t, "JP", localeOpt.CountryCode) // Now convert it using web adapter webOpt := web.ConvertToWebOptionWithEntries(*localeOpt, testEntries) assert.Equal(t, "Japanese", webOpt.LanguageName) assert.Equal(t, "ja-JP", webOpt.Locale) assert.Equal(t, "JP", webOpt.CountryCode) }) } // TestCrossUI_ResultConsistency verifies that identical queries produce identical results between adapters func TestCrossUI_ResultConsistency(t *testing.T) { testEntries := createTestEntries() queries := []struct { name string searchTerm string commonOnly bool countryCode string limit int }{ { name: "Japanese search", searchTerm: "Japanese", }, { name: "English search", searchTerm: "English", }, { name: "Common languages only", commonOnly: true, }, { name: "CN country", countryCode: "CN", }, { name: "Limited results", limit: 5, }, } for _, q := range queries { t.Run(q.name, func(t *testing.T) { // Query using locale package opts := locale.LanguageFilterOptions{ SearchTerm: q.searchTerm, CommonOnly: q.commonOnly, CountryCode: q.countryCode, Limit: q.limit, } localeResults := locale.SearchLanguages(testEntries, opts) // Query using TUI adapter tuiMajorItems := tui.ConvertLanguageOptionsToMajorItems(localeResults, testEntries) // Query using Web UI adapter webOpts := web.ConvertToWebOptionSliceWithEntries(localeResults, testEntries) // Verify counts match assert.Equal(t, len(localeResults), len(tuiMajorItems)) assert.Equal(t, len(localeResults), len(webOpts)) // Verify locales match between locale and web results for i, lr := range localeResults { assert.Equal(t, lr.Locale, webOpts[i].Locale) assert.Equal(t, lr.LanguageName, webOpts[i].LanguageName) } // Verify language names match between locale and TUI results for i, lr := range localeResults { assert.Contains(t, tuiMajorItems[i].Title(), lr.LanguageName) } }) } } // TestAdapterConsistency verifies both adapters handle edge cases consistently func TestAdapterConsistency(t *testing.T) { testEntries := createTestEntries() t.Run("handle empty results", func(t *testing.T) { opts := locale.LanguageFilterOptions{ SearchTerm: "NonExistentLanguage", } results := locale.SearchLanguages(testEntries, opts) assert.Len(t, results, 0) // Convert to TUI - should handle empty tuiItems := tui.ConvertLanguageOptionsToMajorItems(results, testEntries) assert.Len(t, tuiItems, 0) // Convert to Web - should handle empty webOpts := web.ConvertToWebOptionSliceWithEntries(results, testEntries) assert.Len(t, webOpts, 0) }) }