using System;
using System.IO;
using Xunit;

namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;

/// <summary>
/// Regression guard: WizardDraftRepository must not use the (CommandDefinition)
/// overload of Dapper. Dapper.AOT 1.0.48 only generates interceptors for the
/// (sql, object?) extension overloads; using CommandDefinition falls back to
/// Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit and
/// throws PlatformNotSupportedException on NativeAOT (v3.9.0 wizard regression).
/// The v3.9.1 hotfix switched all four methods to the direct overload.
/// </summary>
public sealed class WizardDraftRepositoryAotShapeTests
{
    [Fact]
    public void WizardDraftRepository_GetActiveAsync_ShouldNotUseCommandDefinition()
    {
        var repoRoot = FindRepositoryRoot();
        var source = File.ReadAllText(Path.Combine(
            repoRoot,
            "src",
            "GmRelay.Shared",
            "Features",
            "Sessions",
            "CreateSession",
            "Wizard",
            "WizardDraftRepository.cs"));

        var getActive = ExtractMethodBody(source, "GetActiveAsync", "");
        Assert.DoesNotContain("new CommandDefinition", getActive, StringComparison.Ordinal);
    }

    [Theory]
    [InlineData("UpsertAsync")]
    [InlineData("DeleteAsync")]
    [InlineData("DeleteExpiredAsync")]
    public void WizardDraftRepository_MutatingMethods_ShouldNotUseCommandDefinition(string methodName)
    {
        var repoRoot = FindRepositoryRoot();
        var source = File.ReadAllText(Path.Combine(
            repoRoot,
            "src",
            "GmRelay.Shared",
            "Features",
            "Sessions",
            "CreateSession",
            "Wizard",
            "WizardDraftRepository.cs"));

        var body = ExtractMethodBody(source, methodName, "");
        Assert.DoesNotContain("new CommandDefinition", body, StringComparison.Ordinal);
    }

    /// <summary>
    /// WizardDraftRepository was the only AOT-fatal site in v3.9.0, but the
    /// same pattern (CommandDefinition on a Dapper extension that the AOT
    /// generator cannot reach) is repeated in 4 club-picker / permission
    /// lookups across Telegram and Discord messengers. v3.9.2 hotfix
    /// converted them all to the direct (sql, params) overload. Lock the
    /// regression so the next refactor doesn't reintroduce it.
    /// </summary>
    [Theory]
    [InlineData("src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/TelegramWizardMessenger.cs", "GetOwnerClubsAsync", "")]
    [InlineData("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardMessenger.cs", "GetOwnerClubsAsync", "")]
    [InlineData("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardInteractionModule.cs", "LoadClubsAsync", "internal static class WizardClubLookup")]
    [InlineData("src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordPermissionLookup.cs", "LoadManagerUserIdsAsync", "")]
    public void ClubPickerAndPermissionLookups_ShouldNotUseCommandDefinition(string relativePath, string methodName, string containingClass)
    {
        var repoRoot = FindRepositoryRoot();
        var source = File.ReadAllText(Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));

        var body = ExtractMethodBody(source, methodName, containingClass);
        Assert.DoesNotContain("new CommandDefinition", body, StringComparison.Ordinal);
    }

    /// <summary>
    /// AOT RowFactory for WizardDraft expects to read timestamps as
    /// <see cref="DateTime"/> (UTC). If a future refactor switches the
    /// properties back to <see cref="DateTimeOffset"/>, the AOT row factory
    /// will throw <c>InvalidCastException: DateTime → DateTimeOffset</c>
    /// on the first query against wizard_drafts.
    /// </summary>
    [Fact]
    public void WizardDraft_TimestampsMustBeDateTime_NotDateTimeOffset()
    {
        var repoRoot = FindRepositoryRoot();
        var source = File.ReadAllText(Path.Combine(
            repoRoot,
            "src",
            "GmRelay.Shared",
            "Features",
            "Sessions",
            "CreateSession",
            "Wizard",
            "WizardDraft.cs"));

        Assert.Contains("public DateTime CreatedAt", source, StringComparison.Ordinal);
        Assert.Contains("public DateTime UpdatedAt", source, StringComparison.Ordinal);
        Assert.Contains("public DateTime ExpiresAt", source, StringComparison.Ordinal);
        Assert.DoesNotContain("public DateTimeOffset CreatedAt", source, StringComparison.Ordinal);
        Assert.DoesNotContain("public DateTimeOffset UpdatedAt", source, StringComparison.Ordinal);
        Assert.DoesNotContain("public DateTimeOffset ExpiresAt", source, StringComparison.Ordinal);
    }

    private static string ExtractMethodBody(string source, string methodName, string containingClass)
    {
        var searchFrom = source.IndexOf(methodName, StringComparison.Ordinal);

        // If a containing class is given (non-empty), narrow the search
        // to the first occurrence AFTER the class declaration. This is
        // needed when the same method name is used as a call site
        // elsewhere in the file.
        if (!string.IsNullOrEmpty(containingClass))
        {
            var classIdx = source.IndexOf(containingClass, StringComparison.Ordinal);
            if (classIdx < 0)
            {
                throw new InvalidOperationException($"Could not locate class {containingClass} in source.");
            }
            searchFrom = source.IndexOf(methodName, classIdx, StringComparison.Ordinal);
        }

        if (searchFrom < 0)
        {
            throw new InvalidOperationException($"Could not locate {methodName} in source.");
        }

        // Accept any return type: `public async Task` (no result) or
        // `public async Task<int>` (with result). Search for the keyword
        // "Task" in a 60-char window before the method name so we also
        // pick up `public static async Task<IReadOnlyList<ulong>>`.
        var windowStart = Math.Max(0, searchFrom - 60);
        var idx = source.IndexOf("Task", windowStart, StringComparison.Ordinal);
        if (idx < 0 || idx >= searchFrom)
        {
            throw new InvalidOperationException($"Could not locate {methodName} declaration in source.");
        }

        var braceStart = source.IndexOf('{', idx);
        if (braceStart < 0)
        {
            throw new InvalidOperationException($"Could not locate body opening brace for {methodName}.");
        }

        var depth = 1;
        var pos = braceStart + 1;
        while (pos < source.Length && depth > 0)
        {
            switch (source[pos])
            {
                case '{': depth++; break;
                case '}': depth--; break;
            }
            pos++;
        }

        return source.Substring(braceStart, pos - braceStart);
    }

    private static string FindRepositoryRoot()
    {
        var directory = new DirectoryInfo(AppContext.BaseDirectory);
        while (directory is not null)
        {
            if (File.Exists(Path.Combine(directory.FullName, "Directory.Build.props")))
            {
                return directory.FullName;
            }
            directory = directory.Parent;
        }
        throw new InvalidOperationException("Could not locate repository root.");
    }
}
