using FluentAssertions;
using DragonISO.App.ViewModels;

namespace DragonISO.App.Tests.ViewModels;

// Unit tests for the CommandPaletteViewModel.Matches predicate â€” the
// case-insensitive Contains check across Label / Category / Keywords
// that powers the v2 Ctrl+K filter.
//
// We don't build a full CommandPaletteViewModel here (that requires a
// MainViewModel + IIsoController fake â€” out of scope). Matches is the
// behaviorally-relevant unit; pinning it across a representative
// query set guards against accidental regressions when someone adds a
// scoring algorithm or swaps Contains for StartsWith.
public sealed class CommandPaletteMatchesTests
{
    private static PaletteCommand Cmd(string category, string label, string? keywords = null) =>
        new(category, label, keywords, Shortcut: null, Invoke: () => { });

    [Theory]
    // Label substrings â€” the dominant match path
    [InlineData("Quick", "Stop all ISOs", null, "stop", true)]
    [InlineData("Quick", "Stop all ISOs", null, "STOP", true)]  // case-insensitive
    [InlineData("Quick", "Stop all ISOs", null, "all", true)]
    [InlineData("Quick", "Stop all ISOs", null, "ISO", true)]
    // Category match â€” operator types the section name
    [InlineData("Teams", "Mute / unmute", null, "teams", true)]
    [InlineData("App",   "Help",          null, "app", true)]
    // Keywords match â€” synonym path. The Network/topology command has
    // "ndi groups isolate" in its keywords blob.
    [InlineData("Network", "Apply transcoder topology", "ndi groups isolate", "ndi", true)]
    [InlineData("Network", "Apply transcoder topology", "ndi groups isolate", "isolate", true)]
    // No-match â€” none of label/category/keywords contain the query
    [InlineData("Quick", "Stop all ISOs", null, "espresso", false)]
    [InlineData("Teams", "Mute / unmute", "microphone audio toggle", "monitor", false)]
    public void Matches_Predicate(string category, string label, string? keywords, string query, bool expected)
    {
        CommandPaletteViewModel.Matches(Cmd(category, label, keywords), query)
            .Should().Be(expected);
    }

    [Fact]
    public void Matches_OperatorTypingShortToken_HitsExpectedCategorySpread()
    {
        // "mute" should match the Teams command but not the App theme
        // commands â€” pins the cross-category selectivity that makes
        // the palette useful at all. If a future change makes Matches
        // too permissive (e.g. by indexing the Invoke delegate's
        // method name), the second assertion catches it.
        var muteCmd  = Cmd("Teams", "Mute / unmute", keywords: "microphone audio silence toggle");
        var themeCmd = Cmd("App",   "Theme: dark",   keywords: "appearance night mode");

        CommandPaletteViewModel.Matches(muteCmd,  "mute").Should().BeTrue();
        CommandPaletteViewModel.Matches(themeCmd, "mute").Should().BeFalse();
    }

    [Fact]
    public void Matches_AcrossTheFullPaletteVocabulary_StaysDeterministic()
    {
        // Sanity check: a representative slice of the palette's real
        // commands gives stable matches for the most common operator
        // queries. Pin the count of hits for each query so a careless
        // refactor that flips the predicate's polarity blows up here
        // instead of in production.
        var commands = new[]
        {
            Cmd("Quick",   "Enable all online",          "ISOs enable everyone start everything live"),
            Cmd("Quick",   "Stop all ISOs",              "panic stop everything kill disable"),
            Cmd("Quick",   "Refresh discovery",          "rediscover sources rebuild finder NDI"),
            Cmd("Teams",   "Mute / unmute",              "microphone audio silence toggle"),
            Cmd("Teams",   "Toggle camera",              "video webcam on off"),
            Cmd("Teams",   "Leave call",                 "exit end disconnect quit"),
            Cmd("Network", "Apply transcoder topology",  "ndi groups isolate Dragon-ISO-input private"),
            Cmd("App",     "Theme: dark",                "appearance night mode"),
            Cmd("App",     "Theme: light",               "appearance day mode bright"),
            Cmd("App",     "Theme: follow Windows",      "system auto"),
            Cmd("App",     "Help",                       "shortcuts cheatsheet f1"),
        };

        int Hits(string q) => commands.Count(c => CommandPaletteViewModel.Matches(c, q));

        Hits("theme").Should().Be(3, "three App theme commands carry 'Theme' in the label");
        Hits("stop").Should().Be(1);
        Hits("ndi").Should().Be(2, "Refresh discovery (NDI in keywords) + Apply transcoder topology");
        // "App" matches case-insensitively against the four App-category
        // commands AND substring-matches inside "Apply transcoder topology" â€”
        // a real operator typing "app" would see five rows, which is
        // exactly what Contains delivers. Pinning this so a future move
        // to a stricter (StartsWith / token-boundary) algorithm has to
        // re-decide that affordance deliberately.
        Hits("App").Should().Be(5, "four App-category commands + 'Apply' transcoder topology");
        Hits("xyzzy").Should().Be(0);
    }
}
