using Ikar.Server.Controllers;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure.Hubs;
using Ikar.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;

namespace Ikar.Server.Tests;

public sealed class PrivacyAndStoriesControllerTests
{
    [Fact]
    public async Task UpdatePrivacySettings_NormalizesVisibilityAndDropsOwnUserId()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var bob = CreateUser("bob");
        dbContext.Users.AddRange(alice, bob);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateUsersController(dbContext, alice.Id);
        var response = await controller.UpdateMyPrivacySettings(
            new UpdatePrivacySettingsRequest(
                PhoneNumberVisibility: "invalid",
                LastSeenVisibility: "nobody",
                ProfilePhotoVisibility: "everyone",
                BlockedUserIds: [alice.Id, bob.Id, bob.Id],
                AlwaysAllowUserIds: [alice.Id],
                NeverAllowUserIds: [bob.Id],
                PasscodeLockEnabled: true,
                ShowMessagePreview: false),
            CancellationToken.None);

        var settings = ControllerTestFixture.ValueOf(response);
        Assert.Equal("contacts", settings.PhoneNumberVisibility);
        Assert.Equal("nobody", settings.LastSeenVisibility);
        Assert.Equal([bob.Id], settings.BlockedUserIds);
        var blockedUser = Assert.Single(settings.BlockedUsers);
        Assert.Equal(bob.Id, blockedUser.Id);
        Assert.Equal("bob", blockedUser.DisplayName);
        Assert.Empty(settings.AlwaysAllowUserIds);
        Assert.Empty(settings.AlwaysAllowUsers);
        Assert.Equal([bob.Id], settings.NeverAllowUserIds);
        var neverAllowUser = Assert.Single(settings.NeverAllowUsers);
        Assert.Equal(bob.Id, neverAllowUser.Id);
        Assert.Equal("bob", neverAllowUser.DisplayName);
        Assert.True(settings.PasscodeLockEnabled);
        Assert.False(settings.ShowMessagePreview);
    }

    [Fact]
    public async Task GetUser_HidesPrivatePhoneLastSeenAndAvatar()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var bob = CreateUser("bob");
        var phoneNumber = "+10000000001";
        var normalizedPhoneNumber = PhoneNumberNormalizer.Normalize(phoneNumber)!;
        alice.PhoneNumber = phoneNumber;
        alice.NormalizedPhoneNumber = normalizedPhoneNumber;
        alice.AvatarStoragePath = "avatar.png";
        alice.AvatarContentType = "image/png";
        alice.PrivacySettings = new UserPrivacySettings
        {
            UserId = alice.Id,
            User = alice,
            PhoneNumberVisibility = "nobody",
            LastSeenVisibility = "nobody",
            ProfilePhotoVisibility = "nobody"
        };
        alice.Sessions.Add(new UserSession
        {
            UserId = alice.Id,
            User = alice,
            LastSeenAt = new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero)
        });
        var sharedChat = CreateDirectChat(alice, bob);
        dbContext.AddRange(alice, bob, sharedChat);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateUsersController(dbContext, bob.Id);
        var response = await controller.GetUser(alice.Id, CancellationToken.None);

        var user = ControllerTestFixture.ValueOf(response);
        Assert.Null(user.PhoneNumber);
        Assert.Null(user.LastSeenAt);
        Assert.Null(user.AvatarPath);
        Assert.False(user.IsOnline);
    }

    [Fact]
    public async Task ResolveContacts_UsesPrivacyFilteredUserDto()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var bob = CreateUser("bob");
        var phoneNumber = "+10000000001";
        var normalizedPhoneNumber = PhoneNumberNormalizer.Normalize(phoneNumber)!;
        alice.PhoneNumber = phoneNumber;
        alice.NormalizedPhoneNumber = normalizedPhoneNumber;
        alice.AvatarStoragePath = "avatar.png";
        alice.AvatarContentType = "image/png";
        alice.PrivacySettings = new UserPrivacySettings
        {
            UserId = alice.Id,
            User = alice,
            PhoneNumberVisibility = "nobody",
            LastSeenVisibility = "nobody",
            ProfilePhotoVisibility = "nobody"
        };
        alice.Sessions.Add(new UserSession
        {
            UserId = alice.Id,
            User = alice,
            LastSeenAt = new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero)
        });
        dbContext.AddRange(alice, bob);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateUsersController(dbContext, bob.Id);
        var response = await controller.ResolveContacts(
            new ResolveContactsRequest([phoneNumber]),
            CancellationToken.None);

        var match = Assert.Single(ControllerTestFixture.ValueOf(response));
        Assert.Equal(normalizedPhoneNumber, match.PhoneNumber);
        Assert.Equal(alice.Id, match.User.Id);
        Assert.Null(match.User.PhoneNumber);
        Assert.Null(match.User.LastSeenAt);
        Assert.Null(match.User.AvatarPath);
    }

    [Fact]
    public async Task GetStories_ReturnsEveryoneStoriesWithoutSharedChat()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var bob = CreateUser("bob");
        var charlie = CreateUser("charlie");
        var sharedChat = CreateDirectChat(alice, bob);
        var peerStory = CreateStory(bob, "peer", StoryVisibility.Everyone);
        var publicStory = CreateStory(charlie, "public", StoryVisibility.Everyone);
        dbContext.AddRange(alice, bob, charlie, sharedChat, peerStory, publicStory);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateStoriesController(dbContext, alice.Id);
        var response = await controller.GetStories(CancellationToken.None);

        var stories = ControllerTestFixture.ValueOf(response);
        Assert.Contains(stories, story => story.Id == peerStory.Id);
        Assert.Contains(stories, story => story.Id == publicStory.Id);
    }

    [Fact]
    public async Task GetStories_ContactsOnlyReturnsStoriesFromSharedChats()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var bob = CreateUser("bob");
        var charlie = CreateUser("charlie");
        var sharedChat = CreateDirectChat(alice, bob);
        var visibleStory = CreateStory(bob, "visible", StoryVisibility.Contacts);
        var hiddenStory = CreateStory(charlie, "hidden", StoryVisibility.Contacts);
        dbContext.AddRange(alice, bob, charlie, sharedChat, visibleStory, hiddenStory);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateStoriesController(dbContext, alice.Id);
        var response = await controller.GetStories(CancellationToken.None);

        var stories = ControllerTestFixture.ValueOf(response);
        var story = Assert.Single(stories);
        Assert.Equal(visibleStory.Id, story.Id);
        Assert.Equal("visible", story.Text);
    }

    [Fact]
    public async Task GetStories_PrivateOnlyReturnsOwnStories()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var bob = CreateUser("bob");
        var sharedChat = CreateDirectChat(alice, bob);
        var ownStory = CreateStory(alice, "own", StoryVisibility.Private);
        var peerStory = CreateStory(bob, "peer", StoryVisibility.Private);
        dbContext.AddRange(alice, bob, sharedChat, ownStory, peerStory);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateStoriesController(dbContext, alice.Id);
        var response = await controller.GetStories(CancellationToken.None);

        var stories = ControllerTestFixture.ValueOf(response);
        var story = Assert.Single(stories);
        Assert.Equal(ownStory.Id, story.Id);
        Assert.Equal("own", story.Text);
    }

    [Fact]
    public async Task CreateStory_NotifiesAllClientsForEveryoneStory()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        dbContext.Users.Add(alice);
        await dbContext.SaveChangesAsync();

        var hubContext = new RecordingHubContext<MessengerHub>();
        var controller = ControllerTestFixture.CreateStoriesController(dbContext, alice.Id, hubContext);
        await controller.CreateStory(new CreateStoryRequest("public", StoryVisibility.Everyone), CancellationToken.None);

        var send = Assert.Single(hubContext.Sends, x => x.Method == "StoriesChanged");
        Assert.Equal("All", send.TargetKind);
        Assert.Empty(send.Targets);
        Assert.Equal(0, send.ArgumentCount);
    }

    [Fact]
    public async Task CreateStory_NotifiesOnlyAuthorAndPeersForContactsStory()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var bob = CreateUser("bob");
        var charlie = CreateUser("charlie");
        var sharedChat = CreateDirectChat(alice, bob);
        dbContext.AddRange(alice, bob, charlie, sharedChat);
        await dbContext.SaveChangesAsync();

        var hubContext = new RecordingHubContext<MessengerHub>();
        var controller = ControllerTestFixture.CreateStoriesController(dbContext, alice.Id, hubContext);
        await controller.CreateStory(new CreateStoryRequest("contacts", StoryVisibility.Contacts), CancellationToken.None);

        var send = Assert.Single(hubContext.Sends, x => x.Method == "StoriesChanged");
        Assert.Equal("Users", send.TargetKind);
        Assert.Equal(
            new[] { alice.Id.ToString(), bob.Id.ToString() }.OrderBy(x => x).ToArray(),
            send.Targets.OrderBy(x => x).ToArray());
        Assert.DoesNotContain(charlie.Id.ToString(), send.Targets);
        Assert.Equal(0, send.ArgumentCount);
    }

    [Fact]
    public async Task CreateMediaStory_PreservesAllSelectedFilesInOrder()
    {
        var tempRoot = Path.Combine(Path.GetTempPath(), $"ikar-story-upload-{Guid.NewGuid():N}");
        Directory.CreateDirectory(tempRoot);
        try
        {
            await using var database = new SqliteTestDatabase();
            await using var dbContext = await database.CreateContextAsync();
            var alice = CreateUser("alice");
            dbContext.Users.Add(alice);
            await dbContext.SaveChangesAsync();

            var controller = ControllerTestFixture.CreateStoriesController(
                dbContext,
                alice.Id,
                new RecordingHubContext<MessengerHub>(),
                ControllerTestFixture.CreateAttachmentStorageService(tempRoot));

            var response = await controller.CreateMediaStory(
                new UploadStoryRequest
                {
                    Text = "trip",
                    Visibility = StoryVisibility.Everyone,
                    Files =
                    [
                        CreateFormFile([1, 2, 3], "first.jpg", "image/jpeg"),
                        CreateFormFile([4, 5, 6], "second.jpg", "image/jpeg"),
                        CreateFormFile([7, 8, 9], "third.jpg", "image/jpeg")
                    ]
                },
                CancellationToken.None);

            var dto = ControllerTestFixture.ValueOf(response);
            Assert.Equal(3, dto.Attachments.Count);
            Assert.Collection(
                dto.Attachments,
                attachment =>
                {
                    Assert.Equal("first.jpg", attachment.FileName);
                    Assert.Equal(0, attachment.SortOrder);
                },
                attachment =>
                {
                    Assert.Equal("second.jpg", attachment.FileName);
                    Assert.Equal(1, attachment.SortOrder);
                },
                attachment =>
                {
                    Assert.Equal("third.jpg", attachment.FileName);
                    Assert.Equal(2, attachment.SortOrder);
                });

            var storedStory = await dbContext.Stories
                .Include(x => x.Attachments)
                .SingleAsync(x => x.Id == dto.Id);
            var storedAttachments = storedStory.Attachments
                .OrderBy(x => x.SortOrder)
                .ToList();
            Assert.Equal(3, storedAttachments.Count);
            Assert.All(storedAttachments, attachment => Assert.True(File.Exists(attachment.StoragePath)));
        }
        finally
        {
            Directory.Delete(tempRoot, recursive: true);
        }
    }

    [Fact]
    public async Task GetAvatar_RespectsProfilePhotoContactsVisibility()
    {
        var avatarPath = Path.GetTempFileName();
        await File.WriteAllTextAsync(avatarPath, "avatar");
        try
        {
            await using var database = new SqliteTestDatabase();
            await using var dbContext = await database.CreateContextAsync();
            var alice = CreateUser("alice");
            var bob = CreateUser("bob");
            var charlie = CreateUser("charlie");
            alice.AvatarStoragePath = avatarPath;
            alice.AvatarContentType = "image/png";
            alice.PrivacySettings = new UserPrivacySettings
            {
                UserId = alice.Id,
                User = alice,
                ProfilePhotoVisibility = "contacts"
            };
            var sharedChat = CreateDirectChat(alice, bob);
            dbContext.AddRange(alice, bob, charlie, sharedChat);
            await dbContext.SaveChangesAsync();

            var bobController = ControllerTestFixture.CreateUsersController(dbContext, bob.Id);
            var bobResponse = await bobController.GetAvatar(alice.Id, Path.GetFileName(avatarPath), CancellationToken.None);
            Assert.IsType<PhysicalFileResult>(bobResponse);

            var charlieController = ControllerTestFixture.CreateUsersController(dbContext, charlie.Id);
            var charlieResponse = await charlieController.GetAvatar(alice.Id, Path.GetFileName(avatarPath), CancellationToken.None);
            Assert.IsType<NotFoundResult>(charlieResponse);
        }
        finally
        {
            File.Delete(avatarPath);
        }
    }

    [Fact]
    public async Task GetStories_HidesContactOnlyAuthorProfileFields_ForNonPeerViewer()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var alice = CreateUser("alice");
        var charlie = CreateUser("charlie");
        alice.PhoneNumber = "+10000000001";
        alice.AvatarStoragePath = "alice-avatar.png";
        alice.PrivacySettings = new UserPrivacySettings
        {
            UserId = alice.Id,
            User = alice,
            PhoneNumberVisibility = "contacts",
            LastSeenVisibility = "contacts",
            ProfilePhotoVisibility = "contacts"
        };
        alice.Sessions.Add(new UserSession
        {
            Id = Guid.NewGuid(),
            UserId = alice.Id,
            User = alice,
            LastSeenAt = DateTimeOffset.UtcNow.AddMinutes(-5)
        });
        var story = CreateStory(alice, "public", StoryVisibility.Everyone);
        dbContext.AddRange(alice, charlie, story);
        await dbContext.SaveChangesAsync();

        var controller = ControllerTestFixture.CreateStoriesController(dbContext, charlie.Id);
        var response = await controller.GetStories(CancellationToken.None);

        var visibleStory = Assert.Single(ControllerTestFixture.ValueOf(response));
        Assert.Equal(alice.Id, visibleStory.Author.Id);
        Assert.Null(visibleStory.Author.PhoneNumber);
        Assert.Null(visibleStory.Author.LastSeenAt);
        Assert.Null(visibleStory.Author.AvatarPath);
    }

    private static User CreateUser(string username) =>
        new()
        {
            Id = Guid.NewGuid(),
            Username = username,
            NormalizedUsername = username.ToUpperInvariant(),
            DisplayName = username,
            PasswordHash = "test"
        };

    private static Chat CreateDirectChat(User first, User second)
    {
        var chat = new Chat
        {
            Id = Guid.NewGuid(),
            Type = ChatType.Direct,
            CreatedById = first.Id,
            CreatedBy = first
        };
        chat.Members.Add(new ChatMember { ChatId = chat.Id, Chat = chat, UserId = first.Id, User = first });
        chat.Members.Add(new ChatMember { ChatId = chat.Id, Chat = chat, UserId = second.Id, User = second });
        return chat;
    }

    private static Story CreateStory(User author, string text, StoryVisibility visibility) =>
        new()
        {
            Id = Guid.NewGuid(),
            AuthorUserId = author.Id,
            AuthorUser = author,
            Text = text,
            Visibility = visibility,
            CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
            ExpiresAt = DateTimeOffset.UtcNow.AddHours(1)
        };

    private static FormFile CreateFormFile(byte[] content, string fileName, string contentType)
    {
        var stream = new MemoryStream(content);
        return new FormFile(stream, 0, stream.Length, "files", fileName)
        {
            Headers = new HeaderDictionary(),
            ContentType = contentType
        };
    }
}
