// Port of Go server/events_test.go — auth error advisory publication tests.
// Go reference: golang/nats-server/server/events.go:2631 sendAuthErrorEvent.
//
// Tests cover: SendAuthErrorEvent counter, enqueue behaviour, record field
// preservation, SendConnectEvent, SendDisconnectEvent, and the supporting
// detail record types AuthErrorDetail, ConnectEventDetail, DisconnectEventDetail.

using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server;
using NATS.Server.Events;
using NATS.Server.TestUtilities;

namespace NATS.Server.Monitoring.Tests.Events;

/// <summary>
/// Tests for <see cref="InternalEventSystem.SendAuthErrorEvent"/>,
/// <see cref="InternalEventSystem.SendConnectEvent"/>,
/// <see cref="InternalEventSystem.SendDisconnectEvent"/>, and the three
/// companion detail record types.
/// Go reference: events_test.go TestSystemAccountDisconnectBadLogin,
/// TestSystemAccountNewConnection.
/// </summary>
public class AuthErrorEventTests : IAsyncLifetime
{
    private NatsServer _server = null!;
    private int _port;

    public async Task InitializeAsync()
    {
        _port = TestPortAllocator.GetFreePort();
        _server = new NatsServer(new NatsOptions { Port = _port }, NullLoggerFactory.Instance);
        _ = _server.StartAsync(CancellationToken.None);
        await _server.WaitForReadyAsync();
    }

    public async Task DisposeAsync()
    {
        await _server.ShutdownAsync();
        _server.Dispose();
    }


    // ========================================================================
    // AuthErrorEventCount
    // Go reference: events.go:2631 sendAuthErrorEvent — counter per advisory
    // ========================================================================

    /// <summary>
    /// AuthErrorEventCount starts at zero before any advisories are sent.
    /// Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
    /// </summary>
    [Fact]
    public void AuthErrorEventCount_StartsAtZero()
    {
        // Go reference: events_test.go TestSystemAccountDisconnectBadLogin — no events at startup.
        var es = _server.EventSystem!;
        es.AuthErrorEventCount.ShouldBe(0L);
    }

    /// <summary>
    /// Calling SendAuthErrorEvent once increments the counter to 1.
    /// Go reference: events.go:2631 sendAuthErrorEvent — each call is one advisory.
    /// </summary>
    [Fact]
    public void SendAuthErrorEvent_IncrementsCounter()
    {
        // Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
        var es = _server.EventSystem!;
        var detail = new AuthErrorDetail(
            ClientId: 42,
            RemoteAddress: "127.0.0.1:5000",
            AccountName: "$G",
            UserName: "alice",
            Reason: "Authorization Violation",
            OccurredAt: DateTime.UtcNow);

        es.SendAuthErrorEvent(_server.ServerId, detail);

        es.AuthErrorEventCount.ShouldBe(1L);
    }

    /// <summary>
    /// Each call to SendAuthErrorEvent enqueues a message (counter grows by one per call).
    /// Go reference: events.go:2687 sendInternalMsg — advisory is always enqueued.
    /// </summary>
    [Fact]
    public void SendAuthErrorEvent_EnqueuesMessage()
    {
        // Go reference: events.go sendAuthErrorEvent publishes via sendInternalMsg.
        var es = _server.EventSystem!;
        var detail = new AuthErrorDetail(
            ClientId: 7,
            RemoteAddress: "10.0.0.1:4222",
            AccountName: null,
            UserName: null,
            Reason: "Authentication Timeout",
            OccurredAt: DateTime.UtcNow);

        var before = es.AuthErrorEventCount;
        es.SendAuthErrorEvent(_server.ServerId, detail);
        var after = es.AuthErrorEventCount;

        // The counter increment is the observable side-effect of the enqueue path.
        (after - before).ShouldBe(1L);
    }

    /// <summary>
    /// Sending multiple auth error events increments the counter for each.
    /// Go reference: events.go:2631 sendAuthErrorEvent — cumulative count.
    /// </summary>
    [Fact]
    public void AuthErrorEventCount_MultipleSends_Incremented()
    {
        // Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
        var es = _server.EventSystem!;
        var detail = new AuthErrorDetail(
            ClientId: 1,
            RemoteAddress: "192.168.1.1:9999",
            AccountName: "myacc",
            UserName: "bob",
            Reason: "Bad credentials",
            OccurredAt: DateTime.UtcNow);

        var before = es.AuthErrorEventCount;

        const int count = 5;
        for (var i = 0; i < count; i++)
            es.SendAuthErrorEvent(_server.ServerId, detail);

        (es.AuthErrorEventCount - before).ShouldBe(count);
    }

    // ========================================================================
    // SendConnectEvent
    // Go reference: events.go postConnectEvent / sendConnect
    // ========================================================================

    /// <summary>
    /// SendConnectEvent enqueues a message without throwing.
    /// Go reference: events.go postConnectEvent — advisory fired on client connect.
    /// </summary>
    [Fact]
    public void SendConnectEvent_EnqueuesMessage()
    {
        // Go reference: events_test.go TestSystemAccountNewConnection.
        var es = _server.EventSystem!;
        var detail = new ConnectEventDetail(
            ClientId: 10,
            RemoteAddress: "127.0.0.1:6000",
            AccountName: "$G",
            UserName: "user1",
            ConnectedAt: DateTime.UtcNow);

        var ex = Record.Exception(() => es.SendConnectEvent(_server.ServerId, detail));
        ex.ShouldBeNull();
    }

    // ========================================================================
    // SendDisconnectEvent
    // Go reference: events.go postDisconnectEvent / sendDisconnect
    // ========================================================================

    /// <summary>
    /// SendDisconnectEvent enqueues a message without throwing.
    /// Go reference: events.go postDisconnectEvent — advisory fired on client disconnect.
    /// </summary>
    [Fact]
    public void SendDisconnectEvent_EnqueuesMessage()
    {
        // Go reference: events_test.go TestSystemAccountNewConnection (disconnect part).
        var es = _server.EventSystem!;
        var detail = new DisconnectEventDetail(
            ClientId: 20,
            RemoteAddress: "127.0.0.1:7000",
            AccountName: "$G",
            Reason: "Client Closed",
            DisconnectedAt: DateTime.UtcNow);

        var ex = Record.Exception(() => es.SendDisconnectEvent(_server.ServerId, detail));
        ex.ShouldBeNull();
    }

    // ========================================================================
    // AuthErrorDetail record
    // ========================================================================

    /// <summary>
    /// AuthErrorDetail preserves all fields passed to its constructor.
    /// Go reference: events.go:2631 — all client fields captured in the advisory.
    /// </summary>
    [Fact]
    public void AuthErrorDetail_PreservesAllFields()
    {
        // Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
        var now = DateTime.UtcNow;
        var detail = new AuthErrorDetail(
            ClientId: 99,
            RemoteAddress: "10.0.0.2:1234",
            AccountName: "test-account",
            UserName: "testuser",
            Reason: "Authorization Violation",
            OccurredAt: now);

        detail.ClientId.ShouldBe(99UL);
        detail.RemoteAddress.ShouldBe("10.0.0.2:1234");
        detail.AccountName.ShouldBe("test-account");
        detail.UserName.ShouldBe("testuser");
        detail.Reason.ShouldBe("Authorization Violation");
        detail.OccurredAt.ShouldBe(now);
    }

    /// <summary>
    /// AuthErrorDetail accepts a non-empty Reason (the key advisory field).
    /// Go reference: events.go:2631 sendAuthErrorEvent — reason is always set.
    /// </summary>
    [Fact]
    public void AuthErrorDetail_ReasonRequired()
    {
        // Go reference: events_test.go TestSystemAccountDisconnectBadLogin — reason distinguishes error types.
        var detail = new AuthErrorDetail(
            ClientId: 1,
            RemoteAddress: "127.0.0.1:0",
            AccountName: null,
            UserName: null,
            Reason: "Authentication Timeout",
            OccurredAt: DateTime.UtcNow);

        detail.Reason.ShouldNotBeNullOrEmpty();
        detail.Reason.ShouldBe("Authentication Timeout");
    }

    // ========================================================================
    // ConnectEventDetail record
    // ========================================================================

    /// <summary>
    /// ConnectEventDetail preserves all constructor fields.
    /// Go reference: events.go postConnectEvent — all fields captured on connect.
    /// </summary>
    [Fact]
    public void ConnectEventDetail_PreservesFields()
    {
        // Go reference: events_test.go TestSystemAccountNewConnection.
        var connectedAt = new DateTime(2026, 2, 25, 10, 0, 0, DateTimeKind.Utc);
        var detail = new ConnectEventDetail(
            ClientId: 55,
            RemoteAddress: "192.168.0.5:8080",
            AccountName: "prod-account",
            UserName: "svc-user",
            ConnectedAt: connectedAt);

        detail.ClientId.ShouldBe(55UL);
        detail.RemoteAddress.ShouldBe("192.168.0.5:8080");
        detail.AccountName.ShouldBe("prod-account");
        detail.UserName.ShouldBe("svc-user");
        detail.ConnectedAt.ShouldBe(connectedAt);
    }

    // ========================================================================
    // DisconnectEventDetail record
    // ========================================================================

    /// <summary>
    /// DisconnectEventDetail preserves all constructor fields.
    /// Go reference: events.go postDisconnectEvent — all fields captured on disconnect.
    /// </summary>
    [Fact]
    public void DisconnectEventDetail_PreservesFields()
    {
        // Go reference: events_test.go TestSystemAccountNewConnection (disconnect part).
        var disconnectedAt = new DateTime(2026, 2, 25, 11, 0, 0, DateTimeKind.Utc);
        var detail = new DisconnectEventDetail(
            ClientId: 77,
            RemoteAddress: "172.16.0.3:3000",
            AccountName: "staging-account",
            Reason: "Slow Consumer",
            DisconnectedAt: disconnectedAt);

        detail.ClientId.ShouldBe(77UL);
        detail.RemoteAddress.ShouldBe("172.16.0.3:3000");
        detail.AccountName.ShouldBe("staging-account");
        detail.Reason.ShouldBe("Slow Consumer");
        detail.DisconnectedAt.ShouldBe(disconnectedAt);
    }
}
