using System;
using System.IO;
using System.Linq;
using System.Reflection;
using GmRelay.DiscordBot.Features.Sessions.Wizard;

namespace GmRelay.Bot.Tests.Discord;

/// <summary>
/// Source-level structural smoke tests for the Discord wizard
/// interaction module. The NetCord component-interaction service
/// uses dispatch-by-attribute (no public registry), so a runtime
/// instantiation test would need to spin up the full NetCord host —
/// overkill for a smoke gate. Instead we assert on the source shape
/// (custom-id formats, handler method signatures, dispatcher wiring)
/// so the existing wizard tests catch regressions in the platform-
/// neutral state machine while this file catches regressions in the
/// Discord adapter shell.
/// </summary>
public sealed class DiscordWizardInteractionModuleSourceTests
{
    private static string GetRepoRoot()
    {
        var dir = AppContext.BaseDirectory;
        while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Directory.Build.props")))
        {
            dir = Directory.GetParent(dir)?.FullName;
        }

        return dir ?? throw new InvalidOperationException("Could not find repo root");
    }

    private static string ReadSource(string relativePath)
    {
        var repoRoot = GetRepoRoot();
        var fullPath = Path.Combine(repoRoot, relativePath);
        Assert.True(File.Exists(fullPath), $"Source file {relativePath} should exist.");
        return File.ReadAllText(fullPath);
    }

    [Fact]
    public void Module_ShouldExist()
    {
        var path = "src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs";
        var source = ReadSource(path);
        Assert.Contains("public sealed class DiscordWizardButtonModule", source, StringComparison.Ordinal);
        Assert.Contains("public sealed class DiscordWizardStringMenuModule", source, StringComparison.Ordinal);
        Assert.Contains("public sealed class DiscordWizardModalModule", source, StringComparison.Ordinal);
        Assert.Contains("public sealed class WizardInteractionDispatcher", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Modules_ShouldBeDerivedFromComponentInteractionModule()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // The dispatching modules are thin shells that inherit from
        // NetCord's ComponentInteractionModule<TContext> for each
        // supported component type.
        Assert.Contains("ComponentInteractionModule<ButtonInteractionContext>", source, StringComparison.Ordinal);
        Assert.Contains("ComponentInteractionModule<StringMenuInteractionContext>", source, StringComparison.Ordinal);
        Assert.Contains("ComponentInteractionModule<ModalInteractionContext>", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Modules_ShouldRegisterWizardComponentInteraction()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // All three modules use the [ComponentInteraction("wizard")]
        // prefix registration; the args string carries the rest of
        // the custom-id (e.g. "btn:choice:Type:single" or
        // "select:Visibility" or "modal:Title").
        var count = CountOccurrences(source, "[ComponentInteraction(\"wizard\")]");
        Assert.Equal(3, count);
    }

    [Fact]
    public void Dispatcher_ShouldHandleButtonSelectAndModal()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        Assert.Contains("public async Task HandleButtonAsync", source, StringComparison.Ordinal);
        Assert.Contains("public async Task HandleStringMenuAsync", source, StringComparison.Ordinal);
        Assert.Contains("public async Task HandleModalAsync", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Dispatcher_ShouldParseAllWizardActionKinds()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // The button handler must dispatch on the five action kinds
        // the wizard's callback data format emits: choice, back,
        // cancel, create. The resume flow is a wizard-internal control
        // emitted by the slash command's "Continue / Start over" row.
        Assert.Contains("\"choice\"", source, StringComparison.Ordinal);
        Assert.Contains("\"back\"", source, StringComparison.Ordinal);
        Assert.Contains("\"cancel\"", source, StringComparison.Ordinal);
        Assert.Contains("\"create\"", source, StringComparison.Ordinal);
        Assert.Contains("\"resume\"", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Dispatcher_ShouldWireWizardStateMachine()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // The dispatcher must call the shared GameCreationWizard's
        // HandleInteractionAsync, which is the same entry point the
        // Telegram bot uses. This is the core invariant of the
        // platform-neutral refactor.
        Assert.Contains("GameCreationWizard", source, StringComparison.Ordinal);
        Assert.Contains("HandleInteractionAsync", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Dispatcher_ShouldInvokeSubmitterOnCreate()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // The "create" callback must delegate to DiscordWizardSubmitter
        // (the 3-retry finalize loop) rather than call the wizard's
        // render path.
        Assert.Contains("SubmitAsync", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Program_ShouldRegisterAllThreeComponentServices()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Program.cs");
        // NetCord requires AddComponentInteractions<TInteraction, TContext>
        // per supported interaction type. The wizard needs all three:
        // buttons, StringSelectMenus, and modal submits.
        Assert.Contains("AddComponentInteractions<ButtonInteraction, ButtonInteractionContext>", source, StringComparison.Ordinal);
        Assert.Contains("AddComponentInteractions<StringMenuInteraction, StringMenuInteractionContext>", source, StringComparison.Ordinal);
        Assert.Contains("AddComponentInteractions<ModalInteraction, ModalInteractionContext>", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Program_ShouldRegisterWizardModuleClasses()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Program.cs");
        // The wizard module classes have constructor dependencies
        // (the dispatcher + shared services) that DI must resolve.
        // AddComponentInteractions only registers the IComponentInteractionService,
        // not the module classes themselves.
        Assert.Contains("WizardInteractionDispatcher", source, StringComparison.Ordinal);
        Assert.Contains("DiscordWizardButtonModule", source, StringComparison.Ordinal);
        Assert.Contains("DiscordWizardStringMenuModule", source, StringComparison.Ordinal);
        Assert.Contains("DiscordWizardModalModule", source, StringComparison.Ordinal);
    }

    [Fact]
    public void Dispatcher_ShouldLookupDraftByOwner()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // All three handlers must look up the active draft by
        // (platform="Discord", ownerId=userId) — the wizard's
        // invariant is "one active draft per owner", not
        // "draft-id-in-custom-id".
        Assert.Contains("GetActiveAsync(\"Discord\"", source, StringComparison.Ordinal);
    }

    [Fact]
    public void ModalHandler_ShouldExtractTextFromLabel()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // The wizard's modals wrap a single TextInput in a Label. The
        // handler must walk Components[0] (Label) → .Component (TextInput)
        // → .Value to retrieve the user's text. If this drifts the
        // modal submit silently becomes a no-op.
        Assert.Contains("Components[0]", source, StringComparison.Ordinal);
        Assert.Contains("TextInput", source, StringComparison.Ordinal);
        Assert.Contains(".Value", source, StringComparison.Ordinal);
    }

    [Fact]
    public void StringMenuHandler_ShouldReadSelectedValues()
    {
        var source = ReadSource("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs");
        // StringSelectMenu interactions expose SelectedValues[0]
        // for our MaxValues=1 menus.
        Assert.Contains("SelectedValues[0]", source, StringComparison.Ordinal);
    }

    /// <summary>
    /// Roundtrip the renderer output through the dispatcher's parser to
    /// prove the wire formats agree. This is a real behavioural test
    /// (not a string-grep) — it actually constructs the ButtonProperties
    /// that NetCord would send, strips the [ComponentInteraction("wizard")]
    /// prefix exactly as NetCord does, and asserts the dispatcher's
    /// switch would route the click to the right branch. Catches the
    /// class of "renderer and dispatcher disagree on the wire format"
    /// regressions that the string-grep tests above cannot detect.
    /// </summary>
    [Fact]
    public void Renderer_And_Dispatcher_Agree_On_Wire_Format()
    {
        // Choice button: dispatcher expects `btn:choice:<step>:<value>`.
        var choice = DiscordWizardStep.ChoiceButtonCustomId("Type", "single");
        Assert.Equal("wizard:btn:choice:Type:single", choice);
        var choiceArgs = StripWizardPrefix(choice);
        var choiceParts = choiceArgs.Split(':', 4);
        Assert.Equal("btn", choiceParts[0]);
        Assert.Equal("choice", choiceParts[1]);
        Assert.Equal("Type", choiceParts[2]);
        Assert.Equal("single", choiceParts[3]);

        // Control button: dispatcher expects `btn:<action>:1`.
        var cancel = DiscordWizardStep.ControlButtonCustomId("cancel");
        Assert.Equal("wizard:btn:cancel:1", cancel);
        var cancelArgs = StripWizardPrefix(cancel);
        var cancelParts = cancelArgs.Split(':', 3);
        Assert.Equal("btn", cancelParts[0]);
        Assert.Equal("cancel", cancelParts[1]);

        // Modal trigger: dispatcher expects `btn:modal:<modalStep>`.
        var modal = DiscordWizardStep.ModalTriggerButtonCustomId("SystemFreeText");
        Assert.Equal("wizard:btn:modal:SystemFreeText", modal);
        var modalArgs = StripWizardPrefix(modal);
        var modalParts = modalArgs.Split(':', 3);
        Assert.Equal("btn", modalParts[0]);
        Assert.Equal("modal", modalParts[1]);
        Assert.Equal("SystemFreeText", modalParts[2]);

        // All customIds must fit Discord's 100-char limit.
        Assert.All(
            new[] { choice, cancel, modal },
            cid => Assert.True(
                cid.Length <= DiscordWizardStep.MaxCustomIdLength,
                $"CustomId '{cid}' exceeds 100 chars: {cid.Length}"));
    }

    /// <summary>
    /// The Create/Back/Cancel/Resume control buttons in the renderer
    /// (and in BuildResumeRow) must emit the format the dispatcher's
    /// switch matches directly — NOT the choice-button format. This
    /// test parses every button's customId and asserts the dispatcher
    /// would route it to the right branch.
    /// </summary>
    [Fact]
    public void ControlButtons_Are_Parsed_As_Control_Not_Choice()
    {
        // Real customIds the renderer / BuildResumeRow emit for control actions.
        var controlIds = new[]
        {
            DiscordWizardStep.ControlButtonCustomId("back"),
            DiscordWizardStep.ControlButtonCustomId("cancel"),
            "wizard:btn:create:1",
            "wizard:btn:resume:continue",
            "wizard:btn:resume:restart",
        };

        foreach (var cid in controlIds)
        {
            var parts = StripWizardPrefix(cid).Split(':', 3);
            Assert.Equal("btn", parts[0]);
            // The dispatcher's switch matches these as parts[1] == "back"|"cancel"|"create"|"resume".
            // They must NOT be tagged as "choice" (that would route through the wizard
            // with a nonsensical step name).
            Assert.NotEqual("choice", parts[1]);
        }
    }

    /// <summary>Mirror NetCord's [ComponentInteraction("wizard")] prefix strip.</summary>
    private static string StripWizardPrefix(string customId)
    {
        const string prefix = "wizard:";
        return customId.StartsWith(prefix, StringComparison.Ordinal) ? customId[prefix.Length..] : customId;
    }

    private static int CountOccurrences(string haystack, string needle)
    {
        if (string.IsNullOrEmpty(needle)) return 0;
        var count = 0;
        var idx = 0;
        while ((idx = haystack.IndexOf(needle, idx, StringComparison.Ordinal)) >= 0)
        {
            count++;
            idx += needle.Length;
        }
        return count;
    }
}
