// Port of Go server/accounts_test.go — account routing, limits, and import/export parity tests.
// Reference: golang/nats-server/server/accounts_test.go

using NATS.Server.Auth;
using NATS.Server.Imports;

namespace NATS.Server.Tests.Auth;

/// <summary>
/// Parity tests ported from Go server/accounts_test.go exercising account
/// route mappings, connection limits, import/export cycle detection,
/// system account, and JetStream resource limits.
/// </summary>
public class AccountGoParityTests
{
    // ========================================================================
    // TestAccountBasicRouteMapping
    // Go reference: accounts_test.go:TestAccountBasicRouteMapping
    // ========================================================================

    [Fact]
    public void BasicRouteMapping_SubjectIsolation()
    {
        // Go: TestAccountBasicRouteMapping — messages are isolated to accounts.
        // Different accounts have independent subscription namespaces.
        using var accA = new Account("A");
        using var accB = new Account("B");

        // Add subscriptions to account A's SubList
        var subA = new Subscriptions.Subscription { Subject = "foo", Sid = "1" };
        accA.SubList.Insert(subA);

        // Account B should not see account A's subscriptions
        var resultB = accB.SubList.Match("foo");
        resultB.PlainSubs.Length.ShouldBe(0);

        // Account A should see its own subscription
        var resultA = accA.SubList.Match("foo");
        resultA.PlainSubs.Length.ShouldBe(1);
        resultA.PlainSubs[0].ShouldBe(subA);
    }

    // ========================================================================
    // TestAccountWildcardRouteMapping
    // Go reference: accounts_test.go:TestAccountWildcardRouteMapping
    // ========================================================================

    [Fact]
    public void WildcardRouteMapping_PerAccountMatching()
    {
        // Go: TestAccountWildcardRouteMapping — wildcards work per-account.
        using var acc = new Account("TEST");

        var sub1 = new Subscriptions.Subscription { Subject = "orders.*", Sid = "1" };
        var sub2 = new Subscriptions.Subscription { Subject = "orders.>", Sid = "2" };
        acc.SubList.Insert(sub1);
        acc.SubList.Insert(sub2);

        var result = acc.SubList.Match("orders.new");
        result.PlainSubs.Length.ShouldBe(2);

        var result2 = acc.SubList.Match("orders.new.item");
        result2.PlainSubs.Length.ShouldBe(1); // only "orders.>" matches
        result2.PlainSubs[0].ShouldBe(sub2);
    }

    // ========================================================================
    // Connection limits
    // Go reference: accounts_test.go:TestAccountConnsLimitExceededAfterUpdate
    // ========================================================================

    [Fact]
    public void ConnectionLimit_ExceededAfterUpdate()
    {
        // Go: TestAccountConnsLimitExceededAfterUpdate — reducing max connections
        // below current count prevents new connections.
        using var acc = new Account("TEST") { MaxConnections = 5 };

        // Add 5 clients
        for (ulong i = 1; i <= 5; i++)
            acc.AddClient(i).ShouldBeTrue();

        acc.ClientCount.ShouldBe(5);

        // 6th client should fail
        acc.AddClient(6).ShouldBeFalse();
    }

    [Fact]
    public void ConnectionLimit_RemoveAllowsNew()
    {
        // Go: removing a client frees a slot.
        using var acc = new Account("TEST") { MaxConnections = 2 };

        acc.AddClient(1).ShouldBeTrue();
        acc.AddClient(2).ShouldBeTrue();
        acc.AddClient(3).ShouldBeFalse();

        acc.RemoveClient(1);
        acc.AddClient(3).ShouldBeTrue();
    }

    [Fact]
    public void ConnectionLimit_ZeroMeansUnlimited()
    {
        // Go: MaxConnections=0 means unlimited.
        using var acc = new Account("TEST") { MaxConnections = 0 };

        for (ulong i = 1; i <= 100; i++)
            acc.AddClient(i).ShouldBeTrue();

        acc.ClientCount.ShouldBe(100);
    }

    // ========================================================================
    // Subscription limits
    // Go reference: accounts_test.go TestAccountUserSubPermsWithQueueGroups
    // ========================================================================

    [Fact]
    public void SubscriptionLimit_Enforced()
    {
        // Go: TestAccountUserSubPermsWithQueueGroups — subscription count limits.
        using var acc = new Account("TEST") { MaxSubscriptions = 3 };

        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.IncrementSubscriptions().ShouldBeFalse();

        acc.SubscriptionCount.ShouldBe(3);
    }

    [Fact]
    public void SubscriptionLimit_DecrementAllowsNew()
    {
        using var acc = new Account("TEST") { MaxSubscriptions = 2 };

        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.IncrementSubscriptions().ShouldBeFalse();

        acc.DecrementSubscriptions();
        acc.IncrementSubscriptions().ShouldBeTrue();
    }

    // ========================================================================
    // System account
    // Go reference: events_test.go:TestSystemAccountNewConnection
    // ========================================================================

    [Fact]
    public void SystemAccount_IsSystemAccountFlag()
    {
        // Go: TestSystemAccountNewConnection — system account identification.
        using var sysAcc = new Account(Account.SystemAccountName) { IsSystemAccount = true };
        using var globalAcc = new Account(Account.GlobalAccountName);

        sysAcc.IsSystemAccount.ShouldBeTrue();
        sysAcc.Name.ShouldBe("$SYS");

        globalAcc.IsSystemAccount.ShouldBeFalse();
        globalAcc.Name.ShouldBe("$G");
    }

    // ========================================================================
    // Import/Export cycle detection
    // Go reference: accounts_test.go — addServiceImport with checkForImportCycle
    // ========================================================================

    [Fact]
    public void ImportExport_DirectCycleDetected()
    {
        // Go: cycle detection prevents A importing from B when B imports from A.
        using var accA = new Account("A");
        using var accB = new Account("B");

        accA.AddServiceExport("svc.a", ServiceResponseType.Singleton, [accB]);
        accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]);

        // A imports from B
        accA.AddServiceImport(accB, "from.b", "svc.b");

        // B importing from A would create a cycle: B -> A -> B
        var ex = Should.Throw<InvalidOperationException>(() =>
            accB.AddServiceImport(accA, "from.a", "svc.a"));
        ex.Message.ShouldContain("cycle");
    }

    [Fact]
    public void ImportExport_IndirectCycleDetected()
    {
        // Go: indirect cycles through A -> B -> C -> A are detected.
        using var accA = new Account("A");
        using var accB = new Account("B");
        using var accC = new Account("C");

        accA.AddServiceExport("svc.a", ServiceResponseType.Singleton, [accC]);
        accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]);
        accC.AddServiceExport("svc.c", ServiceResponseType.Singleton, [accB]);

        // A -> B
        accA.AddServiceImport(accB, "from.b", "svc.b");
        // B -> C
        accB.AddServiceImport(accC, "from.c", "svc.c");

        // C -> A would close the cycle: C -> A -> B -> C
        var ex = Should.Throw<InvalidOperationException>(() =>
            accC.AddServiceImport(accA, "from.a", "svc.a"));
        ex.Message.ShouldContain("cycle");
    }

    [Fact]
    public void ImportExport_NoCycle_Succeeds()
    {
        // Go: linear import chain A -> B -> C is allowed.
        using var accA = new Account("A");
        using var accB = new Account("B");
        using var accC = new Account("C");

        accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]);
        accC.AddServiceExport("svc.c", ServiceResponseType.Singleton, [accB]);

        accA.AddServiceImport(accB, "from.b", "svc.b");
        accB.AddServiceImport(accC, "from.c", "svc.c");
        // No exception — linear chain is allowed.
    }

    [Fact]
    public void ImportExport_UnauthorizedAccount_Throws()
    {
        // Go: unauthorized import throws.
        using var accA = new Account("A");
        using var accB = new Account("B");
        using var accC = new Account("C");

        // B exports only to C, not A
        accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accC]);

        Should.Throw<UnauthorizedAccessException>(() =>
            accA.AddServiceImport(accB, "from.b", "svc.b"));
    }

    [Fact]
    public void ImportExport_NoExport_Throws()
    {
        // Go: importing a non-existent export throws.
        using var accA = new Account("A");
        using var accB = new Account("B");

        Should.Throw<InvalidOperationException>(() =>
            accA.AddServiceImport(accB, "from.b", "svc.nonexistent"));
    }

    // ========================================================================
    // Stream import/export
    // Go reference: accounts_test.go TestAccountBasicRouteMapping (stream exports)
    // ========================================================================

    [Fact]
    public void StreamImportExport_BasicFlow()
    {
        // Go: basic stream export from A, imported by B.
        using var accA = new Account("A");
        using var accB = new Account("B");

        accA.AddStreamExport("events.>", [accB]);
        accB.AddStreamImport(accA, "events.>", "imported.events.>");

        accB.Imports.Streams.Count.ShouldBe(1);
        accB.Imports.Streams[0].From.ShouldBe("events.>");
        accB.Imports.Streams[0].To.ShouldBe("imported.events.>");
    }

    [Fact]
    public void StreamImport_Unauthorized_Throws()
    {
        using var accA = new Account("A");
        using var accB = new Account("B");
        using var accC = new Account("C");

        accA.AddStreamExport("events.>", [accC]); // only C authorized

        Should.Throw<UnauthorizedAccessException>(() =>
            accB.AddStreamImport(accA, "events.>", "imported.>"));
    }

    [Fact]
    public void StreamImport_NoExport_Throws()
    {
        using var accA = new Account("A");
        using var accB = new Account("B");

        Should.Throw<InvalidOperationException>(() =>
            accB.AddStreamImport(accA, "nonexistent.>", "imported.>"));
    }

    // ========================================================================
    // JetStream account limits
    // Go reference: accounts_test.go (JS limits section)
    // ========================================================================

    [Fact]
    public void JetStreamLimits_MaxStreams_Enforced()
    {
        // Go: per-account JetStream stream limit.
        using var acc = new Account("TEST")
        {
            JetStreamLimits = new AccountLimits { MaxStreams = 2 },
        };

        acc.TryReserveStream().ShouldBeTrue();
        acc.TryReserveStream().ShouldBeTrue();
        acc.TryReserveStream().ShouldBeFalse();

        acc.ReleaseStream();
        acc.TryReserveStream().ShouldBeTrue();
    }

    [Fact]
    public void JetStreamLimits_MaxConsumers_Enforced()
    {
        using var acc = new Account("TEST")
        {
            JetStreamLimits = new AccountLimits { MaxConsumers = 3 },
        };

        acc.TryReserveConsumer().ShouldBeTrue();
        acc.TryReserveConsumer().ShouldBeTrue();
        acc.TryReserveConsumer().ShouldBeTrue();
        acc.TryReserveConsumer().ShouldBeFalse();
    }

    [Fact]
    public void JetStreamLimits_MaxStorage_Enforced()
    {
        using var acc = new Account("TEST")
        {
            JetStreamLimits = new AccountLimits { MaxStorage = 1024 },
        };

        acc.TrackStorageDelta(512).ShouldBeTrue();
        acc.TrackStorageDelta(512).ShouldBeTrue();
        acc.TrackStorageDelta(1).ShouldBeFalse(); // would exceed

        acc.TrackStorageDelta(-256).ShouldBeTrue(); // free some
        acc.TrackStorageDelta(256).ShouldBeTrue();
    }

    [Fact]
    public void JetStreamLimits_Unlimited_AllowsAny()
    {
        using var acc = new Account("TEST")
        {
            JetStreamLimits = AccountLimits.Unlimited,
        };

        for (int i = 0; i < 100; i++)
        {
            acc.TryReserveStream().ShouldBeTrue();
            acc.TryReserveConsumer().ShouldBeTrue();
        }

        acc.TrackStorageDelta(long.MaxValue / 2).ShouldBeTrue();
    }

    // ========================================================================
    // Account stats tracking
    // Go reference: accounts_test.go TestAccountReqMonitoring
    // ========================================================================

    [Fact]
    public void AccountStats_InboundOutbound()
    {
        // Go: TestAccountReqMonitoring — per-account message/byte stats.
        using var acc = new Account("TEST");

        acc.IncrementInbound(10, 1024);
        acc.IncrementOutbound(5, 512);

        acc.InMsgs.ShouldBe(10);
        acc.InBytes.ShouldBe(1024);
        acc.OutMsgs.ShouldBe(5);
        acc.OutBytes.ShouldBe(512);
    }

    [Fact]
    public void AccountStats_CumulativeAcrossIncrements()
    {
        using var acc = new Account("TEST");

        acc.IncrementInbound(10, 1024);
        acc.IncrementInbound(5, 512);

        acc.InMsgs.ShouldBe(15);
        acc.InBytes.ShouldBe(1536);
    }

    // ========================================================================
    // User revocation
    // Go reference: accounts_test.go TestAccountClaimsUpdatesWithServiceImports
    // ========================================================================

    [Fact]
    public void UserRevocation_RevokedBeforeIssuedAt()
    {
        // Go: TestAccountClaimsUpdatesWithServiceImports — user revocation by NKey.
        using var acc = new Account("TEST");

        acc.RevokeUser("UABC123", 1000);

        // JWT issued at 999 (before revocation) is revoked
        acc.IsUserRevoked("UABC123", 999).ShouldBeTrue();
        // JWT issued at 1000 (exactly at revocation) is revoked
        acc.IsUserRevoked("UABC123", 1000).ShouldBeTrue();
        // JWT issued at 1001 (after revocation) is NOT revoked
        acc.IsUserRevoked("UABC123", 1001).ShouldBeFalse();
    }

    [Fact]
    public void UserRevocation_WildcardRevokesAll()
    {
        using var acc = new Account("TEST");

        acc.RevokeUser("*", 500);

        acc.IsUserRevoked("ANY_USER_1", 499).ShouldBeTrue();
        acc.IsUserRevoked("ANY_USER_2", 500).ShouldBeTrue();
        acc.IsUserRevoked("ANY_USER_3", 501).ShouldBeFalse();
    }

    [Fact]
    public void UserRevocation_UnrevokedUser_NotRevoked()
    {
        using var acc = new Account("TEST");
        acc.IsUserRevoked("UNKNOWN_USER", 1000).ShouldBeFalse();
    }

    // ========================================================================
    // Remove service/stream imports
    // Go reference: accounts_test.go TestAccountRouteMappingChangesAfterClientStart
    // ========================================================================

    [Fact]
    public void RemoveServiceImport_RemovesCorrectly()
    {
        // Go: TestAccountRouteMappingChangesAfterClientStart — dynamic import removal.
        using var accA = new Account("A");
        using var accB = new Account("B");

        accB.AddServiceExport("svc.b", ServiceResponseType.Singleton, [accA]);
        accA.AddServiceImport(accB, "from.b", "svc.b");
        accA.Imports.Services.ContainsKey("from.b").ShouldBeTrue();

        accA.RemoveServiceImport("from.b").ShouldBeTrue();
        accA.Imports.Services.ContainsKey("from.b").ShouldBeFalse();
    }

    [Fact]
    public void RemoveStreamImport_RemovesCorrectly()
    {
        using var accA = new Account("A");
        using var accB = new Account("B");

        accA.AddStreamExport("events.>", [accB]);
        accB.AddStreamImport(accA, "events.>", "imported.>");
        accB.Imports.Streams.Count.ShouldBe(1);

        accB.RemoveStreamImport("events.>").ShouldBeTrue();
        accB.Imports.Streams.Count.ShouldBe(0);
    }

    [Fact]
    public void RemoveNonexistent_ReturnsFalse()
    {
        using var acc = new Account("TEST");
        acc.RemoveServiceImport("nonexistent").ShouldBeFalse();
        acc.RemoveStreamImport("nonexistent").ShouldBeFalse();
    }
}
