using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;

public sealed class DashboardApiKeyManagementServiceTests
{
    [Fact]
    public async Task CreateAsync_UnauthorizedUser_DoesNotCallStore()
    {
        FakeApiKeyAdminStore adminStore = new();
        DashboardApiKeyManagementService service = CreateService(adminStore);

        DashboardApiKeyManagementResult result = await service.CreateAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            CreateRequest(),
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, adminStore.CreateCount);
    }

    [Fact]
    public async Task CreateAsync_AuthorizedUser_StoresHashOfSecretAndAudits()
    {
        FakeApiKeyAdminStore adminStore = new();
        FakeApiKeyAuditStore auditStore = new();
        FakeApiKeySecretHasher hasher = new();
        DashboardApiKeyManagementService service = CreateService(adminStore, auditStore, hasher);

        DashboardApiKeyManagementResult result = await service.CreateAsync(
            CreateAuthorizedUser(),
            CreateRequest(),
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.NotNull(result.ApiKey);
        Assert.StartsWith("mxgw_operator01_", result.ApiKey, StringComparison.Ordinal);
        string secret = result.ApiKey["mxgw_operator01_".Length..];
        Assert.Equal(secret, hasher.LastSecret);
        Assert.DoesNotContain("mxgw_operator01_", hasher.LastSecret, StringComparison.Ordinal);
        ApiKeyCreateRequest stored = Assert.Single(adminStore.CreatedRequests);
        Assert.Equal("operator01", stored.KeyId);
        Assert.Equal("Operator", stored.DisplayName);
        Assert.Contains(GatewayScopes.SessionOpen, stored.Scopes);
        Assert.Equal(["Area1/*"], stored.Constraints.BrowseSubtrees);
        Assert.Contains(auditStore.Entries, entry =>
            entry.EventType == "dashboard-create-key"
            && entry.KeyId == "operator01");
    }

    [Fact]
    public async Task RevokeAsync_UnauthorizedUser_DoesNotCallStore()
    {
        FakeApiKeyAdminStore adminStore = new();
        DashboardApiKeyManagementService service = CreateService(adminStore);

        DashboardApiKeyManagementResult result = await service.RevokeAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            "operator01",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, adminStore.RevokeCount);
    }

    [Fact]
    public async Task RevokeAsync_AuthorizedUser_RevokesAndAudits()
    {
        FakeApiKeyAdminStore adminStore = new() { RevokeResult = true };
        FakeApiKeyAuditStore auditStore = new();
        DashboardApiKeyManagementService service = CreateService(adminStore, auditStore);

        DashboardApiKeyManagementResult result = await service.RevokeAsync(
            CreateAuthorizedUser(),
            "operator01",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.Equal("operator01", adminStore.LastRevokedKeyId);
        Assert.Contains(auditStore.Entries, entry =>
            entry.EventType == "dashboard-revoke-key"
            && entry.KeyId == "operator01"
            && entry.Details == "revoked");
    }

    [Fact]
    public async Task RotateAsync_AuthorizedUser_RotatesHashAndAudits()
    {
        FakeApiKeyAdminStore adminStore = new() { RotateResult = true };
        FakeApiKeyAuditStore auditStore = new();
        FakeApiKeySecretHasher hasher = new();
        DashboardApiKeyManagementService service = CreateService(adminStore, auditStore, hasher);

        DashboardApiKeyManagementResult result = await service.RotateAsync(
            CreateAuthorizedUser(),
            "operator01",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.NotNull(result.ApiKey);
        Assert.StartsWith("mxgw_operator01_", result.ApiKey, StringComparison.Ordinal);
        Assert.Equal(hasher.HashSecret(hasher.LastSecret!), adminStore.LastRotatedSecretHash);
        Assert.Contains(auditStore.Entries, entry =>
            entry.EventType == "dashboard-rotate-key"
            && entry.KeyId == "operator01"
            && entry.Details == "rotated");
    }

    [Fact]
    public async Task DeleteAsync_UnauthorizedUser_DoesNotCallStore()
    {
        FakeApiKeyAdminStore adminStore = new();
        DashboardApiKeyManagementService service = CreateService(adminStore);

        DashboardApiKeyManagementResult result = await service.DeleteAsync(
            new ClaimsPrincipal(new ClaimsIdentity()),
            "operator01",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, adminStore.DeleteCount);
    }

    [Fact]
    public async Task DeleteAsync_AuthorizedUser_DeletesRevokedKeyAndAudits()
    {
        FakeApiKeyAdminStore adminStore = new() { DeleteResult = true };
        FakeApiKeyAuditStore auditStore = new();
        DashboardApiKeyManagementService service = CreateService(adminStore, auditStore);

        DashboardApiKeyManagementResult result = await service.DeleteAsync(
            CreateAuthorizedUser(),
            "operator01",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.Equal("operator01", adminStore.LastDeletedKeyId);
        Assert.Contains(auditStore.Entries, entry =>
            entry.EventType == "dashboard-delete-key"
            && entry.KeyId == "operator01"
            && entry.Details == "deleted");
    }

    [Fact]
    public async Task DeleteAsync_WhenStoreRefuses_ReportsFriendlyError()
    {
        FakeApiKeyAdminStore adminStore = new() { DeleteResult = false };
        DashboardApiKeyManagementService service = CreateService(adminStore);

        DashboardApiKeyManagementResult result = await service.DeleteAsync(
            CreateAuthorizedUser(),
            "operator01",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Contains("Revoke", result.Message, StringComparison.Ordinal);
    }

    /// <summary>
    ///     Server-004 regression: the dashboard create path must reject a request
    ///     carrying a non-canonical scope string rather than persisting a key whose
    ///     scope the authorization resolver never matches.
    /// </summary>
    [Fact]
    public async Task CreateAsync_UnknownScope_DoesNotCallStore()
    {
        FakeApiKeyAdminStore adminStore = new();
        DashboardApiKeyManagementService service = CreateService(adminStore);

        DashboardApiKeyManagementRequest request = CreateRequest() with
        {
            Scopes = new HashSet<string>(
                [GatewayScopes.SessionOpen, "invoke", "metadata"],
                StringComparer.Ordinal),
        };

        DashboardApiKeyManagementResult result = await service.CreateAsync(
            CreateAuthorizedUser(),
            request,
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, adminStore.CreateCount);
    }

    private static DashboardApiKeyManagementService CreateService(
        FakeApiKeyAdminStore? adminStore = null,
        FakeApiKeyAuditStore? auditStore = null,
        FakeApiKeySecretHasher? hasher = null)
    {
        DefaultHttpContext httpContext = new();
        httpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback;

        return new DashboardApiKeyManagementService(
            new DashboardApiKeyAuthorization(),
            adminStore ?? new FakeApiKeyAdminStore(),
            auditStore ?? new FakeApiKeyAuditStore(),
            hasher ?? new FakeApiKeySecretHasher(),
            new HttpContextAccessor { HttpContext = httpContext });
    }

    private static DashboardApiKeyManagementRequest CreateRequest()
    {
        return new DashboardApiKeyManagementRequest(
            KeyId: "operator01",
            DisplayName: "Operator",
            Scopes: new HashSet<string>([GatewayScopes.SessionOpen], StringComparer.Ordinal),
            Constraints: ApiKeyConstraints.Empty with
            {
                BrowseSubtrees = ["Area1/*"],
            });
    }

    private static ClaimsPrincipal CreateAuthorizedUser()
    {
        ClaimsIdentity identity = new(
            [new Claim(ClaimTypes.Role, DashboardRoles.Admin)],
            DashboardAuthenticationDefaults.AuthenticationScheme,
            ClaimTypes.Name,
            ClaimTypes.Role);

        return new ClaimsPrincipal(identity);
    }

    private sealed class FakeApiKeyAdminStore : IApiKeyAdminStore
    {
        public int CreateCount { get; private set; }

        public int RevokeCount { get; private set; }

        public int DeleteCount { get; private set; }

        public bool RevokeResult { get; init; }

        public bool RotateResult { get; init; }

        public bool DeleteResult { get; init; }

        public string? LastRevokedKeyId { get; private set; }

        public string? LastDeletedKeyId { get; private set; }

        public byte[]? LastRotatedSecretHash { get; private set; }

        public List<ApiKeyCreateRequest> CreatedRequests { get; } = [];

        public Task CreateAsync(ApiKeyCreateRequest request, CancellationToken cancellationToken)
        {
            CreateCount++;
            CreatedRequests.Add(request);
            return Task.CompletedTask;
        }

        public Task<IReadOnlyList<ApiKeyRecord>> ListAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult<IReadOnlyList<ApiKeyRecord>>([]);
        }

        public Task<bool> RevokeAsync(
            string keyId,
            DateTimeOffset revokedUtc,
            CancellationToken cancellationToken)
        {
            RevokeCount++;
            LastRevokedKeyId = keyId;
            return Task.FromResult(RevokeResult);
        }

        public Task<bool> RotateAsync(
            string keyId,
            byte[] secretHash,
            DateTimeOffset rotatedUtc,
            CancellationToken cancellationToken)
        {
            LastRotatedSecretHash = secretHash;
            return Task.FromResult(RotateResult);
        }

        public Task<bool> DeleteAsync(string keyId, CancellationToken cancellationToken)
        {
            DeleteCount++;
            LastDeletedKeyId = keyId;
            return Task.FromResult(DeleteResult);
        }
    }

    private sealed class FakeApiKeyAuditStore : IApiKeyAuditStore
    {
        public List<ApiKeyAuditEntry> Entries { get; } = [];

        public Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken cancellationToken)
        {
            Entries.Add(entry);
            return Task.CompletedTask;
        }

        public Task<IReadOnlyList<ApiKeyAuditRecord>> ListRecentAsync(
            int count,
            CancellationToken cancellationToken)
        {
            return Task.FromResult<IReadOnlyList<ApiKeyAuditRecord>>([]);
        }
    }

    private sealed class FakeApiKeySecretHasher : IApiKeySecretHasher
    {
        public string? LastSecret { get; private set; }

        public byte[] HashSecret(string secret)
        {
            LastSecret = secret;
            return System.Text.Encoding.UTF8.GetBytes($"hash:{secret}");
        }
    }
}
