using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Xunit;

namespace foodmarket.IntegrationTests;

[Collection(ApiCollection.Name)]
public class TwoFactorTests
{
    private readonly ApiFactory _factory;
    public TwoFactorTests(ApiFactory factory) => _factory = factory;

    /// <summary>Enroll возвращает SharedKey + otpauth-URI; Verify с правильным
    /// кодом включает 2FA; Status показывает enabled.</summary>
    [Fact]
    public async Task Enroll_verify_status_workflow()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"tfa-{Guid.NewGuid():N}");

        // 1) Изначально 2FA выключен.
        var status1 = await api.GetJsonAsync("/api/me/2fa/status");
        status1.GetProperty("enabled").GetBoolean().Should().BeFalse();

        // 2) Enroll → секрет.
        var enroll = await api.Http.PostAsJsonAsync("/api/me/2fa/enroll", new { });
        enroll.EnsureSuccessStatusCode();
        var enrollResp = await enroll.Content.ReadFromJsonAsync<JsonElement>();
        var sharedKey = enrollResp.GetProperty("sharedKey").GetString();
        var uri = enrollResp.GetProperty("authenticatorUri").GetString();
        sharedKey.Should().NotBeNullOrWhiteSpace();
        uri.Should().Contain("otpauth://totp/");
        uri.Should().Contain(sharedKey);

        // 3) Сгенерировать TOTP-код по тому же RFC 6238 и Verify.
        var code = GenerateTotp(sharedKey!);
        var verify = await api.Http.PostAsJsonAsync("/api/me/2fa/verify", new { code });
        verify.EnsureSuccessStatusCode();

        // 4) Status теперь enabled.
        var status2 = await api.GetJsonAsync("/api/me/2fa/status");
        status2.GetProperty("enabled").GetBoolean().Should().BeTrue();
    }

    /// <summary>Wrong code → 400.</summary>
    [Fact]
    public async Task Verify_with_wrong_code_returns_400()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"tfa-wrong-{Guid.NewGuid():N}");
        await api.Http.PostAsJsonAsync("/api/me/2fa/enroll", new { });
        using var r = await api.Http.PostAsJsonAsync("/api/me/2fa/verify", new { code = "000000" });
        ((int)r.StatusCode).Should().Be(400);
    }

    /// <summary>После включения 2FA password-grant без otp_code → invalid_grant
    /// с описанием "2fa_required". С правильным otp_code — token выдаётся.</summary>
    [Fact]
    public async Task Token_endpoint_requires_otp_when_2fa_enabled()
    {
        var api = new ApiActor(_factory.CreateClient());
        var (email, password) = await api.SignupAndLoginAsync($"tfa-tok-{Guid.NewGuid():N}");

        var enroll = await api.Http.PostAsJsonAsync("/api/me/2fa/enroll", new { });
        var enrollResp = await enroll.Content.ReadFromJsonAsync<JsonElement>();
        var sharedKey = enrollResp.GetProperty("sharedKey").GetString();
        await api.Http.PostAsJsonAsync("/api/me/2fa/verify", new { code = GenerateTotp(sharedKey!) });

        // Без otp_code → invalid_grant.
        using var noOtp = await api.Http.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "password",
            ["username"] = email,
            ["password"] = password,
            ["client_id"] = "food-market-web",
            ["scope"] = "openid profile email roles api offline_access",
        }));
        ((int)noOtp.StatusCode).Should().Be(400);
        var noOtpBody = await noOtp.Content.ReadAsStringAsync();
        noOtpBody.Should().Contain("2fa_required");

        // С правильным otp_code → success.
        // TOTP-step 30 секунд; в тестах потенциально надо ждать новый шаг —
        // используем тот же код, что прошёл при verify (в пределах окна).
        using var withOtp = await api.Http.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "password",
            ["username"] = email,
            ["password"] = password,
            ["client_id"] = "food-market-web",
            ["scope"] = "openid profile email roles api offline_access",
            ["otp_code"] = GenerateTotp(sharedKey!),
        }));
        withOtp.IsSuccessStatusCode.Should().BeTrue(
            $"token c otp_code должен пройти: {(int)withOtp.StatusCode} {await withOtp.Content.ReadAsStringAsync()}");
    }

    [Fact]
    public async Task Disable_requires_code()
    {
        var api = new ApiActor(_factory.CreateClient());
        await api.SignupAndLoginAsync($"tfa-dis-{Guid.NewGuid():N}");
        var enroll = await api.Http.PostAsJsonAsync("/api/me/2fa/enroll", new { });
        var sharedKey = (await enroll.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("sharedKey").GetString();
        await api.Http.PostAsJsonAsync("/api/me/2fa/verify", new { code = GenerateTotp(sharedKey!) });

        // Disable без code → 400.
        using var bad = await api.Http.PostAsJsonAsync("/api/me/2fa/disable", new { code = (string?)null });
        ((int)bad.StatusCode).Should().Be(400);

        // Disable с code → 200.
        using var ok = await api.Http.PostAsJsonAsync("/api/me/2fa/disable", new { code = GenerateTotp(sharedKey!) });
        ok.IsSuccessStatusCode.Should().BeTrue();

        var status = await api.GetJsonAsync("/api/me/2fa/status");
        status.GetProperty("enabled").GetBoolean().Should().BeFalse();
    }

    /// <summary>Generate RFC 6238 TOTP for given base32-encoded shared key.
    /// 30-second time step, 6 digits, SHA1 (default for AuthenticatorTokenProvider).</summary>
    private static string GenerateTotp(string base32Key)
    {
        var key = FromBase32(base32Key);
        var counter = (long)Math.Floor((DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds / 30);
        var counterBytes = BitConverter.GetBytes(counter);
        if (BitConverter.IsLittleEndian) Array.Reverse(counterBytes);

        using var hmac = new HMACSHA1(key);
        var hash = hmac.ComputeHash(counterBytes);
        var offset = hash[^1] & 0x0F;
        var binary = ((hash[offset] & 0x7F) << 24)
                   | ((hash[offset + 1] & 0xFF) << 16)
                   | ((hash[offset + 2] & 0xFF) << 8)
                   | (hash[offset + 3] & 0xFF);
        var code = binary % 1_000_000;
        return code.ToString("D6");
    }

    private static byte[] FromBase32(string s)
    {
        const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
        s = s.Trim().TrimEnd('=').ToUpperInvariant();
        var bytes = new List<byte>();
        int buf = 0, bits = 0;
        foreach (var c in s)
        {
            var v = alphabet.IndexOf(c);
            if (v < 0) continue;
            buf = (buf << 5) | v;
            bits += 5;
            if (bits >= 8)
            {
                bits -= 8;
                bytes.Add((byte)((buf >> bits) & 0xFF));
            }
        }
        return bytes.ToArray();
    }
}
