using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using GmRelay.Web.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

namespace GmRelay.Bot.Tests.Web;

public sealed class TelegramAuthServiceTests
{
    [Fact]
    public void Verify_ShouldAcceptValidTelegramPayload()
    {
        const string botToken = "test-bot-token";
        var authDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var query = CreateQueryCollection(
            botToken,
            new Dictionary<string, string>
            {
                ["auth_date"] = authDate,
                ["first_name"] = "Ada",
                ["id"] = "424242",
                ["last_name"] = "Lovelace",
                ["username"] = "ada"
            });
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.Verify(query, out var telegramId, out var name);

        Assert.True(verified);
        Assert.Equal(424242L, telegramId);
        Assert.Equal("Ada Lovelace", name);
    }

    [Fact]
    public void Verify_ShouldRejectTamperedHash()
    {
        const string botToken = "test-bot-token";
        var values = new Dictionary<string, string>
        {
            ["auth_date"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
            ["first_name"] = "Ada",
            ["id"] = "424242"
        };
        var query = CreateQueryCollection(botToken, values);
        var invalidQuery = new QueryCollection(new Dictionary<string, StringValues>(query.ToDictionary(
            pair => pair.Key,
            pair => pair.Value))
        {
            ["hash"] = "00"
        });
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.Verify(invalidQuery, out _, out _);

        Assert.False(verified);
    }

    [Fact]
    public void Verify_ShouldRejectExpiredPayload()
    {
        const string botToken = "test-bot-token";
        var expiredAuthDate = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds().ToString();
        var query = CreateQueryCollection(
            botToken,
            new Dictionary<string, string>
            {
                ["auth_date"] = expiredAuthDate,
                ["first_name"] = "Ada",
                ["id"] = "424242"
            });
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.Verify(query, out _, out _);

        Assert.False(verified);
    }

    [Fact]
    public void VerifyWebAppInitData_ShouldAcceptValidTelegramWebAppPayload()
    {
        const string botToken = "test-bot-token";
        var initData = CreateWebAppInitData(
            botToken,
            new Dictionary<string, string>
            {
                ["auth_date"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
                ["query_id"] = "AAHdF6IQAAAAAN0XohDhrOrc",
                ["user"] = """{"id":424242,"first_name":"Ada","last_name":"Lovelace","username":"ada"}"""
            });
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.VerifyWebAppInitData(initData, out var telegramId, out var name);

        Assert.True(verified);
        Assert.Equal(424242L, telegramId);
        Assert.Equal("Ada Lovelace", name);
    }

    [Fact]
    public void VerifyWebAppInitData_ShouldRejectTamperedHash()
    {
        const string botToken = "test-bot-token";
        var initData = CreateWebAppInitData(
            botToken,
            new Dictionary<string, string>
            {
                ["auth_date"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
                ["user"] = """{"id":424242,"first_name":"Ada"}"""
            });
        var tamperedInitData = initData.Replace("hash=", "hash=00", StringComparison.Ordinal);
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.VerifyWebAppInitData(tamperedInitData, out _, out _);

        Assert.False(verified);
    }

    [Fact]
    public void VerifyWebAppInitData_ShouldRejectExpiredPayload()
    {
        const string botToken = "test-bot-token";
        var initData = CreateWebAppInitData(
            botToken,
            new Dictionary<string, string>
            {
                ["auth_date"] = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds().ToString(),
                ["user"] = """{"id":424242,"first_name":"Ada"}"""
            });
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.VerifyWebAppInitData(initData, out _, out _);

        Assert.False(verified);
    }

    [Fact]
    public void VerifyLoginPayload_ShouldAcceptValidTelegramWidgetCallbackPayload()
    {
        const string botToken = "test-bot-token";
        var authDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        var values = new Dictionary<string, string>
        {
            ["auth_date"] = authDate.ToString(),
            ["first_name"] = "Ada",
            ["id"] = "424242",
            ["last_name"] = "Lovelace",
            ["photo_url"] = "https://t.me/i/userpic/320/ada.jpg",
            ["username"] = "ada"
        };
        var payload = new TelegramLoginPayload(
            424242,
            "Ada",
            "Lovelace",
            "ada",
            "https://t.me/i/userpic/320/ada.jpg",
            authDate,
            ComputeTelegramHash(botToken, values));
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.VerifyLoginPayload(payload, out var telegramId, out var name);

        Assert.True(verified);
        Assert.Equal(424242L, telegramId);
        Assert.Equal("Ada Lovelace", name);
    }

    [Fact]
    public void VerifyLoginPayload_ShouldRejectTamperedCallbackHash()
    {
        var payload = new TelegramLoginPayload(
            424242,
            "Ada",
            null,
            null,
            null,
            DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
            "00");
        var service = new TelegramAuthService(CreateConfiguration("test-bot-token"));

        var verified = service.VerifyLoginPayload(payload, out _, out _);

        Assert.False(verified);
    }

    [Fact]
    public void VerifyLoginPayload_ShouldRejectExpiredCallbackPayload()
    {
        const string botToken = "test-bot-token";
        var authDate = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds();
        var values = new Dictionary<string, string>
        {
            ["auth_date"] = authDate.ToString(),
            ["first_name"] = "Ada",
            ["id"] = "424242"
        };
        var payload = new TelegramLoginPayload(
            424242,
            "Ada",
            null,
            null,
            null,
            authDate,
            ComputeTelegramHash(botToken, values));
        var service = new TelegramAuthService(CreateConfiguration(botToken));

        var verified = service.VerifyLoginPayload(payload, out _, out _);

        Assert.False(verified);
    }

    [Fact]
    public void VerifyLoginPayload_ShouldRejectMissingRequiredCallbackFields()
    {
        var payload = new TelegramLoginPayload(
            0,
            "",
            null,
            null,
            null,
            DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
            "");
        var service = new TelegramAuthService(CreateConfiguration("test-bot-token"));

        var verified = service.VerifyLoginPayload(payload, out _, out _);

        Assert.False(verified);
    }

    [Fact]
    public void TelegramLoginPayload_ShouldDeserializeTelegramWidgetSnakeCaseJson()
    {
        var payload = JsonSerializer.Deserialize<TelegramLoginPayload>(
            """
            {
              "id": 424242,
              "first_name": "Ada",
              "last_name": "Lovelace",
              "username": "ada",
              "photo_url": "https://t.me/i/userpic/320/ada.jpg",
              "auth_date": 1714300000,
              "hash": "abcdef"
            }
            """);

        Assert.NotNull(payload);
        Assert.Equal(424242L, payload.Id);
        Assert.Equal("Ada", payload.FirstName);
        Assert.Equal("Lovelace", payload.LastName);
        Assert.Equal("ada", payload.Username);
        Assert.Equal("https://t.me/i/userpic/320/ada.jpg", payload.PhotoUrl);
        Assert.Equal(1714300000L, payload.AuthDate);
        Assert.Equal("abcdef", payload.Hash);
    }

    private static IConfiguration CreateConfiguration(string botToken) =>
        new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Telegram:BotToken"] = botToken
            })
            .Build();

    private static QueryCollection CreateQueryCollection(string botToken, Dictionary<string, string> values)
    {
        var hash = ComputeTelegramHash(botToken, values);
        var queryValues = values.ToDictionary(
            pair => pair.Key,
            pair => new StringValues(pair.Value));
        queryValues["hash"] = new StringValues(hash);
        return new QueryCollection(queryValues);
    }

    private static string ComputeTelegramHash(string botToken, IReadOnlyDictionary<string, string> values)
    {
        var dataCheckString = string.Join(
            "\n",
            values
                .OrderBy(pair => pair.Key, StringComparer.Ordinal)
                .Select(pair => $"{pair.Key}={pair.Value}"));
        var secretKey = SHA256.HashData(Encoding.UTF8.GetBytes(botToken));
        var hashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString));
        return Convert.ToHexString(hashBytes).ToLowerInvariant();
    }

    private static string CreateWebAppInitData(string botToken, IReadOnlyDictionary<string, string> values)
    {
        var hash = ComputeTelegramWebAppHash(botToken, values);
        var encodedPairs = values
            .OrderBy(pair => pair.Key, StringComparer.Ordinal)
            .Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")
            .Append($"hash={hash}");

        return string.Join("&", encodedPairs);
    }

    private static string ComputeTelegramWebAppHash(string botToken, IReadOnlyDictionary<string, string> values)
    {
        var dataCheckString = string.Join(
            "\n",
            values
                .OrderBy(pair => pair.Key, StringComparer.Ordinal)
                .Select(pair => $"{pair.Key}={pair.Value}"));
        var secretKey = HMACSHA256.HashData(Encoding.UTF8.GetBytes("WebAppData"), Encoding.UTF8.GetBytes(botToken));
        var hashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString));
        return Convert.ToHexString(hashBytes).ToLowerInvariant();
    }
}
