using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Sessions;

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

public sealed class DashboardSessionAdminServiceTests
{
    /// <summary>Verifies that a viewer cannot close a session.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSessionAsync_ViewerCannotManage()
    {
        FakeSessionManager sessionManager = new();
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.CloseSessionAsync(
            CreateUser(DashboardRoles.Viewer),
            "session-1",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, sessionManager.CloseCount);
    }

    /// <summary>Verifies that an admin can close a session.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSessionAsync_AdminClosesSession()
    {
        FakeSessionManager sessionManager = new();
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.CloseSessionAsync(
            CreateUser(DashboardRoles.Admin),
            "session-1",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.Equal(1, sessionManager.CloseCount);
        Assert.Equal("session-1", sessionManager.LastClosedSessionId);
    }

    /// <summary>Verifies that closing a missing session returns a friendly error message.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSessionAsync_WhenSessionMissing_ReportsFriendlyError()
    {
        FakeSessionManager sessionManager = new()
        {
            CloseThrowsNotFound = true,
        };
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.CloseSessionAsync(
            CreateUser(DashboardRoles.Admin),
            "session-missing",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Contains("not found", result.Message, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>Verifies that a viewer cannot kill a worker.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task KillWorkerAsync_ViewerCannotManage()
    {
        FakeSessionManager sessionManager = new();
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.KillWorkerAsync(
            CreateUser(DashboardRoles.Viewer),
            "session-1",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, sessionManager.KillCount);
    }

    /// <summary>Verifies that an admin can kill a worker.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task KillWorkerAsync_AdminKillsWorker()
    {
        FakeSessionManager sessionManager = new();
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.KillWorkerAsync(
            CreateUser(DashboardRoles.Admin),
            "session-1",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.Equal(1, sessionManager.KillCount);
        Assert.Equal("session-1", sessionManager.LastKilledSessionId);

        Assert.Equal("dashboard-admin-kill", sessionManager.LastKillReason);
    }

    /// <summary>Verifies that killing a worker with a blank session ID returns failure.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task KillWorkerAsync_BlankSessionId_ReturnsFailure()
    {
        FakeSessionManager sessionManager = new();
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.KillWorkerAsync(
            CreateUser(DashboardRoles.Admin),
            "   ",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, sessionManager.KillCount);
    }

    /// <summary>
    /// <c>CloseSessionAsync</c> has the same blank-session-id guard as
    /// <c>KillWorkerAsync</c> but previously had no parallel test. Coverage was asymmetric.
    /// A guard-removal regression on the close path would slip through.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSessionAsync_BlankSessionId_ReturnsFailure()
    {
        FakeSessionManager sessionManager = new();
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.CloseSessionAsync(
            CreateUser(DashboardRoles.Admin),
            "   ",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.Equal(0, sessionManager.CloseCount);
    }

    /// <summary>Verifies that CanManage rejects unauthenticated users and viewers.</summary>
    [Fact]
    public void CanManage_RejectsUnauthenticatedAndViewer()
    {
        DashboardSessionAdminService service = CreateService(new FakeSessionManager());

        Assert.False(service.CanManage(new ClaimsPrincipal(new ClaimsIdentity())));
        Assert.False(service.CanManage(CreateUser(DashboardRoles.Viewer)));
        Assert.True(service.CanManage(CreateUser(DashboardRoles.Admin)));
    }

    /// <summary>
    /// Regression: an unexpected (non-<see cref="SessionManagerException"/>)
    /// exception from <c>CloseSessionAsync</c> — e.g. an <see cref="InvalidOperationException"/>
    /// or <see cref="IOException"/> surfaced from <c>RemoveSessionAsync</c>/<c>DisposeAsync</c> —
    /// must be converted to a friendly <see cref="DashboardSessionAdminResult.Fail(string)"/>
    /// rather than propagating raw into Blazor's error boundary.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSessionAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
    {
        FakeSessionManager sessionManager = new()
        {
            CloseThrowsUnexpected = new InvalidOperationException("unexpected"),
        };
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.CloseSessionAsync(
            CreateUser(DashboardRoles.Admin),
            "session-1",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.False(string.IsNullOrWhiteSpace(result.Message));
    }

    /// <summary>
    /// Regression: same friendly-fail contract for the Kill path.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task KillWorkerAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
    {
        FakeSessionManager sessionManager = new()
        {
            KillThrowsUnexpected = new IOException("pipe broken"),
        };
        DashboardSessionAdminService service = CreateService(sessionManager);

        DashboardSessionAdminResult result = await service.KillWorkerAsync(
            CreateUser(DashboardRoles.Admin),
            "session-1",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        Assert.False(string.IsNullOrWhiteSpace(result.Message));
    }

    /// <summary>
    /// Verifies that a successful close writes a canonical <c>dashboard-close-session</c>
    /// <see cref="AuditEvent"/> — actor, session-id target, and Success outcome — to the
    /// audit store, not only the operational log.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSessionAsync_AdminClose_WritesCanonicalAuditEvent()
    {
        FakeSessionManager sessionManager = new();
        RecordingAuditWriter auditWriter = new();
        DashboardSessionAdminService service = CreateService(sessionManager, auditWriter);

        DashboardSessionAdminResult result = await service.CloseSessionAsync(
            CreateUser(DashboardRoles.Admin),
            "session-1",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        AuditEvent audit = Assert.Single(auditWriter.Events);
        Assert.Equal(DashboardSessionAdminService.CloseSessionAction, audit.Action);
        Assert.Equal(DashboardSessionAdminService.SessionAdminCategory, audit.Category);
        Assert.Equal("session-1", audit.Target);
        Assert.Equal("tester", audit.Actor);
        Assert.Equal(AuditOutcome.Success, audit.Outcome);
    }

    /// <summary>
    /// Verifies that a successful kill writes a canonical <c>dashboard-kill-worker</c>
    /// <see cref="AuditEvent"/> to the audit store.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task KillWorkerAsync_AdminKill_WritesCanonicalAuditEvent()
    {
        FakeSessionManager sessionManager = new();
        RecordingAuditWriter auditWriter = new();
        DashboardSessionAdminService service = CreateService(sessionManager, auditWriter);

        DashboardSessionAdminResult result = await service.KillWorkerAsync(
            CreateUser(DashboardRoles.Admin),
            "session-1",
            CancellationToken.None);

        Assert.True(result.Succeeded);
        AuditEvent audit = Assert.Single(auditWriter.Events);
        Assert.Equal(DashboardSessionAdminService.KillWorkerAction, audit.Action);
        Assert.Equal("session-1", audit.Target);
        Assert.Equal(AuditOutcome.Success, audit.Outcome);
    }

    /// <summary>
    /// Verifies that an unauthorized (viewer) close attempt still writes a <c>Denied</c>
    /// audit row, so rejected destructive attempts are durably recorded.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task CloseSessionAsync_ViewerDenied_WritesDeniedAuditEvent()
    {
        FakeSessionManager sessionManager = new();
        RecordingAuditWriter auditWriter = new();
        DashboardSessionAdminService service = CreateService(sessionManager, auditWriter);

        DashboardSessionAdminResult result = await service.CloseSessionAsync(
            CreateUser(DashboardRoles.Viewer),
            "session-1",
            CancellationToken.None);

        Assert.False(result.Succeeded);
        AuditEvent audit = Assert.Single(auditWriter.Events);
        Assert.Equal(DashboardSessionAdminService.CloseSessionAction, audit.Action);
        Assert.Equal(AuditOutcome.Denied, audit.Outcome);
    }

    private static DashboardSessionAdminService CreateService(
        ISessionManager sessionManager,
        IAuditWriter? auditWriter = null)
    {
        DefaultHttpContext httpContext = new();
        httpContext.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback;

        return new DashboardSessionAdminService(
            sessionManager,
            new HttpContextAccessor { HttpContext = httpContext },
            auditWriter ?? new RecordingAuditWriter());
    }

    private static ClaimsPrincipal CreateUser(string role)
    {
        ClaimsIdentity identity = new(
            [new Claim(ClaimTypes.Name, "tester"), new Claim(ClaimTypes.Role, role)],
            DashboardAuthenticationDefaults.AuthenticationScheme,
            ClaimTypes.Name,
            ClaimTypes.Role);

        return new ClaimsPrincipal(identity);
    }

    private sealed class RecordingAuditWriter : IAuditWriter
    {
        private readonly ConcurrentQueue<AuditEvent> _events = new();

        /// <summary>Gets the audit events written through this writer, in order.</summary>
        public IReadOnlyList<AuditEvent> Events => _events.ToArray();

        /// <summary>Records the given audit event for later inspection by the test.</summary>
        /// <param name="evt">The audit event to record.</param>
        /// <param name="ct">Token to cancel the asynchronous operation.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
        {
            _events.Enqueue(evt);
            return Task.CompletedTask;
        }
    }

    private sealed class FakeSessionManager : ISessionManager
    {
        /// <summary>Gets the number of times CloseSessionAsync was invoked.</summary>
        public int CloseCount { get; private set; }

        /// <summary>Gets the number of times KillWorkerAsync was invoked.</summary>
        public int KillCount { get; private set; }

        /// <summary>Gets the last session ID passed to CloseSessionAsync.</summary>
        public string? LastClosedSessionId { get; private set; }

        /// <summary>Gets the last session ID passed to KillWorkerAsync.</summary>
        public string? LastKilledSessionId { get; private set; }

        /// <summary>Gets the last reason string passed to KillWorkerAsync.</summary>
        public string? LastKillReason { get; private set; }

        /// <summary>Gets a value indicating whether CloseSessionAsync should throw SessionNotFound.</summary>
        public bool CloseThrowsNotFound { get; init; }

        /// <summary>Gets the exception CloseSessionAsync should throw unexpectedly.</summary>
        public Exception? CloseThrowsUnexpected { get; init; }

        /// <summary>Gets the exception KillWorkerAsync should throw unexpectedly.</summary>
        public Exception? KillThrowsUnexpected { get; init; }

        /// <inheritdoc />
        public Task<GatewaySession> OpenSessionAsync(
            SessionOpenRequest request,
            string? clientIdentity,
            string? ownerKeyId,
            CancellationToken cancellationToken)
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc />
        public bool TryGetSession(
            string sessionId,
            [MaybeNullWhen(false)] out GatewaySession session)
        {
            session = null;
            return false;
        }

        /// <inheritdoc />
        public Task<WorkerCommandReply> InvokeAsync(
            string sessionId,
            WorkerCommand command,
            CancellationToken cancellationToken)
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc />
        public Task<SessionCloseResult> CloseSessionAsync(
            string sessionId,
            CancellationToken cancellationToken)
        {
            CloseCount++;
            LastClosedSessionId = sessionId;
            if (CloseThrowsNotFound)
            {
                throw new SessionManagerException(
                    SessionManagerErrorCode.SessionNotFound,
                    $"Session {sessionId} was not found.");
            }

            if (CloseThrowsUnexpected is not null)
            {
                throw CloseThrowsUnexpected;
            }

            return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
        }

        /// <inheritdoc />
        public Task<SessionCloseResult> KillWorkerAsync(
            string sessionId,
            string reason,
            CancellationToken cancellationToken)
        {
            KillCount++;
            LastKilledSessionId = sessionId;
            LastKillReason = reason;
            if (KillThrowsUnexpected is not null)
            {
                throw KillThrowsUnexpected;
            }

            return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
        }

        /// <inheritdoc />
        public Task<int> CloseExpiredLeasesAsync(
            DateTimeOffset now,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(0);
        }

        /// <inheritdoc />
        public Task ShutdownAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}
