using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class AuthSessionLifecycleTests
{
    [Fact]
    public async Task Active_cache_falls_back_to_postgres_when_redis_is_unavailable()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["ConnectionStrings:Redis"] =
                "localhost:6399,connectTimeout=200,syncTimeout=200,asyncTimeout=200,abortConnect=false",
            ["Security:AuthorizationCache:Mode"] = "Active"
        });
        var seed = await SeedActiveMemberAsync(factory);
        var tokens = await IssueAsync(factory, seed);
        Assert.True(TryLocate(factory, tokens.RefreshToken, out var locator));
        using var scope = factory.CreateSystemScope("Validate PostgreSQL authorization fallback");
        Assert.NotNull(await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
            .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
    }

    [Fact]
    public async Task Active_redis_cache_rejects_disabled_membership_on_the_next_validation()
    {
        var redis = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS");
        if (string.IsNullOrWhiteSpace(redis)) return;

        var commandRecorder = new RecordingDbCommandInterceptor();
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["ConnectionStrings:Redis"] = redis,
            ["Security:AuthorizationCache:Mode"] = "Active"
        }, dbCommandInterceptor: commandRecorder);
        var seed = await SeedActiveMemberAsync(factory);
        var tokens = await IssueAsync(factory, seed);
        Assert.True(TryLocate(factory, tokens.RefreshToken, out var locator));

        using (var scope = factory.CreateSystemScope("Warm Redis access security state"))
        {
            Assert.NotNull(await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
                .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
            var cached = await scope.ServiceProvider.GetRequiredService<IAccessSecurityCache>()
                .GetAsync(
                    new AccessSecurityCacheLookup(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
            Assert.True(cached?.Complete);
        }

        commandRecorder.Reset();
        using (var scope = factory.CreateSystemScope("Validate hot Redis access security state"))
        {
            Assert.NotNull(await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
                .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
        }

        Assert.Empty(commandRecorder.Snapshot());

        using (var scope = factory.CreateSystemScope("Disable membership and invalidate Redis"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var membership = await dbContext.TenantMemberships.SingleAsync(item =>
                item.TenantId == seed.TenantId && item.UserId == seed.UserId);
            membership.Status = MembershipStatus.Disabled;
            await dbContext.SaveChangesAsync();
            await scope.ServiceProvider.GetRequiredService<IAuthorizationStateInvalidator>()
                .InvalidateMembershipAsync(seed.TenantId, seed.UserId);
        }

        using (var scope = factory.CreateSystemScope("Validate disabled cached membership"))
        {
            Assert.Null(await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
                .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
        }
    }

    [Fact]
    public async Task Authorization_version_bypasses_stale_local_permission_snapshot_after_revocation()
    {
        var redis = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS");
        if (string.IsNullOrWhiteSpace(redis)) return;
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["ConnectionStrings:Redis"] = redis,
            ["Security:AuthorizationCache:Mode"] = "Active"
        });
        var seed = await SeedActiveMemberAsync(factory);
        var roleId = Guid.NewGuid();
        const string permissionCode = "tenant:auth-cache:manage";
        await factory.SeedAsync(new BackendPermission
        {
            Code = permissionCode,
            Name = "Authorization cache test permission",
            Area = BackendPermissionArea.Tenant,
            PermissionModuleCode =
                PermissionModuleCatalog.ResolvePermissionModuleCode(BackendPermissions.TenantSettingsManage)
        });
        using (var scope = factory.CreateSystemScope("Seed versioned tenant permission"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            dbContext.AuthorizationScopeVersions.Add(new AuthorizationScopeVersion
            {
                Realm = AuthRealm.Tenant,
                TenantId = seed.TenantId
            });
            await dbContext.SaveChangesAsync();
            dbContext.TenantBackendRoles.Add(new TenantBackendRole
            {
                Id = roleId,
                TenantId = seed.TenantId,
                Code = "versioned-cache-role",
                Name = "Versioned cache role"
            });
            dbContext.TenantBackendRolePermissions.Add(new TenantBackendRolePermission
            {
                TenantId = seed.TenantId,
                RoleId = roleId,
                PermissionCode = permissionCode
            });
            dbContext.TenantBackendUserRoles.Add(new TenantBackendUserRole
            {
                TenantId = seed.TenantId,
                UserId = seed.UserId,
                RoleId = roleId
            });
            await dbContext.SaveChangesAsync();
        }

        var tokens = await IssueAsync(factory, seed);
        Assert.True(TryLocate(factory, tokens.RefreshToken, out var locator));
        AuthSessionValidationResult validated;
        using (var scope = factory.CreateSystemScope("Warm session state for permission snapshot"))
        {
            validated = Assert.IsType<AuthSessionValidationResult>(await scope.ServiceProvider
                .GetRequiredService<IAuthSessionStore>()
                .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
        }

        Assert.True((await LoadAccessAsync(factory, seed, validated)).HasTenantPermission(permissionCode));

        using (var scope = factory.CreateSystemScope("Revoke permission and publish durable version"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var binding = await dbContext.TenantBackendRolePermissions.SingleAsync(item =>
                item.TenantId == seed.TenantId && item.RoleId == roleId && item.PermissionCode == permissionCode);
            dbContext.Remove(binding);
            await dbContext.SaveChangesAsync();
            await scope.ServiceProvider.GetRequiredService<IAuthorizationCacheInvalidationProcessor>()
                .ProcessPendingAsync();
        }

        using (var scope = factory.CreateSystemScope("Reload session authorization version"))
        {
            validated = Assert.IsType<AuthSessionValidationResult>(await scope.ServiceProvider
                .GetRequiredService<IAuthSessionStore>()
                .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
        }

        Assert.False((await LoadAccessAsync(factory, seed, validated)).HasTenantPermission(permissionCode));
    }

    private static async Task<CurrentAccessSnapshot> LoadAccessAsync(
        ApiTestFactory factory, SessionSeed seed, AuthSessionValidationResult validated)
    {
        using var scope = factory.Services.CreateScope();
        scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
            .Initialize(seed.TenantId, null, TenantResolutionSource.Jwt);
        ((CurrentUser)scope.ServiceProvider.GetRequiredService<ICurrentUser>()).Load(new ClaimsPrincipal(
            new ClaimsIdentity([new Claim(TikuClaimTypes.UserId, seed.UserId.ToString())], "integration")));
        scope.ServiceProvider.GetRequiredService<IRequestSecurityState>().SetValidatedSession(validated);
        return await scope.ServiceProvider.GetRequiredService<ICurrentAccessContext>().GetAsync();
    }

    [Fact]
    public async Task Refresh_rotation_creates_a_child_and_replay_revokes_the_entire_family()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedActiveMemberAsync(factory);
        var original = await IssueAsync(factory, seed);

        AuthTokenPair rotated;
        using (var scope = factory.CreateSystemScope("Rotate refresh token"))
        {
            rotated = await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
                .RotateAsync(original.RefreshToken, "127.0.0.1", "integration-test");
        }

        Assert.True(TryLocate(factory, original.RefreshToken, out var originalLocator));
        Assert.True(TryLocate(factory, rotated.RefreshToken, out var rotatedLocator));

        using (var scope = factory.CreateSystemScope("Verify rotated session lineage"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var sessions = await dbContext.AuthSessions
                .Where(session => session.Id == originalLocator.SessionId || session.Id == rotatedLocator.SessionId)
                .OrderBy(session => session.ParentSessionId == null ? 0 : 1)
                .ToListAsync();

            Assert.Equal(2, sessions.Count);
            Assert.Equal(originalLocator.SessionId, sessions[0].Id);
            Assert.Equal(rotatedLocator.SessionId, sessions[0].ReplacedBySessionId);
            Assert.Equal("rotated", sessions[0].RevokedReason);
            Assert.Equal(originalLocator.SessionId, sessions[1].ParentSessionId);
            Assert.Equal(sessions[0].TokenFamilyId, sessions[1].TokenFamilyId);
        }

        using (var scope = factory.CreateSystemScope("Replay rotated refresh token"))
        {
            await Assert.ThrowsAsync<SessionRevokedException>(() =>
                scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
                    .RotateAsync(original.RefreshToken, null, null));
        }

        using (var scope = factory.CreateSystemScope("Verify refresh family revocation"))
        {
            var store = scope.ServiceProvider.GetRequiredService<IAuthSessionStore>();
            var validation = await store.ValidateAccessSessionAsync(
                rotatedLocator.SessionId,
                seed.UserId,
                AuthRealm.Tenant,
                seed.TenantId);
            Assert.Null(validation);

            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var family = await dbContext.AuthSessions
                .Where(session => session.TokenFamilyId == originalLocator.SessionId)
                .ToListAsync();
            Assert.All(family, session => Assert.NotNull(session.RevokedAt));
            Assert.Contains(family, session => session.RevokedReason == "refresh_token_reuse");
        }
    }

    [Fact]
    public async Task Concurrent_refresh_allows_only_one_rotation_and_revokes_the_replayed_family()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedActiveMemberAsync(factory);
        var original = await IssueAsync(factory, seed);
        Assert.True(TryLocate(factory, original.RefreshToken, out var originalLocator));

        using var firstScope = factory.CreateSystemScope("First concurrent refresh");
        using var secondScope = factory.CreateSystemScope("Second concurrent refresh");
        var first = TryRotateAsync(
            firstScope.ServiceProvider.GetRequiredService<IAuthSessionStore>(),
            original.RefreshToken);
        var second = TryRotateAsync(
            secondScope.ServiceProvider.GetRequiredService<IAuthSessionStore>(),
            original.RefreshToken);
        var results = await Task.WhenAll(first, second);

        Assert.Single(results, result => result is not null);
        Assert.Single(results, result => result is null);

        using var verificationScope = factory.CreateSystemScope("Verify concurrent refresh family");
        var dbContext = verificationScope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var family = await dbContext.AuthSessions
            .Where(session => session.TokenFamilyId == originalLocator.SessionId)
            .ToListAsync();
        Assert.Equal(2, family.Count);
        Assert.All(family, session => Assert.NotNull(session.RevokedAt));
        Assert.Contains(family, session => session.RevokedReason == "refresh_token_reuse");
    }

    [Fact]
    public async Task Access_session_fails_immediately_after_membership_is_disabled()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedActiveMemberAsync(factory);
        var tokens = await IssueAsync(factory, seed);
        Assert.True(TryLocate(factory, tokens.RefreshToken, out var locator));

        using (var scope = factory.CreateSystemScope("Disable tenant membership"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var membership = await dbContext.TenantMemberships.SingleAsync(item =>
                item.TenantId == seed.TenantId && item.UserId == seed.UserId);
            membership.Status = MembershipStatus.Disabled;
            await dbContext.SaveChangesAsync();
        }

        using (var scope = factory.CreateSystemScope("Validate disabled membership session"))
        {
            var validation = await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
                .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId);
            Assert.Null(validation);
        }
    }

    [Fact]
    public async Task Access_session_fails_immediately_after_security_stamp_changes()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedActiveMemberAsync(factory);
        var tokens = await IssueAsync(factory, seed);
        Assert.True(TryLocate(factory, tokens.RefreshToken, out var locator));

        using (var scope = factory.CreateSystemScope("Change user security stamp"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var user = await dbContext.Users.SingleAsync(item => item.Id == seed.UserId);
            user.SecurityStamp = Guid.NewGuid().ToString("N");
            await dbContext.SaveChangesAsync();
        }

        using (var scope = factory.CreateSystemScope("Validate stale security stamp session"))
        {
            var validation = await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
                .ValidateAccessSessionAsync(locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId);
            Assert.Null(validation);
        }
    }

    [Fact]
    public async Task Tenant_session_remains_valid_after_a_backend_permission_is_revoked()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedActiveMemberAsync(factory);
        var role = new TenantBackendRole
        {
            TenantId = seed.TenantId,
            Code = "session-test-admin",
            Name = "Session test administrator"
        };
        const string permissionCode = "tenant:session-test:manage";
        await factory.SeedAsync(
            new BackendPermission
            {
                Code = permissionCode,
                Name = permissionCode,
                Area = BackendPermissionArea.Tenant,
                PermissionModuleCode = "tenant_dashboard"
            },
            role,
            new TenantBackendRolePermission
            {
                TenantId = seed.TenantId,
                RoleId = role.Id,
                PermissionCode = permissionCode
            },
            new TenantBackendUserRole
            {
                TenantId = seed.TenantId,
                UserId = seed.UserId,
                RoleId = role.Id
            });
        var tokens = await IssueAsync(factory, seed);
        Assert.True(TryLocate(factory, tokens.RefreshToken, out var locator));

        using (var scope = factory.CreateSystemScope("Revoke final backend permission"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var binding = await dbContext.TenantBackendRolePermissions.SingleAsync(item =>
                item.TenantId == seed.TenantId && item.RoleId == role.Id);
            dbContext.TenantBackendRolePermissions.Remove(binding);
            await dbContext.SaveChangesAsync();
        }

        using (var scope = factory.CreateSystemScope("Validate tenant session"))
        {
            var store = scope.ServiceProvider.GetRequiredService<IAuthSessionStore>();
            Assert.NotNull(await store.ValidateAccessSessionAsync(
                locator.SessionId, seed.UserId, AuthRealm.Tenant, seed.TenantId));
            Assert.NotNull(await store.RotateAsync(tokens.RefreshToken, null, null));
        }
    }

    private static bool TryLocate(
        ApiTestFactory factory,
        string refreshToken,
        out RefreshTokenLocator locator)
    {
        using var scope = factory.CreateSystemScope("Parse refresh token locator");
        return scope.ServiceProvider.GetRequiredService<IAuthSessionStore>()
            .TryParseRefreshToken(refreshToken, out locator);
    }

    private static async Task<AuthTokenPair?> TryRotateAsync(IAuthSessionStore store, string refreshToken)
    {
        try
        {
            return await store.RotateAsync(refreshToken, null, null);
        }
        catch (SessionRevokedException)
        {
            return null;
        }
    }

    private static async Task<AuthTokenPair> IssueAsync(
        ApiTestFactory factory,
        SessionSeed seed)
    {
        using var scope = factory.CreateSystemScope("Issue authentication session");
        return await scope.ServiceProvider.GetRequiredService<IAuthSessionStore>().IssueAsync(
            new AuthSessionIssueRequest(
                seed.UserId,
                seed.Phone,
                null,
                seed.SecurityStamp,
                AuthRealm.Tenant,
                seed.TenantId,
                "integration-test",
                "127.0.0.1",
                "integration-test"));
    }

    private static async Task<SessionSeed> SeedActiveMemberAsync(ApiTestFactory factory)
    {
        var tenantId = Guid.NewGuid();
        var user = new User
        {
            Id = Guid.NewGuid(),
            Phone = $"13{Random.Shared.Next(100_000_000, 1_000_000_000)}",
            Name = "Session lifecycle user"
        };
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Session lifecycle tenant",
                Status = TenantStatus.Active
            },
            user,
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = user.Id,
                Role = TenantRole.Student,
                Status = MembershipStatus.Active
            });

        return new SessionSeed(tenantId, user.Id, user.Phone, user.SecurityStamp!);
    }

    private sealed record SessionSeed(Guid TenantId, Guid UserId, string Phone, string SecurityStamp);
}