< Summary

Information
Class: Chronicis.Client.Services.WikiLinkAutocompleteService
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/WikiLinkAutocompleteService.cs
Line coverage
100%
Covered lines: 82
Uncovered lines: 0
Coverable lines: 82
Total lines: 260
Line coverage: 100%
Branch coverage
100%
Covered branches: 36
Total branches: 36
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Hide()100%22100%
SelectNext()100%44100%
SelectPrevious()100%44100%
SetSelectedIndex(...)100%66100%
GetSelectedSuggestion()100%66100%
TryParseExternalQuery(...)100%1212100%
NormalizeLookupKey(...)100%22100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/WikiLinkAutocompleteService.cs

#LineLine coverage
 1using Chronicis.Shared.DTOs;
 2
 3namespace Chronicis.Client.Services;
 4
 5public class WikiLinkAutocompleteService : IWikiLinkAutocompleteService
 6{
 7    private readonly ILinkApiService _linkApiService;
 8    private readonly IExternalLinkApiService _externalLinkApiService;
 9    private readonly IResourceProviderApiService _resourceProviderApiService;
 10    private readonly ILogger<WikiLinkAutocompleteService> _logger;
 1711    private readonly Dictionary<Guid, IReadOnlyList<WorldResourceProviderDto>> _worldProviderCache = new();
 12
 13    public event Action? OnShow;
 14    public event Action? OnHide;
 15    public event Action? OnSuggestionsUpdated;
 16
 17    public (double X, double Y) Position { get; private set; }
 18    public bool IsVisible { get; private set; }
 19    public string Query { get; private set; } = string.Empty;
 20    public bool IsExternalQuery { get; private set; }
 21    public string? ExternalSourceKey { get; private set; }
 22    public List<WikiLinkAutocompleteItem> Suggestions { get; private set; } = new();
 23    public int SelectedIndex { get; private set; }
 24    public bool IsLoading { get; private set; }
 25
 26    public WikiLinkAutocompleteService(
 27        ILinkApiService linkApiService,
 28        IExternalLinkApiService externalLinkApiService,
 29        IResourceProviderApiService resourceProviderApiService,
 30        ILogger<WikiLinkAutocompleteService> logger)
 31    {
 1732        _linkApiService = linkApiService;
 1733        _externalLinkApiService = externalLinkApiService;
 1734        _resourceProviderApiService = resourceProviderApiService;
 1735        _logger = logger;
 1736    }
 37
 38    public async Task ShowAsync(string query, double x, double y, Guid? worldId)
 39    {
 40        Position = (x, y);
 41        IsVisible = true;
 42        SelectedIndex = 0;
 43
 44        var worldProviders = await GetWorldProvidersAsync(worldId);
 45        IsExternalQuery = TryParseExternalQuery(query, worldProviders, out var sourceKey, out var remainder);
 46        ExternalSourceKey = IsExternalQuery ? sourceKey : null;
 47        Query = IsExternalQuery ? remainder : query;
 48
 49        OnShow?.Invoke();
 50
 51        // For external queries: no minimum length (show categories)
 52        // For internal queries: require 3 characters minimum
 53        var minLength = IsExternalQuery ? 0 : 3;
 54
 55        if (Query.Length < minLength)
 56        {
 57            Suggestions = new();
 58            OnSuggestionsUpdated?.Invoke();
 59            return;
 60        }
 61
 62        IsLoading = true;
 63        OnSuggestionsUpdated?.Invoke();
 64
 65        try
 66        {
 67            if (IsExternalQuery)
 68            {
 69                var externalSuggestions = await _externalLinkApiService.GetSuggestionsAsync(
 70                    worldId ?? Guid.Empty,
 71                    ExternalSourceKey ?? string.Empty,
 72                    Query,
 73                    CancellationToken.None);
 74
 75                Suggestions = externalSuggestions
 76                    .Select(WikiLinkAutocompleteItem.FromExternal)
 77                    .ToList();
 78            }
 79            else
 80            {
 81                var internalSuggestions = await _linkApiService.GetSuggestionsAsync(
 82                    worldId ?? Guid.Empty,
 83                    Query);
 84
 85                Suggestions = internalSuggestions
 86                    .Select(WikiLinkAutocompleteItem.FromInternal)
 87                    .ToList();
 88            }
 89        }
 90        catch (Exception ex)
 91        {
 92            _logger.LogError(ex, "Error getting autocomplete suggestions for query: {Query}", query);
 93            Suggestions = new();
 94        }
 95        finally
 96        {
 97            IsLoading = false;
 98            OnSuggestionsUpdated?.Invoke();
 99        }
 100    }
 101
 102    public void Hide()
 103    {
 2104        IsVisible = false;
 2105        Suggestions = new();
 2106        IsExternalQuery = false;
 2107        ExternalSourceKey = null;
 2108        Query = string.Empty;
 2109        SelectedIndex = 0;
 110
 2111        OnHide?.Invoke();
 1112    }
 113
 114    public void SelectNext()
 115    {
 4116        if (Suggestions.Any())
 117        {
 3118            SelectedIndex = (SelectedIndex + 1) % Suggestions.Count;
 3119            OnSuggestionsUpdated?.Invoke();
 120        }
 2121    }
 122
 123    public void SelectPrevious()
 124    {
 3125        if (Suggestions.Any())
 126        {
 2127            SelectedIndex = (SelectedIndex - 1 + Suggestions.Count) % Suggestions.Count;
 2128            OnSuggestionsUpdated?.Invoke();
 129        }
 2130    }
 131
 132    public void SetSelectedIndex(int index)
 133    {
 5134        if (index >= 0 && index < Suggestions.Count)
 135        {
 2136            SelectedIndex = index;
 2137            OnSuggestionsUpdated?.Invoke();
 138        }
 4139    }
 140
 141    public WikiLinkAutocompleteItem? GetSelectedSuggestion()
 142    {
 3143        if (Suggestions.Any() && SelectedIndex >= 0 && SelectedIndex < Suggestions.Count)
 144        {
 1145            return Suggestions[SelectedIndex];
 146        }
 2147        return null;
 148    }
 149
 150    private async Task<IReadOnlyList<WorldResourceProviderDto>> GetWorldProvidersAsync(Guid? worldId)
 151    {
 152        if (!worldId.HasValue || worldId.Value == Guid.Empty)
 153        {
 154            return Array.Empty<WorldResourceProviderDto>();
 155        }
 156
 157        if (_worldProviderCache.TryGetValue(worldId.Value, out var cached))
 158        {
 159            return cached;
 160        }
 161
 162        var providers = await _resourceProviderApiService.GetWorldProvidersAsync(worldId.Value)
 163            ?? new List<WorldResourceProviderDto>();
 164
 165        foreach (var provider in providers)
 166        {
 167            if (string.IsNullOrWhiteSpace(provider.LookupKey))
 168            {
 169                provider.LookupKey = provider.Provider.Code;
 170            }
 171        }
 172
 173        _worldProviderCache[worldId.Value] = providers;
 174        return providers;
 175    }
 176
 177    private static bool TryParseExternalQuery(
 178        string query,
 179        IReadOnlyList<WorldResourceProviderDto> worldProviders,
 180        out string sourceKey,
 181        out string remainder)
 182    {
 17183        sourceKey = string.Empty;
 17184        remainder = string.Empty;
 185
 17186        if (string.IsNullOrWhiteSpace(query))
 1187            return false;
 188
 16189        var slashIndex = query.IndexOf('/');
 16190        if (slashIndex < 0)
 191        {
 12192            var lowerQuery = query.ToLowerInvariant();
 193
 12194            var exactMatch = worldProviders
 12195                .Select(p => new
 12196                {
 12197                    ProviderCode = p.Provider.Code.ToLowerInvariant(),
 12198                    LookupKey = NormalizeLookupKey(p.LookupKey, p.Provider.Code),
 12199                })
 12200                .FirstOrDefault(p =>
 12201                    p.ProviderCode.Equals(lowerQuery, StringComparison.OrdinalIgnoreCase)
 12202                    || p.LookupKey.Equals(lowerQuery, StringComparison.OrdinalIgnoreCase));
 203
 12204            if (exactMatch != null)
 205            {
 2206                sourceKey = exactMatch.ProviderCode;
 2207                remainder = string.Empty;
 2208                return true;
 209            }
 210
 10211            var startsWithMatch = worldProviders
 10212                .Select(p => new
 10213                {
 10214                    ProviderCode = p.Provider.Code.ToLowerInvariant(),
 10215                    LookupKey = NormalizeLookupKey(p.LookupKey, p.Provider.Code),
 10216                })
 10217                .Where(p =>
 10218                    lowerQuery.StartsWith(p.ProviderCode, StringComparison.OrdinalIgnoreCase)
 10219                    || lowerQuery.StartsWith(p.LookupKey, StringComparison.OrdinalIgnoreCase))
 10220                .OrderByDescending(p => p.LookupKey.Length)
 10221                .ThenByDescending(p => p.ProviderCode.Length)
 10222                .FirstOrDefault();
 223
 10224            if (startsWithMatch != null)
 225            {
 1226                sourceKey = startsWithMatch.ProviderCode;
 1227                remainder = string.Empty;
 1228                return true;
 229            }
 230
 9231            return false;
 232        }
 233
 4234        var sourcePrefix = query.Substring(0, slashIndex).Trim().ToLowerInvariant();
 4235        remainder = query.Substring(slashIndex + 1);
 236
 4237        var providerMatch = worldProviders
 4238            .Select(p => new
 4239            {
 4240                ProviderCode = p.Provider.Code.ToLowerInvariant(),
 4241                LookupKey = NormalizeLookupKey(p.LookupKey, p.Provider.Code),
 4242            })
 4243            .FirstOrDefault(p =>
 4244                p.ProviderCode.Equals(sourcePrefix, StringComparison.OrdinalIgnoreCase)
 4245                || p.LookupKey.Equals(sourcePrefix, StringComparison.OrdinalIgnoreCase));
 246
 4247        sourceKey = providerMatch?.ProviderCode ?? sourcePrefix;
 4248        return true;
 249    }
 250
 251    private static string NormalizeLookupKey(string? lookupKey, string providerCode)
 252    {
 6253        if (string.IsNullOrWhiteSpace(lookupKey))
 254        {
 1255            return providerCode.ToLowerInvariant();
 256        }
 257
 5258        return lookupKey.Trim().ToLowerInvariant();
 259    }
 260}