using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;

public sealed class AccountTests
{
    [Fact] // T:80
    public void AccountMultipleServiceImportsWithSameSubjectFromDifferentAccounts_ShouldSucceed()
    {
        var importer = Account.NewAccount("CLIENTS");
        var svcE = Account.NewAccount("SVC-E");
        var svcW = Account.NewAccount("SVC-W");

        importer.Imports.Services = new Dictionary<string, List<ServiceImportEntry>>
        {
            ["SvcReq.>"] =
            [
                new ServiceImportEntry { Account = svcE, From = "SvcReq.>", To = "SvcReq.>" },
                new ServiceImportEntry { Account = svcW, From = "SvcReq.>", To = "SvcReq.>" },
            ],
        };

        var copied = Account.NewAccount("CLIENTS");
        importer.ShallowCopy(copied);

        copied.Imports.Services.ShouldNotBeNull();
        copied.Imports.Services!.ShouldContainKey("SvcReq.>");
        copied.Imports.Services["SvcReq.>"].Count.ShouldBe(2);

        var accounts = copied.Imports.Services["SvcReq.>"]
            .Select(static si => si.Account?.Name)
            .OrderBy(static n => n)
            .ToArray();
        accounts.ShouldBe(["SVC-E", "SVC-W"]);
    }

    [Fact] // T:83
    public void AccountBasicRouteMapping_ShouldSucceed()
    {
        var acc = Account.NewAccount("global");
        acc.AddMapping("foo", "bar").ShouldBeNull();

        var (dest, mapped) = acc.SelectMappedSubject("foo");
        mapped.ShouldBeTrue();
        dest.ShouldBe("bar");

        acc.RemoveMapping("foo").ShouldBeTrue();
        var (destAfterRemove, mappedAfterRemove) = acc.SelectMappedSubject("foo");
        mappedAfterRemove.ShouldBeFalse();
        destAfterRemove.ShouldBe("foo");
    }

    [Fact] // T:84
    public void AccountWildcardRouteMapping_ShouldSucceed()
    {
        var acc = Account.NewAccount("global");
        acc.AddMapping("foo.*.*", "bar.$2.$1").ShouldBeNull();
        acc.AddMapping("bar.*.>", "baz.$1.>").ShouldBeNull();

        var (mappedDest, mapped) = acc.SelectMappedSubject("foo.1.2");
        mapped.ShouldBeTrue();
        mappedDest.ShouldBe("bar.2.1");

        var (remappedDest, remapped) = acc.SelectMappedSubject("bar.2.1");
        remapped.ShouldBeTrue();
        remappedDest.ShouldBe("baz.2.1");
    }

    [Fact] // T:85
    public void AccountRouteMappingChangesAfterClientStart_ShouldSucceed()
    {
        var acc = Account.NewAccount("global");

        var (beforeDest, beforeMapped) = acc.SelectMappedSubject("foo");
        beforeMapped.ShouldBeFalse();
        beforeDest.ShouldBe("foo");

        acc.AddMapping("foo", "bar").ShouldBeNull();
        var (afterAddDest, afterAddMapped) = acc.SelectMappedSubject("foo");
        afterAddMapped.ShouldBeTrue();
        afterAddDest.ShouldBe("bar");

        acc.RemoveMapping("foo").ShouldBeTrue();
        var (afterRemoveDest, afterRemoveMapped) = acc.SelectMappedSubject("foo");
        afterRemoveMapped.ShouldBeFalse();
        afterRemoveDest.ShouldBe("foo");
    }

    [Fact] // T:88
    public void GlobalAccountRouteMappingsConfiguration_ShouldSucceed()
    {
        var acc = Account.NewAccount("global");
        acc.AddMapping("foo", "bar").ShouldBeNull();
        acc.AddWeightedMappings(
            "foo.*",
            MapDest.New("bar.v1.$1", 40),
            MapDest.New("baz.v2.$1", 20)).ShouldBeNull();
        acc.AddMapping("bar.*.*", "RAB.$2.$1").ShouldBeNull();

        var (simpleDest, simpleMapped) = acc.SelectMappedSubject("foo");
        simpleMapped.ShouldBeTrue();
        simpleDest.ShouldBe("bar");

        var (crossDest, crossMapped) = acc.SelectMappedSubject("bar.11.22");
        crossMapped.ShouldBeTrue();
        crossDest.ShouldBe("RAB.22.11");

        var counts = new Dictionary<string, int>(StringComparer.Ordinal);
        for (var i = 0; i < 400; i++)
        {
            var (dest, mapped) = acc.SelectMappedSubject("foo.22");
            mapped.ShouldBeTrue();
            counts.TryGetValue(dest, out var current);
            counts[dest] = current + 1;
        }

        counts.ShouldContainKey("bar.v1.22");
        counts.ShouldContainKey("baz.v2.22");
        counts.ShouldContainKey("foo.22");
    }

    [Fact] // T:90
    public void AccountRouteMappingsWithLossInjection_ShouldSucceed()
    {
        var acc = Account.NewAccount("global");
        acc.AddWeightedMappings("foo", MapDest.New("foo", 80)).ShouldBeNull();
        acc.AddWeightedMappings("bar", MapDest.New("bar", 0)).ShouldBeNull();

        var fooMapped = 0;
        var fooUnmapped = 0;
        for (var i = 0; i < 2000; i++)
        {
            var (_, mapped) = acc.SelectMappedSubject("foo");
            if (mapped) fooMapped++;
            else fooUnmapped++;
        }

        fooMapped.ShouldBeGreaterThan(0);
        fooUnmapped.ShouldBeGreaterThan(0);

        for (var i = 0; i < 200; i++)
        {
            var (dest, mapped) = acc.SelectMappedSubject("bar");
            mapped.ShouldBeFalse();
            dest.ShouldBe("bar");
        }
    }

    [Fact] // T:91
    public void AccountRouteMappingsWithOriginClusterFilter_ShouldSucceed()
    {
        var acc = Account.NewAccount("global");
        acc.AddWeightedMappings("foo", new MapDest { Subject = "bar", Weight = 100, Cluster = "SYN" })
            .ShouldBeNull();

        var (dest, mapped) = acc.SelectMappedSubject("foo");
        mapped.ShouldBeTrue();
        dest.ShouldBe("foo");
    }

    [Fact] // T:92
    public void AccountServiceImportWithRouteMappings_ShouldSucceed()
    {
        var exporter = Account.NewAccount("foo");
        var importer = Account.NewAccount("bar");

        exporter.AddMapping("request", "request.v2").ShouldBeNull();
        importer.Imports.Services = new Dictionary<string, List<ServiceImportEntry>>
        {
            ["request"] = [new ServiceImportEntry { Account = exporter, From = "request", To = "request" }],
        };

        var (mappedSubject, mapped) = exporter.SelectMappedSubject("request");
        mapped.ShouldBeTrue();
        mappedSubject.ShouldBe("request.v2");

        importer.Imports.Services.ShouldContainKey("request");
        importer.Imports.Services["request"].Count.ShouldBe(1);
        importer.Imports.Services["request"][0].To.ShouldBe("request");
    }

    [Fact] // T:93
    public void AccountImportsWithWildcardSupport_ShouldSucceed()
    {
        var acc = Account.NewAccount("bar");
        acc.AddMapping("request.*", "my.request.$1").ShouldBeNull();
        acc.AddMapping("events.*", "foo.events.$1").ShouldBeNull();
        acc.AddMapping("info.*.*.>", "foo.info.$2.$1.>").ShouldBeNull();

        acc.SelectMappedSubject("request.22").ShouldBe(("my.request.22", true));
        acc.SelectMappedSubject("events.22").ShouldBe(("foo.events.22", true));
        acc.SelectMappedSubject("info.11.22.bar").ShouldBe(("foo.info.22.11.bar", true));
    }

    [Fact] // T:94
    public void AccountImportsWithWildcardSupportStreamAndService_ShouldSucceed()
    {
        var source = Account.NewAccount("foo");
        var target = Account.NewAccount("bar");

        target.Imports.Services = new Dictionary<string, List<ServiceImportEntry>>
        {
            ["request.*"] = [new ServiceImportEntry
            {
                Account = source,
                From = "request.*",
                To = "my.request.$1",
                Transform = RequireTransform("request.*", "my.request.$1"),
            }],
        };
        target.Imports.Streams =
        [
            new StreamImportEntry
            {
                Account = source,
                From = "events.*",
                To = "foo.events.$1",
                Transform = RequireTransform("events.*", "foo.events.$1"),
            },
        ];

        target.Imports.Services["request.*"].Single().Transform!.TransformSubject("request.22")
            .ShouldBe("my.request.22");
        target.Imports.Streams.Single().Transform!.TransformSubject("events.22")
            .ShouldBe("foo.events.22");
    }

    [Fact] // T:97
    public void AccountSystemPermsWithGlobalAccess_ShouldSucceed()
    {
        var global = Account.NewAccount("$G");
        var system = Account.NewAccount("$SYS");
        system.Exports.Services = new Dictionary<string, ServiceExportEntry>
        {
            ["$SYS.REQ.>"] = new ServiceExportEntry { Account = system },
        };

        global.IsExportService("$SYS.REQ.INFO").ShouldBeFalse();
        system.IsExportService("$SYS.REQ.INFO").ShouldBeTrue();

        system.CheckServiceExportApproved(global, "$SYS.REQ.INFO", null).ShouldBeTrue();
    }

    [Fact] // T:81
    public void MultipleStreamImportsWithSameSubjectDifferentPrefix_ShouldSucceed()
    {
        var fooAccount = Account.NewAccount("foo");
        var barAccount = Account.NewAccount("bar");
        var importAccount = Account.NewAccount("import");

        fooAccount.AddStreamExport("test", null).ShouldBeNull();
        barAccount.AddStreamExport("test", null).ShouldBeNull();

        importAccount.AddStreamImport(fooAccount, "test", "foo").ShouldBeNull();
        importAccount.AddStreamImport(barAccount, "test", "bar").ShouldBeNull();

        importAccount.Imports.Streams.ShouldNotBeNull();
        importAccount.Imports.Streams!.Count.ShouldBe(2);
        importAccount.Imports.Streams.Select(si => si.To).OrderBy(static t => t).ToArray()
            .ShouldBe(["bar.test", "foo.test"]);
    }

    [Fact] // T:82
    public void MultipleStreamImportsWithSameSubject_ShouldSucceed()
    {
        var fooAccount = Account.NewAccount("foo");
        var barAccount = Account.NewAccount("bar");
        var importAccount = Account.NewAccount("import");

        fooAccount.AddStreamExport("test", null).ShouldBeNull();
        barAccount.AddStreamExport("test", null).ShouldBeNull();

        importAccount.AddStreamImport(fooAccount, "test", string.Empty).ShouldBeNull();
        importAccount.AddStreamImport(fooAccount, "test", string.Empty).ShouldBe(ServerErrors.ErrStreamImportDuplicate);
        importAccount.AddStreamImport(barAccount, "test", string.Empty).ShouldBeNull();

        importAccount.Imports.Streams.ShouldNotBeNull();
        importAccount.Imports.Streams!.Count.ShouldBe(2);
        importAccount.Imports.Streams.Select(si => si.Account?.Name).OrderBy(static n => n).ToArray()
            .ShouldBe(["bar", "foo"]);
    }

    [Fact] // T:95
    public void BenchmarkNewRouteReply()
    {
        var globalAccount = Account.NewAccount("$G");

        var first = globalAccount.NewServiceReply(tracking: false);
        var second = globalAccount.NewServiceReply(tracking: false);
        var tracked = globalAccount.NewServiceReply(tracking: true);

        first.Length.ShouldBeGreaterThan(20);
        second.Length.ShouldBeGreaterThan(20);
        first.SequenceEqual(second).ShouldBeFalse();

        var trackedText = Encoding.ASCII.GetString(tracked);
        trackedText.EndsWith(".T", StringComparison.Ordinal).ShouldBeTrue();
    }

    [Fact] // T:98
    public void ImportSubscriptionPartialOverlapWithPrefix_ShouldSucceed()
    {
        var transform = RequireTransform(">", "myprefix.>");
        var mapped = transform.TransformSubject("test");
        mapped.ShouldBe("myprefix.test");

        foreach (var filter in new[] { ">", "myprefix.*", "myprefix.>", "myprefix.test", "*.>", "*.*", "*.test" })
            SubscriptionIndex.SubjectIsSubsetMatch(mapped, filter).ShouldBeTrue();
    }

    [Fact] // T:99
    public void ImportSubscriptionPartialOverlapWithTransform_ShouldSucceed()
    {
        var transform = RequireTransform("*.*.>", "myprefix.$2.$1.>");
        var mapped = transform.TransformSubject("1.2.test");
        mapped.ShouldBe("myprefix.2.1.test");

        foreach (var filter in new[]
        {
            ">", "*.*.*.>", "*.2.*.>", "*.*.1.>", "*.2.1.>", "*.*.*.*", "*.2.1.*", "*.*.*.test",
            "*.*.1.test", "*.2.*.test", "*.2.1.test", "myprefix.*.*.*", "myprefix.>", "myprefix.*.>",
            "myprefix.*.*.>", "myprefix.2.>", "myprefix.2.1.>", "myprefix.*.1.>", "myprefix.2.*.>",
            "myprefix.2.1.*", "myprefix.*.*.test", "myprefix.2.1.test",
        })
        {
            SubscriptionIndex.SubjectIsSubsetMatch(mapped, filter).ShouldBeTrue();
        }
    }

    [Fact] // T:104
    public void AccountUserSubPermsWithQueueGroups_ShouldSucceed()
    {
        var c = new ClientConnection(ClientKind.Client);
        c.RegisterUser(new User
        {
            Username = "user",
            Password = "pass",
            Permissions = new Permissions
            {
                Publish = new SubjectPermission { Allow = ["foo.restricted"] },
                Subscribe = new SubjectPermission
                {
                    Allow = ["foo.>"],
                    Deny = ["foo.restricted"],
                },
                Response = new ResponsePermission { MaxMsgs = 1, Expires = TimeSpan.Zero },
            },
        });

        c.Perms.ShouldNotBeNull();
        c.Perms!.Sub.Allow.ShouldNotBeNull();
        c.Perms.Sub.Deny.ShouldNotBeNull();

        c.Perms.Sub.Allow!.Match("foo.restricted").PSubs.Count.ShouldBeGreaterThan(0);
        c.Perms.Sub.Deny!.Match("foo.restricted").PSubs.Count.ShouldBeGreaterThan(0);

        var (_, queue) = ClientConnection.SplitSubjectQueue("foo.> qg");
        queue.ShouldNotBeNull();
        Encoding.ASCII.GetString(queue!).ShouldBe("qg");
    }

    [Fact] // T:106
    public void AccountImportOwnExport_ShouldSucceed()
    {
        var a = Account.NewAccount("A");
        a.Exports.Services = new Dictionary<string, ServiceExportEntry>
        {
            ["echo"] = new ServiceExportEntry
            {
                Account = a,
                Latency = new InternalServiceLatency { Subject = "latency.echo", Sampling = 100 },
            },
        };
        a.Imports.Services = new Dictionary<string, List<ServiceImportEntry>>
        {
            ["echo"] = [new ServiceImportEntry { Account = a, From = "echo", To = "echo" }],
        };

        a.IsExportService("echo").ShouldBeTrue();
        a.CheckServiceExportApproved(a, "echo", null).ShouldBeTrue();
        a.Imports.Services["echo"].Count.ShouldBe(1);
    }

    [Fact] // T:107
    public void AccountImportDuplicateResponseDeliveryWithLeafnodes_ShouldSucceed()
    {
        var exporter = Account.NewAccount("A");
        var importer = Account.NewAccount("B");

        exporter.Exports.Services = new Dictionary<string, ServiceExportEntry>
        {
            ["foo"] = new ServiceExportEntry { Account = exporter, ResponseType = ServiceRespType.Streamed },
        };
        importer.Imports.Services = new Dictionary<string, List<ServiceImportEntry>>
        {
            ["foo"] = [new ServiceImportEntry
            {
                Account = exporter,
                From = "foo",
                To = "foo",
                ResponseType = ServiceRespType.Streamed,
            }],
        };

        importer.Imports.Services["foo"].Count.ShouldBe(1);
        importer.Imports.Services["foo"][0].DidDeliver.ShouldBeFalse();
        exporter.CheckServiceExportApproved(importer, "foo", null).ShouldBeTrue();
    }

    [Fact] // T:109
    public void AccountServiceAndStreamExportDoubleDelivery_ShouldSucceed()
    {
        var tenant = Account.NewAccount("tenant1");
        tenant.Exports.Streams = new Dictionary<string, StreamExport>
        {
            ["DW.>"] = new StreamExport(),
        };
        tenant.Exports.Services = new Dictionary<string, ServiceExportEntry>
        {
            ["DW.>"] = new ServiceExportEntry { Account = tenant },
        };

        tenant.CheckStreamExportApproved(tenant, "DW.test.123", null).ShouldBeTrue();
        tenant.CheckServiceExportApproved(tenant, "DW.test.123", null).ShouldBeTrue();
        tenant.IsExportService("DW.test.123").ShouldBeTrue();
    }

    [Fact] // T:110
    public void AccountServiceImportNoResponders_ShouldSucceed()
    {
        var exporter = Account.NewAccount("accExp");
        var importer = Account.NewAccount("accImp");

        importer.Imports.Services = new Dictionary<string, List<ServiceImportEntry>>
        {
            ["foo"] = [new ServiceImportEntry { Account = exporter, From = "foo", To = "foo" }],
        };

        importer.Imports.Services["foo"].Count.ShouldBe(1);
        exporter.CheckServiceExportApproved(importer, "foo", null).ShouldBeFalse();
    }

    [Fact] // T:100
    public void AccountLimitsServerConfig_ShouldSucceed()
    {
        var acc = Account.NewAccount("A");
        acc.MaxConnections = 1;

        var c1 = new ClientConnection(ClientKind.Client) { Cid = 1001 };
        c1.RegisterWithAccount(acc);

        Should.Throw<TooManyAccountConnectionsException>(() =>
            new ClientConnection(ClientKind.Client) { Cid = 1002 }.RegisterWithAccount(acc));
    }

    [Fact] // T:101
    public void AccountMaxConnectionsDisconnectsNewestFirst_ShouldSucceed()
    {
        var acc = Account.NewAccount("A");
        acc.MaxConnections = 2;

        var c1 = new ClientConnection(ClientKind.Client) { Cid = 1011 };
        var c2 = new ClientConnection(ClientKind.Client) { Cid = 1012 };
        c1.RegisterWithAccount(acc);
        c2.RegisterWithAccount(acc);

        var toDisconnect = acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-101", Name = "srv-101" },
            Account = "A",
            Conns = 1,
        });

        toDisconnect.Count.ShouldBe(1);
        toDisconnect[0].Cid.ShouldBe(1011ul);
    }

    [Fact] // T:102
    public void AccountUpdateRemoteServerDisconnectsNewestFirst_ShouldSucceed()
    {
        var acc = Account.NewAccount("A");
        acc.MaxConnections = 2;

        new ClientConnection(ClientKind.Client) { Cid = 1021 }.RegisterWithAccount(acc);
        new ClientConnection(ClientKind.Client) { Cid = 1022 }.RegisterWithAccount(acc);

        var first = acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-102", Name = "srv-102" },
            Account = "A",
            Conns = 1,
        });
        first.Count.ShouldBe(1);
        first[0].Cid.ShouldBe(1021ul);

        var second = acc.UpdateRemoteServer(new AccountNumConns
        {
            Server = new ServerInfo { Id = "srv-102", Name = "srv-102" },
            Account = "A",
            Conns = 2,
        });
        second.Count.ShouldBe(2);
    }

    private static SubjectTransform RequireTransform(string src, string dest)
    {
        var (transform, err) = SubjectTransform.New(src, dest);
        err.ShouldBeNull();
        transform.ShouldNotBeNull();
        return transform!;
    }
}
