namespace GmRelay.Bot.Tests.Web;

public sealed class MasterProfilesTests
{
    [Fact]
    public async Task MigrationV028_ShouldAddMasterProfilesWithoutExternalIdentifiers()
    {
        var migration = await ReadRepositoryFileAsync("src/GmRelay.Bot/Migrations/V028__add_master_profiles.sql");

        Assert.Contains("CREATE TABLE master_profiles", migration, StringComparison.Ordinal);
        Assert.Contains("player_id", migration, StringComparison.Ordinal);
        Assert.Contains("public_slug", migration, StringComparison.Ordinal);
        Assert.Contains("is_public", migration, StringComparison.Ordinal);
        Assert.Contains("display_name", migration, StringComparison.Ordinal);
        Assert.Contains("bio", migration, StringComparison.Ordinal);
        Assert.Contains("ux_master_profiles_public_slug", migration, StringComparison.Ordinal);
        Assert.DoesNotContain("external_user_id", migration, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("telegram_id", migration, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("discord", migration, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task SessionStore_ShouldExposeSanitizedMasterProfileContracts()
    {
        var sessionStore = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/ISessionStore.cs");

        Assert.Contains("MasterProfileSettings", sessionStore, StringComparison.Ordinal);
        Assert.Contains("PublicMasterProfile", sessionStore, StringComparison.Ordinal);
        Assert.Contains("PublicMasterClub", sessionStore, StringComparison.Ordinal);
        Assert.Contains("GetMasterProfileSettingsAsync", sessionStore, StringComparison.Ordinal);
        Assert.Contains("UpdateMasterProfileSettingsAsync", sessionStore, StringComparison.Ordinal);
        Assert.Contains("GetPublicMasterProfileBySlugAsync", sessionStore, StringComparison.Ordinal);

        var publicProfileSection = RecordSection(sessionStore, "PublicMasterProfile");
        Assert.DoesNotContain("AvatarUrl", publicProfileSection, StringComparison.Ordinal);
        Assert.DoesNotContain("ExternalUserId", publicProfileSection, StringComparison.Ordinal);
        Assert.DoesNotContain("TelegramId", publicProfileSection, StringComparison.Ordinal);
        Assert.DoesNotContain("DiscordId", publicProfileSection, StringComparison.Ordinal);
    }

    [Fact]
    public async Task PublicMasterProfilePage_ShouldBePublicAndHideTechnicalIdentityData()
    {
        var publicProfilePage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/PublicMasterProfile.razor");

        Assert.Contains("@page \"/gm/{Slug}\"", publicProfilePage, StringComparison.Ordinal);
        Assert.Contains("@layout PublicLayout", publicProfilePage, StringComparison.Ordinal);
        Assert.Contains("GetPublicMasterProfileBySlugAsync", publicProfilePage, StringComparison.Ordinal);
        Assert.DoesNotContain("@attribute [Authorize]", publicProfilePage, StringComparison.Ordinal);
        Assert.DoesNotContain("ExternalUserId", publicProfilePage, StringComparison.Ordinal);
        Assert.DoesNotContain("TelegramId", publicProfilePage, StringComparison.Ordinal);
        Assert.DoesNotContain("DiscordId", publicProfilePage, StringComparison.Ordinal);
        Assert.DoesNotContain("AvatarUrl", publicProfilePage, StringComparison.Ordinal);
        Assert.DoesNotContain("LinkedIdentity", publicProfilePage, StringComparison.Ordinal);
    }

    [Fact]
    public async Task ProfilePage_ShouldManageMasterProfilePublication()
    {
        var profilePage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/Profile.razor");
        var authorizedService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/AuthorizedSessionService.cs");

        Assert.Contains("GetMasterProfileSettingsForCurrentUserAsync", profilePage, StringComparison.Ordinal);
        Assert.Contains("UpdateMasterProfileSettingsForCurrentUserAsync", profilePage, StringComparison.Ordinal);
        Assert.Contains("masterProfileModel", profilePage, StringComparison.Ordinal);
        Assert.Contains("PublicMasterProfileUrl", profilePage, StringComparison.Ordinal);
        Assert.Contains("NormalizeMasterProfileSlug", authorizedService, StringComparison.Ordinal);
    }

    [Fact]
    public async Task TelegramLoginEndpoints_ShouldUpsertPlayersForProfileManagement()
    {
        var program = await ReadRepositoryFileAsync("src/GmRelay.Web/Program.cs");

        Assert.Contains("ISessionStore sessionStore", EndpointSection(program, "auth/telegram-webapp"), StringComparison.Ordinal);
        Assert.Contains("sessionStore.UpsertPlayerAsync", EndpointSection(program, "auth/telegram-webapp"), StringComparison.Ordinal);
        Assert.Contains("ISessionStore sessionStore", EndpointSection(program, "auth/telegram-login"), StringComparison.Ordinal);
        Assert.Contains("sessionStore.UpsertPlayerAsync", EndpointSection(program, "auth/telegram-login"), StringComparison.Ordinal);
    }

    [Fact]
    public async Task PublicGamePages_ShouldLinkPublishedMasterProfilesWithoutPrivateIds()
    {
        var publicClubPage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/PublicClub.razor");
        var publicSessionPage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/PublicSession.razor");
        var showcasePage = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/Showcase.razor");
        var sessionService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs");

        Assert.Contains("MasterProfileSlug", publicClubPage, StringComparison.Ordinal);
        Assert.Contains("MasterProfileSlug", publicSessionPage, StringComparison.Ordinal);
        Assert.Contains("MasterProfileSlug", showcasePage, StringComparison.Ordinal);
        Assert.Contains("master_profiles", sessionService, StringComparison.Ordinal);
        Assert.DoesNotContain("targetExternalUserId", PublicQuerySection(sessionService), StringComparison.Ordinal);
        Assert.DoesNotContain("target_platform", PublicQuerySection(sessionService), StringComparison.Ordinal);
    }

    [Fact]
    public async Task PublicMasterProfileQueries_ShouldIncludeCoGmManagedPublishedGames()
    {
        var sessionService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs");

        var clubsQuery = MethodSection(sessionService, "GetPublicClubsForMasterAsync");
        var sessionsQuery = MethodSection(sessionService, "GetPublicSessionsForMasterAsync");

        Assert.Contains("JOIN group_managers gm", clubsQuery, StringComparison.Ordinal);
        Assert.Contains("JOIN group_managers gm", sessionsQuery, StringComparison.Ordinal);
        Assert.DoesNotContain("gm.role = @OwnerRole", clubsQuery, StringComparison.Ordinal);
        Assert.DoesNotContain("gm.role = @OwnerRole", sessionsQuery, StringComparison.Ordinal);
    }

    [Fact]
    public async Task PublicOwnerProfileLinks_ShouldOrderByExistingGroupManagerTimestamp()
    {
        var migration = await ReadRepositoryFileAsync("src/GmRelay.Bot/Migrations/V008__add_group_managers.sql");
        var sessionService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs");

        Assert.Contains("created_at", migration, StringComparison.Ordinal);
        Assert.DoesNotContain("added_at", migration, StringComparison.Ordinal);
        Assert.DoesNotContain("gm.added_at", PublicQuerySection(sessionService), StringComparison.Ordinal);
        Assert.Contains("gm.created_at", PublicQuerySection(sessionService), StringComparison.Ordinal);
    }

    private static string RecordSection(string source, string recordName)
    {
        var start = source.IndexOf($"record {recordName}", StringComparison.Ordinal);
        if (start < 0)
            return string.Empty;

        var end = source.IndexOf(");", start, StringComparison.Ordinal);
        return end < 0 ? source[start..] : source[start..(end + 2)];
    }

    private static string PublicQuerySection(string source)
    {
        var start = source.IndexOf("GetPublicMasterProfileBySlugAsync", StringComparison.Ordinal);
        if (start < 0)
            return string.Empty;

        var end = source.IndexOf("// --- Identity linking", start, StringComparison.Ordinal);
        return end < 0 ? source[start..] : source[start..end];
    }

    private static string MethodSection(string source, string methodName)
    {
        var start = -1;
        var searchFrom = 0;
        while (searchFrom < source.Length)
        {
            var candidate = source.IndexOf(methodName, searchFrom, StringComparison.Ordinal);
            if (candidate < 0)
                return string.Empty;

            var lineStart = source.LastIndexOf('\n', candidate);
            var headerStart = lineStart < 0 ? 0 : lineStart + 1;
            var header = source[headerStart..candidate];
            if (header.Contains("private static async Task", StringComparison.Ordinal))
            {
                start = candidate;
                break;
            }

            searchFrom = candidate + methodName.Length;
        }

        var nextMethod = source.IndexOf("\n    private static async Task", start + methodName.Length, StringComparison.Ordinal);
        if (nextMethod < 0)
        {
            nextMethod = source.IndexOf("\n    public async Task", start + methodName.Length, StringComparison.Ordinal);
        }

        return nextMethod < 0 ? source[start..] : source[start..nextMethod];
    }

    private static string EndpointSection(string source, string route)
    {
        var start = source.IndexOf($"\"/{route}\"", StringComparison.Ordinal);
        if (start < 0)
            return string.Empty;

        var nextEndpoint = source.IndexOf("app.Map", start + route.Length, StringComparison.Ordinal);
        return nextEndpoint < 0 ? source[start..] : source[start..nextEndpoint];
    }

    private static async Task<string> ReadRepositoryFileAsync(string relativePath)
    {
        var directory = new DirectoryInfo(AppContext.BaseDirectory);
        while (directory is not null)
        {
            var candidate = Path.Combine(directory.FullName, relativePath);
            if (File.Exists(candidate))
            {
                return await File.ReadAllTextAsync(candidate);
            }

            directory = directory.Parent;
        }

        throw new FileNotFoundException($"Could not locate repository file '{relativePath}'.");
    }
}
