using Ikar.Server.Controllers;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure;
using Ikar.Server.Infrastructure.Auth;
using Ikar.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace Ikar.Server.Tests;

public sealed class AuthEmailControllerTests
{
    [Fact]
    public async Task VerifyEmailCode_NewEmail_CreatesUserAndSession()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var sender = new RecordingEmailCodeSender();
        var challengeStore = new EmailAuthChallengeStore();
        var controller = CreateController(dbContext, challengeStore, sender);

        var challengeResult = await controller.RequestEmailCode(
            new RequestEmailCodeRequest("user@yandex.ru", "+10000000001"),
            CancellationToken.None);
        var challenge = ControllerTestFixture.ValueOf(challengeResult);

        var sessionResult = await controller.VerifyEmailCode(
            new VerifyEmailCodeRequest(challenge.ChallengeId, "user@yandex.ru", "+10000000001", sender.LastCode!, "Иван"),
            CancellationToken.None);
        var session = ControllerTestFixture.ValueOf(sessionResult);

        Assert.Equal("Иван", session.User.DisplayName);
        Assert.NotEmpty(session.AccessToken);
        Assert.NotEmpty(session.RefreshToken);
        Assert.Contains(dbContext.Users, user => user.NormalizedEmail == "USER@YANDEX.RU");
        Assert.Contains(dbContext.Users, user => user.NormalizedPhoneNumber == PhoneNumberNormalizer.Normalize("+10000000001"));
    }

    [Fact]
    public async Task RequestEmailCode_NewAccountWithUnsupportedEmailDomain_ReturnsValidationProblem()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var sender = new RecordingEmailCodeSender();
        var controller = CreateController(dbContext, new EmailAuthChallengeStore(), sender);

        var result = await controller.RequestEmailCode(
            new RequestEmailCodeRequest("user@gmail.com", "+10000000009"),
            CancellationToken.None);

        var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
        Assert.Equal(EmailAddressNormalizer.UnsupportedRegistrationDomainMessage, badRequest.Value);
        Assert.Null(sender.LastCode);
    }

    [Fact]
    public async Task VerifyEmailCode_ExistingPhoneOnlyUser_LinksEmailAndCreatesSession()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        const string phoneNumber = "+10000000002";
        var existingUser = new User
        {
            Username = "legacy",
            NormalizedUsername = "LEGACY",
            DisplayName = "Legacy",
            PasswordHash = "test",
            PhoneNumber = phoneNumber,
            NormalizedPhoneNumber = PhoneNumberNormalizer.Normalize(phoneNumber)
        };
        dbContext.Users.Add(existingUser);
        await dbContext.SaveChangesAsync();

        var sender = new RecordingEmailCodeSender();
        var controller = CreateController(dbContext, new EmailAuthChallengeStore(), sender);

        var challengeResult = await controller.RequestEmailCode(
            new RequestEmailCodeRequest("legacy@mail.ru", phoneNumber),
            CancellationToken.None);
        var challenge = ControllerTestFixture.ValueOf(challengeResult);

        var sessionResult = await controller.VerifyEmailCode(
            new VerifyEmailCodeRequest(challenge.ChallengeId, "legacy@mail.ru", phoneNumber, sender.LastCode!, null),
            CancellationToken.None);
        var session = ControllerTestFixture.ValueOf(sessionResult);

        var storedUser = Assert.Single(dbContext.Users, user => user.Id == existingUser.Id);
        Assert.Equal(existingUser.Id, session.User.Id);
        Assert.Equal("LEGACY@MAIL.RU", storedUser.NormalizedEmail);
        Assert.Equal(PhoneNumberNormalizer.Normalize(phoneNumber), storedUser.NormalizedPhoneNumber);
    }

    [Fact]
    public async Task RequestEmailCode_EmailAndPhoneFromDifferentAccounts_ReturnsConflict()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        dbContext.Users.AddRange(
            new User
            {
                Username = "email-user",
                NormalizedUsername = "EMAIL-USER",
                DisplayName = "Email",
                PasswordHash = "test",
                Email = "user@yandex.ru",
                NormalizedEmail = "USER@YANDEX.RU",
                PhoneNumber = "+10000000003",
                NormalizedPhoneNumber = "10000000003"
            },
            new User
            {
                Username = "phone-user",
                NormalizedUsername = "PHONE-USER",
                DisplayName = "Phone",
                PasswordHash = "test",
                PhoneNumber = "+10000000004",
                NormalizedPhoneNumber = "10000000004"
            });
        await dbContext.SaveChangesAsync();

        var sender = new RecordingEmailCodeSender();
        var controller = CreateController(dbContext, new EmailAuthChallengeStore(), sender);

        var result = await controller.RequestEmailCode(
            new RequestEmailCodeRequest("user@yandex.ru", "+10000000004"),
            CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(result.Result);
        Assert.Null(sender.LastCode);
    }

    [Fact]
    public async Task VerifyEmailCode_WithDifferentPhoneThanChallenge_ReturnsUnauthorized()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var sender = new RecordingEmailCodeSender();
        var controller = CreateController(dbContext, new EmailAuthChallengeStore(), sender);

        var challengeResult = await controller.RequestEmailCode(
            new RequestEmailCodeRequest("user@yandex.ru", "+10000000005"),
            CancellationToken.None);
        var challenge = ControllerTestFixture.ValueOf(challengeResult);

        var sessionResult = await controller.VerifyEmailCode(
            new VerifyEmailCodeRequest(challenge.ChallengeId, "user@yandex.ru", "+10000000006", sender.LastCode!, "Иван"),
            CancellationToken.None);

        Assert.IsType<UnauthorizedObjectResult>(sessionResult.Result);
    }

    [Fact]
    public async Task RequestPhoneCode_IsDisabled()
    {
        await using var database = new SqliteTestDatabase();
        await using var dbContext = await database.CreateContextAsync();
        var controller = CreateController(dbContext, new EmailAuthChallengeStore(), new RecordingEmailCodeSender());

        var result = await controller.RequestPhoneCode(
            new RequestPhoneCodeRequest("+10000009999"),
            CancellationToken.None);

        var response = Assert.IsType<ObjectResult>(result.Result);
        Assert.Equal(StatusCodes.Status410Gone, response.StatusCode);
    }

    private static AuthController CreateController(
        IkarDbContext dbContext,
        EmailAuthChallengeStore emailChallengeStore,
        IEmailCodeSender emailCodeSender)
    {
        var controller = new AuthController(
            dbContext,
            new TokenService(dbContext, Options.Create(new JwtOptions())),
            new PresenceTracker(),
            emailChallengeStore,
            emailCodeSender);
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext()
        };
        return controller;
    }

    private sealed class RecordingEmailCodeSender : IEmailCodeSender
    {
        public string? LastCode { get; private set; }

        public Task SendLoginCodeAsync(
            string email,
            string code,
            DateTimeOffset expiresAt,
            CancellationToken cancellationToken = default)
        {
            LastCode = code;
            return Task.CompletedTask;
        }
    }
}
