using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class AuthRecoveryAndDeviceEndpointTests
{
    [Fact]
    public async Task Password_reset_send_does_not_reveal_account_existence()
    {
        var provider = new CapturingSmsProvider();
        await using var factory = new ApiTestFactory(smsProvider: provider);
        var seed = await SeedUserAsync(factory);
        using var client = factory.CreateClient();
        client.DefaultRequestHeaders.Add("x-tenant-code", seed.TenantId.ToString("N"));

        var existing = await client.PostAsJsonAsync(
            "/api/tenant/auth/password/reset/sms/send",
            new PasswordResetSmsSendDto
            {
                TenantCode = seed.TenantId.ToString("N"),
                Phone = seed.Phone,
                DeviceId = "known-device"
            });
        var missing = await client.PostAsJsonAsync(
            "/api/tenant/auth/password/reset/sms/send",
            new PasswordResetSmsSendDto
            {
                TenantCode = seed.TenantId.ToString("N"),
                Phone = "13999999999",
                DeviceId = "unknown-device"
            });

        Assert.Equal(HttpStatusCode.Accepted, existing.StatusCode);
        Assert.Equal(HttpStatusCode.Accepted, missing.StatusCode);
        Assert.Equal(1, provider.SendCount);
        Assert.DoesNotContain(provider.Code!, await existing.Content.ReadAsStringAsync(), StringComparison.Ordinal);
        Assert.DoesNotContain(provider.Code!, await missing.Content.ReadAsStringAsync(), StringComparison.Ordinal);
    }

    [Fact]
    public async Task Password_reset_consumes_reset_code_and_revokes_existing_sessions()
    {
        var provider = new CapturingSmsProvider();
        await using var factory = new ApiTestFactory(smsProvider: provider);
        var seed = await SeedUserAsync(factory);
        using var client = factory.CreateClient();
        var oldTokens = await client.LoginAsTenantAsync(seed.TenantId, seed.Phone);

        var send = await client.PostAsJsonAsync(
            "/api/tenant/auth/password/reset/sms/send",
            new PasswordResetSmsSendDto
            {
                TenantCode = seed.TenantId.ToString("N"),
                Phone = seed.Phone,
                DeviceId = "reset-device"
            });
        Assert.Equal(HttpStatusCode.Accepted, send.StatusCode);
        Assert.NotNull(provider.Code);

        var resetRequest = new PasswordResetDto
        {
            TenantCode = seed.TenantId.ToString("N"),
            Phone = seed.Phone,
            Code = provider.Code!,
            NewPassword = "ResetPassword2026"
        };
        var reset = await client.PostAsJsonAsync("/api/tenant/auth/password/reset", resetRequest);
        Assert.Equal(HttpStatusCode.NoContent, reset.StatusCode);

        client.UseAccessToken(oldTokens);
        Assert.Equal(HttpStatusCode.Unauthorized, (await client.GetAsync("/api/tenant/me")).StatusCode);
        Assert.Equal(
            HttpStatusCode.Unauthorized,
            (await PostPasswordLoginAsync(client, seed, PasswordTestUserExtensions.TestPassword)).StatusCode);
        Assert.Equal(
            HttpStatusCode.OK,
            (await PostPasswordLoginAsync(client, seed, resetRequest.NewPassword)).StatusCode);
        Assert.Equal(
            HttpStatusCode.Unauthorized,
            (await client.PostAsJsonAsync("/api/tenant/auth/password/reset", resetRequest)).StatusCode);
    }

    [Fact]
    public async Task Authenticated_password_change_rotates_to_a_new_session()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedUserAsync(factory);
        using var client = factory.CreateClient();
        var oldTokens = await client.LoginAsTenantAsync(seed.TenantId, seed.Phone);
        client.UseAccessToken(oldTokens);

        var changed = await client.PostAsJsonAsync(
            "/api/tenant/auth/password/change",
            new AuthenticatedPasswordChangeDto
            {
                CurrentPassword = PasswordTestUserExtensions.TestPassword,
                NewPassword = "ChangedPassword2026"
            });
        Assert.Equal(HttpStatusCode.OK, changed.StatusCode);
        using var body = JsonDocument.Parse(await changed.Content.ReadAsStringAsync());
        var accessToken = body.RootElement.GetProperty("user").GetProperty("tokens").GetProperty("accessToken")
            .GetString();
        var refreshToken = body.RootElement.GetProperty("user").GetProperty("tokens").GetProperty("refreshToken")
            .GetString();
        Assert.False(string.IsNullOrWhiteSpace(accessToken));
        Assert.False(string.IsNullOrWhiteSpace(refreshToken));

        client.UseAccessToken(oldTokens);
        Assert.Equal(HttpStatusCode.Unauthorized, (await client.GetAsync("/api/tenant/me")).StatusCode);
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/tenant/me")).StatusCode);
        Assert.Equal(
            HttpStatusCode.Unauthorized,
            (await PostPasswordLoginAsync(client, seed, PasswordTestUserExtensions.TestPassword)).StatusCode);
        Assert.Equal(
            HttpStatusCode.OK,
            (await PostPasswordLoginAsync(client, seed, "ChangedPassword2026")).StatusCode);
    }

    [Fact]
    public async Task Device_sessions_are_scoped_and_only_other_owned_families_can_be_revoked()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedUserAsync(factory);
        var other = await SeedUserAsync(factory);
        using var firstClient = factory.CreateClient();
        using var secondClient = factory.CreateClient();
        using var otherClient = factory.CreateClient();
        _ = await firstClient.LoginAsTenantAsync(seed.TenantId, seed.Phone);
        var secondTokens = await secondClient.LoginAsTenantAsync(seed.TenantId, seed.Phone);
        var otherTokens = await otherClient.LoginAsTenantAsync(other.TenantId, other.Phone);
        secondClient.UseAccessToken(secondTokens);
        otherClient.UseAccessToken(otherTokens);

        using var sessions =
            JsonDocument.Parse(
                await (await secondClient.GetAsync("/api/tenant/me/sessions")).Content.ReadAsStringAsync());
        var items = sessions.RootElement.EnumerateArray().ToArray();
        Assert.Equal(2, items.Length);
        var currentFamily = items.Single(item => item.GetProperty("isCurrent").GetBoolean())
            .GetProperty("sessionFamilyId").GetGuid();
        var otherOwnedFamily = items.Single(item => !item.GetProperty("isCurrent").GetBoolean())
            .GetProperty("sessionFamilyId").GetGuid();

        Assert.Equal(
            HttpStatusCode.Conflict,
            (await secondClient.DeleteAsync($"/api/tenant/me/sessions/{currentFamily}")).StatusCode);
        Assert.Equal(
            HttpStatusCode.NotFound,
            (await otherClient.DeleteAsync($"/api/tenant/me/sessions/{otherOwnedFamily}")).StatusCode);
        Assert.Equal(
            HttpStatusCode.NoContent,
            (await secondClient.DeleteAsync($"/api/tenant/me/sessions/{otherOwnedFamily}")).StatusCode);
        using var remaining =
            JsonDocument.Parse(
                await (await secondClient.GetAsync("/api/tenant/me/sessions")).Content.ReadAsStringAsync());
        Assert.Single(remaining.RootElement.EnumerateArray());
    }

    [Fact]
    public async Task Tenant_administrator_reset_requires_same_tenant_and_forces_password_change()
    {
        await using var factory = new ApiTestFactory();
        var admin = await SeedUserAsync(factory);
        var target = await SeedMemberAsync(factory, admin.TenantId, TenantRole.Teacher);
        var crossTenantTarget = await SeedUserAsync(factory);
        using var targetClient = factory.CreateClient();
        var targetTokens = await targetClient.LoginAsTenantAsync(target.TenantId, target.Phone);
        using var adminClient = factory.CreateClient();
        adminClient.UseAccessToken(await adminClient.LoginAsTenantAsync(admin.TenantId, admin.Phone));

        var reset = await adminClient.PostAsJsonAsync(
            $"/api/tenant/members/{target.UserId}/password-reset",
            new AdministrativePasswordResetDto
            {
                TemporaryPassword = "TemporaryPassword2026",
                Reason = "Account recovery verification"
            });
        var crossTenant = await adminClient.PostAsJsonAsync(
            $"/api/tenant/members/{crossTenantTarget.UserId}/password-reset",
            new AdministrativePasswordResetDto
            {
                TemporaryPassword = "TemporaryPassword2026",
                Reason = "Must not cross tenant boundary"
            });

        Assert.Equal(HttpStatusCode.NoContent, reset.StatusCode);
        Assert.Equal(HttpStatusCode.NotFound, crossTenant.StatusCode);
        targetClient.UseAccessToken(targetTokens);
        Assert.Equal(HttpStatusCode.Unauthorized, (await targetClient.GetAsync("/api/tenant/me")).StatusCode);
        var temporaryLogin = await PostPasswordLoginAsync(targetClient, target, "TemporaryPassword2026");
        Assert.Equal(HttpStatusCode.OK, temporaryLogin.StatusCode);
        Assert.Contains("password_change_required", await temporaryLogin.Content.ReadAsStringAsync(),
            StringComparison.Ordinal);

        using var scope = factory.CreateSystemScope("Verify administrative password reset");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.True(await dbContext.Users.Where(item => item.Id == target.UserId)
            .Select(item => item.ForcePasswordChange).SingleAsync());
        Assert.True(await dbContext.AuditLogs.AnyAsync(item =>
            item.TenantId == admin.TenantId &&
            item.ActorUserId == admin.UserId &&
            item.Action == "auth.password.reset_by_administrator" &&
            item.TargetId == target.UserId.ToString()));
    }

    private static Task<HttpResponseMessage> PostPasswordLoginAsync(
        HttpClient client,
        UserSeed seed,
        string password)
    {
        return client.PostAsJsonAsync(
            "/api/tenant/auth/login/password",
            new PasswordLoginDto
            {
                Realm = AuthRealm.Tenant,
                TenantCode = seed.TenantId.ToString("N"),
                Identifier = seed.Phone,
                Password = password
            });
    }

    private static async Task<UserSeed> SeedUserAsync(ApiTestFactory factory)
    {
        var tenantId = Guid.NewGuid();
        var user = new User
        {
            Id = Guid.NewGuid(),
            Phone = $"13{Random.Shared.NextInt64(100_000_000, 1_000_000_000)}",
            Name = "Recovery test user"
        }.WithTestPassword();
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Recovery test tenant",
                Status = TenantStatus.Active
            },
            user,
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = user.Id,
                Role = TenantRole.TenantAdmin,
                Status = MembershipStatus.Active
            });
        return new UserSeed(tenantId, user.Id, user.Phone!);
    }

    private static async Task<UserSeed> SeedMemberAsync(
        ApiTestFactory factory,
        Guid tenantId,
        TenantRole role)
    {
        var user = new User
        {
            Id = Guid.NewGuid(),
            Phone = $"13{Random.Shared.NextInt64(100_000_000, 1_000_000_000)}",
            Name = "Administrative reset target"
        }.WithTestPassword();
        await factory.SeedAsync(
            user,
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = user.Id,
                Role = role,
                Status = MembershipStatus.Active
            });
        return new UserSeed(tenantId, user.Id, user.Phone!);
    }

    private sealed record UserSeed(Guid TenantId, Guid UserId, string Phone);

    private sealed class CapturingSmsProvider : ISmsProvider
    {
        public int SendCount { get; private set; }
        public string? Code { get; private set; }

        public Task<SmsProviderSendResult> SendAsync(
            SmsProviderSendRequest request,
            CancellationToken cancellationToken = default)
        {
            SendCount++;
            Code = request.Code;
            return Task.FromResult(new SmsProviderSendResult("test", "sent", "reset-message-id"));
        }
    }
}