using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure.Auth;
using Ikar.Server.Infrastructure.Calls;
using Ikar.Server.Infrastructure.Push;
using Ikar.Server.Infrastructure.Storage;
using Ikar.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Ikar.Server.Tests;

public sealed class ReliabilityAndSecurityTests
{
    [Fact]
    public void EmailChallenge_EnforcesResendDelayAndAttemptLimit()
    {
        var store = new EmailAuthChallengeStore();
        var challenge = store.Create("user@example.com", "10000000000", isRegistered: true);

        var rateLimit = Assert.Throws<AuthChallengeRateLimitException>(() =>
            store.Create("user@example.com", "10000000000", isRegistered: true));
        Assert.True(rateLimit.RetryAfter > TimeSpan.Zero);

        for (var attempt = 0; attempt < 5; attempt++)
        {
            Assert.Null(store.Consume(challenge.Id, challenge.NormalizedEmail, challenge.NormalizedPhoneNumber, "000000"));
        }

        Assert.Null(store.Consume(challenge.Id, challenge.NormalizedEmail, challenge.NormalizedPhoneNumber, challenge.Code));
    }

    [Fact]
    public void AttachmentInspector_UsesFileSignatureInsteadOfClaimedMimeType()
    {
        using var fakeImageStream = new MemoryStream("not-an-image"u8.ToArray());
        var fakeImage = new FormFile(fakeImageStream, 0, fakeImageStream.Length, "file", "photo.jpg")
        {
            Headers = new HeaderDictionary(),
            ContentType = "image/jpeg"
        };
        Assert.Equal("application/octet-stream", AttachmentContentInspector.Inspect(fakeImage).ContentType);

        using var jpegStream = new MemoryStream([0xFF, 0xD8, 0xFF, 0xE0]);
        var jpeg = new FormFile(jpegStream, 0, jpegStream.Length, "file", "photo.bin")
        {
            Headers = new HeaderDictionary(),
            ContentType = "application/octet-stream"
        };
        var inspectedJpeg = AttachmentContentInspector.Inspect(jpeg);
        Assert.Equal("image/jpeg", inspectedJpeg.ContentType);
        Assert.True(inspectedJpeg.IsImage);
    }

    [Fact]
    public void CallStore_ExpiresUnansweredCalls()
    {
        var store = new CallSessionStore();
        Assert.True(store.TryCreate(
            Guid.NewGuid(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            CallMediaType.Audio,
            out var created,
            out _));

        var expired = store.DrainExpired(
            created!.CreatedAt.AddSeconds(61),
            TimeSpan.FromSeconds(60),
            TimeSpan.FromHours(8));

        var ended = Assert.Single(expired);
        Assert.Equal("no_answer", ended.EndReason);
        Assert.Equal(CallSessionState.Ended, ended.State);
    }

    [Fact]
    public async Task SendMessage_ReusingClientMessageId_ReturnsOriginalMessage()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var user = new User
        {
            Username = "reliable_sender",
            NormalizedUsername = "RELIABLE_SENDER",
            DisplayName = "Reliable Sender",
            PasswordHash = "test"
        };
        var chat = new Chat
        {
            Type = ChatType.Group,
            Title = "Idempotency",
            CreatedBy = user,
            CreatedById = user.Id
        };
        chat.Members.Add(new ChatMember
        {
            Chat = chat,
            ChatId = chat.Id,
            User = user,
            UserId = user.Id,
            IsOwner = true,
            Role = ChatMemberRole.Owner
        });
        dbContext.AddRange(user, chat);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateChatsController(dbContext, user.Id);
        var clientMessageId = Guid.NewGuid();
        var request = new SendMessageRequest("Send once", ClientMessageId: clientMessageId);
        var first = ControllerTestFixture.ValueOf(await controller.SendMessage(chat.Id, request, CancellationToken.None));
        var second = ControllerTestFixture.ValueOf(await controller.SendMessage(chat.Id, request, CancellationToken.None));

        Assert.Equal(first.Id, second.Id);
        Assert.Equal(clientMessageId, second.ClientMessageId);
        Assert.Equal(1, await dbContext.Messages.CountAsync());
    }

    [Fact]
    public async Task PushOutbox_IsCommittedWithTheMessageAndDeduplicated()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var user = new User
        {
            Username = "outbox_sender",
            NormalizedUsername = "OUTBOX_SENDER",
            DisplayName = "Outbox Sender",
            PasswordHash = "test"
        };
        var chat = new Chat
        {
            Type = ChatType.Group,
            Title = "Outbox",
            CreatedBy = user,
            CreatedById = user.Id
        };
        chat.Members.Add(new ChatMember
        {
            Chat = chat,
            ChatId = chat.Id,
            User = user,
            UserId = user.Id,
            IsOwner = true,
            Role = ChatMemberRole.Owner
        });
        var message = new Message
        {
            Chat = chat,
            ChatId = chat.Id,
            Sender = user,
            SenderId = user.Id,
            Text = "Persist with outbox"
        };
        dbContext.Messages.Add(message);

        using var services = new ServiceCollection().BuildServiceProvider();
        var queue = new PushDispatchQueue(services.GetRequiredService<IServiceScopeFactory>());
        queue.StageMessages(dbContext, message, message);
        await dbContext.SaveChangesAsync();

        var outbox = Assert.Single(await dbContext.PushDispatchOutbox.AsNoTracking().ToListAsync());
        Assert.Equal(message.Id, outbox.MessageId);
        Assert.Equal($"message:{message.Id:D}", outbox.DeduplicationKey);
    }
}
