// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Config;

namespace ZB.MOM.NatsNet.Server.Tests;

/// <summary>
/// Tests for ServerOptions — mirrors tests from server/opts_test.go.
/// </summary>
public class ServerOptionsTests
{
    /// <summary>
    /// Mirrors TestDefaultOptions — verifies baseline defaults are applied.
    /// </summary>
    [Fact] // T:2512
    public void DefaultOptions_ShouldSetBaselineDefaults()
    {
        var opts = new ServerOptions();
        opts.SetBaselineOptions();

        opts.Host.ShouldBe(ServerConstants.DefaultHost);
        opts.Port.ShouldBe(ServerConstants.DefaultPort);
        opts.MaxConn.ShouldBe(ServerConstants.DefaultMaxConnections);
        opts.HttpHost.ShouldBe(ServerConstants.DefaultHost);
        opts.PingInterval.ShouldBe(ServerConstants.DefaultPingInterval);
        opts.MaxPingsOut.ShouldBe(ServerConstants.DefaultPingMaxOut);
        opts.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds);
        opts.AuthTimeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds);
        opts.MaxControlLine.ShouldBe(ServerConstants.MaxControlLineSize);
        opts.MaxPayload.ShouldBe(ServerConstants.MaxPayloadSize);
        opts.MaxPending.ShouldBe(ServerConstants.MaxPendingSize);
        opts.WriteDeadline.ShouldBe(ServerConstants.DefaultFlushDeadline);
        opts.MaxClosedClients.ShouldBe(ServerConstants.DefaultMaxClosedClients);
        opts.LameDuckDuration.ShouldBe(ServerConstants.DefaultLameDuckDuration);
        opts.LameDuckGracePeriod.ShouldBe(ServerConstants.DefaultLameDuckGracePeriod);
        opts.LeafNode.ReconnectInterval.ShouldBe(ServerConstants.DefaultLeafNodeReconnect);
        opts.ConnectErrorReports.ShouldBe(ServerConstants.DefaultConnectErrorReports);
        opts.ReconnectErrorReports.ShouldBe(ServerConstants.DefaultReconnectErrorReports);
        opts.JetStreamMaxMemory.ShouldBe(-1);
        opts.JetStreamMaxStore.ShouldBe(-1);
        opts.SyncInterval.ShouldBe(TimeSpan.FromMinutes(2));
        opts.JetStreamRequestQueueLimit.ShouldBe(4096);
    }

    /// <summary>
    /// Mirrors TestOptions_RandomPort — RANDOM_PORT should become 0 after baseline.
    /// </summary>
    [Fact] // T:2513
    public void RandomPort_ShouldResolveToZero()
    {
        var opts = new ServerOptions { Port = ServerConstants.RandomPort };
        opts.SetBaselineOptions();
        opts.Port.ShouldBe(0);
    }

    /// <summary>
    /// Mirrors TestMergeOverrides — flag options override file options.
    /// </summary>
    [Fact] // T:2516
    public void MergeOverrides_ShouldLetFlagsWin()
    {
        var fileOpts = new ServerOptions
        {
            Host = "0.0.0.0",
            Port = 4222,
            Username = "fileuser",
            Password = "filepass",
        };

        var flagOpts = new ServerOptions
        {
            Port = 9999,
            Username = "flaguser",
        };

        var merged = ServerOptions.MergeOptions(fileOpts, flagOpts);

        merged.Port.ShouldBe(9999);
        merged.Username.ShouldBe("flaguser");
        // Host not overridden (empty in flags)
        merged.Host.ShouldBe("0.0.0.0");
        // Password not overridden (empty in flags)
        merged.Password.ShouldBe("filepass");
    }

    /// <summary>
    /// Mirrors TestOptionsClone — deep copy should produce independent objects.
    /// </summary>
    [Fact] // T:2537
    public void Clone_ShouldDeepCopyAllFields()
    {
        var opts = new ServerOptions
        {
            ConfigFile = "./configs/test.conf",
            Host = "127.0.0.1",
            Port = 2222,
            Username = "derek",
            Password = "porkchop",
            AuthTimeout = 1.0,
            Debug = true,
            Trace = true,
            Logtime = false,
            HttpPort = ServerConstants.DefaultHttpPort,
            HttpBasePath = ServerConstants.DefaultHttpBasePath,
            PidFile = "/tmp/nats-server/nats-server.pid",
            ProfPort = 6789,
            Syslog = true,
            RemoteSyslog = "udp://foo.com:33",
            MaxControlLine = 2048,
            MaxPayload = 65536,
            MaxConn = 100,
            PingInterval = TimeSpan.FromSeconds(60),
            MaxPingsOut = 3,
            Cluster = new ClusterOpts
            {
                NoAdvertise = true,
                ConnectRetries = 2,
                WriteDeadline = TimeSpan.FromSeconds(3),
            },
            Gateway = new GatewayOpts
            {
                Name = "A",
                Gateways =
                [
                    new RemoteGatewayOpts { Name = "B", Urls = [new Uri("nats://host:5222")] },
                    new RemoteGatewayOpts { Name = "C" },
                ],
            },
            WriteDeadline = TimeSpan.FromSeconds(3),
            Routes = [new Uri("nats://localhost:4222")],
        };

        var clone = opts.Clone();

        // Values should match.
        clone.ConfigFile.ShouldBe(opts.ConfigFile);
        clone.Host.ShouldBe(opts.Host);
        clone.Port.ShouldBe(opts.Port);
        clone.Username.ShouldBe(opts.Username);
        clone.Gateway.Name.ShouldBe("A");
        clone.Gateway.Gateways.Count.ShouldBe(2);
        clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222");

        // Mutating clone should not affect original.
        clone.Username = "changed";
        opts.Username.ShouldBe("derek");

        // Mutating original gateway URLs should not affect clone.
        opts.Gateway.Gateways[0].Urls[0] = new Uri("nats://other:9999");
        clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222");
    }

    /// <summary>
    /// Mirrors TestOptionsCloneNilLists — clone of empty lists should produce empty lists.
    /// </summary>
    [Fact] // T:2538
    public void CloneNilLists_ShouldProduceEmptyLists()
    {
        var opts = new ServerOptions();
        var clone = opts.Clone();
        clone.Routes.ShouldNotBeNull();
    }

    /// <summary>
    /// Mirrors TestExpandPath — tilde and env var expansion.
    /// </summary>
    [Fact] // T:2563
    public void ExpandPath_ShouldExpandTildeAndEnvVars()
    {
        // Absolute path should not change.
        var result = ServerOptions.ExpandPath("/foo/bar");
        result.ShouldBe("/foo/bar");

        // Tilde expansion.
        var home = ServerOptions.HomeDir();
        var expanded = ServerOptions.ExpandPath("~/test");
        expanded.ShouldBe(Path.Combine(home, "test"));
    }

    /// <summary>
    /// Mirrors TestDefaultAuthTimeout — verifies auth timeout logic.
    /// </summary>
    [Fact] // T:2572
    public void DefaultAuthTimeout_ShouldBe2sWithoutTls()
    {
        var timeout = ServerOptions.GetDefaultAuthTimeout(null, 0);
        timeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds);
    }

    [Fact]
    public void DefaultAuthTimeout_ShouldBeTlsTimeoutPlusOneWithTls()
    {
        // When TLS is configured, auth timeout = tls_timeout + 1
        var timeout = ServerOptions.GetDefaultAuthTimeout(new object(), 4.0);
        timeout.ShouldBe(5.0);
    }

    /// <summary>
    /// Tests RoutesFromStr parsing.
    /// </summary>
    [Fact] // T:2576 (partial)
    public void RoutesFromStr_ShouldParseCommaSeparatedUrls()
    {
        var routes = ServerOptions.RoutesFromStr("nats://localhost:4222,nats://localhost:4223");
        routes.Count.ShouldBe(2);
        routes[0].Authority.ShouldBe("localhost:4222");
        routes[1].Authority.ShouldBe("localhost:4223");
    }

    /// <summary>
    /// Tests NormalizeBasePath.
    /// </summary>
    [Fact] // T:2581
    public void NormalizeBasePath_ShouldCleanPaths()
    {
        ServerOptions.NormalizeBasePath("").ShouldBe("/");
        ServerOptions.NormalizeBasePath("/").ShouldBe("/");
        ServerOptions.NormalizeBasePath("foo").ShouldBe("/foo");
        ServerOptions.NormalizeBasePath("/foo/").ShouldBe("/foo");
        ServerOptions.NormalizeBasePath("//foo//bar//").ShouldBe("/foo/bar");
    }

    /// <summary>
    /// Tests ConfigFlags.NoErrOnUnknownFields.
    /// </summary>
    [Fact] // T:2502
    public void NoErrOnUnknownFields_ShouldToggleFlag()
    {
        ConfigFlags.NoErrOnUnknownFields(true);
        ConfigFlags.AllowUnknownTopLevelField.ShouldBeTrue();

        ConfigFlags.NoErrOnUnknownFields(false);
        ConfigFlags.AllowUnknownTopLevelField.ShouldBeFalse();
    }

    /// <summary>
    /// Tests SetBaseline with cluster port set.
    /// </summary>
    [Fact]
    public void SetBaseline_WithClusterPort_ShouldApplyClusterDefaults()
    {
        var opts = new ServerOptions
        {
            Cluster = new ClusterOpts { Port = 6222 },
        };
        opts.SetBaselineOptions();

        opts.Cluster.Host.ShouldBe(ServerConstants.DefaultHost);
        opts.Cluster.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds);
        opts.Cluster.PoolSize.ShouldBe(ServerConstants.DefaultRoutePoolSize);
        opts.Cluster.Compression.Mode.ShouldBe(CompressionModes.Accept);
        // System account should be auto-pinned.
        opts.Cluster.PinnedAccounts.ShouldContain(ServerConstants.DefaultSystemAccount);
    }

    /// <summary>
    /// Tests SetBaseline with leaf node port set.
    /// </summary>
    [Fact]
    public void SetBaseline_WithLeafNodePort_ShouldApplyLeafDefaults()
    {
        var opts = new ServerOptions
        {
            LeafNode = new LeafNodeOpts { Port = 7422 },
        };
        opts.SetBaselineOptions();

        opts.LeafNode.Host.ShouldBe(ServerConstants.DefaultHost);
        opts.LeafNode.Compression.Mode.ShouldBe(CompressionModes.S2Auto);
    }

    /// <summary>
    /// Tests OverrideCluster with valid URL.
    /// </summary>
    [Fact] // T:2518
    public void OverrideCluster_ShouldParseUrlAndSetHostPort()
    {
        var opts = new ServerOptions
        {
            Cluster = new ClusterOpts { ListenStr = "nats://user:pass@127.0.0.1:6222" },
        };
        var err = opts.OverrideCluster();
        err.ShouldBeNull();
        opts.Cluster.Host.ShouldBe("127.0.0.1");
        opts.Cluster.Port.ShouldBe(6222);
        opts.Cluster.Username.ShouldBe("user");
        opts.Cluster.Password.ShouldBe("pass");
    }

    /// <summary>
    /// Tests OverrideCluster with empty string disables clustering.
    /// </summary>
    [Fact]
    public void OverrideCluster_EmptyString_ShouldDisableClustering()
    {
        var opts = new ServerOptions
        {
            Cluster = new ClusterOpts { Port = 6222, ListenStr = "" },
        };
        var err = opts.OverrideCluster();
        err.ShouldBeNull();
        opts.Cluster.Port.ShouldBe(0);
    }

    /// <summary>
    /// Tests MaybeReadPidFile returns original string when file doesn't exist.
    /// </summary>
    [Fact] // T:2585
    public void MaybeReadPidFile_NonExistent_ShouldReturnOriginalString()
    {
        ServerOptions.MaybeReadPidFile("12345").ShouldBe("12345");
    }

    /// <summary>
    /// Tests that MergeOptions handles null inputs.
    /// </summary>
    [Fact]
    public void MergeOptions_NullInputs_ShouldReturnNonNull()
    {
        var result = ServerOptions.MergeOptions(null, null);
        result.ShouldNotBeNull();

        var fileOpts = new ServerOptions { Port = 1234 };
        var r1 = ServerOptions.MergeOptions(fileOpts, null);
        r1.Port.ShouldBe(1234);

        var flagOpts = new ServerOptions { Port = 5678 };
        var r2 = ServerOptions.MergeOptions(null, flagOpts);
        r2.Port.ShouldBe(5678);
    }

    /// <summary>
    /// Mirrors TestListenMonitoringDefault — when Host is set without HTTPHost,
    /// SetBaselineOptions should copy Host to HTTPHost.
    /// </summary>
    [Fact] // T:2524
    public void ListenMonitoringDefault_ShouldSetHttpHostToHost()
    {
        var opts = new ServerOptions { Host = "10.0.1.22" };
        opts.SetBaselineOptions();

        opts.Host.ShouldBe("10.0.1.22");
        opts.HttpHost.ShouldBe("10.0.1.22");
        opts.Port.ShouldBe(ServerConstants.DefaultPort);
    }

    /// <summary>
    /// Mirrors TestGetStorageSize — StorageSizeJsonConverter.Parse converts K/M/G/T suffixes
    /// and returns 0 for empty input; invalid suffixes throw.
    /// </summary>
    [Fact] // T:2576
    public void GetStorageSize_ShouldParseSuffixes()
    {
        StorageSizeJsonConverter.Parse("1K").ShouldBe(1024L);
        StorageSizeJsonConverter.Parse("1M").ShouldBe(1048576L);
        StorageSizeJsonConverter.Parse("1G").ShouldBe(1073741824L);
        StorageSizeJsonConverter.Parse("1T").ShouldBe(1099511627776L);
        StorageSizeJsonConverter.Parse("").ShouldBe(0L);

        Should.Throw<FormatException>(() => StorageSizeJsonConverter.Parse("1L"));
        Should.Throw<FormatException>(() => StorageSizeJsonConverter.Parse("TT"));
    }

    /// <summary>
    /// Mirrors TestClusterNameAndGatewayNameConflict — when Cluster.Name != Gateway.Name,
    /// ValidateOptions should return ErrClusterNameConfigConflict.
    /// </summary>
    [Fact] // T:2571
    public void ClusterNameAndGatewayNameConflict_ShouldReturnConflictError()
    {
        var opts = new ServerOptions
        {
            Cluster = new ClusterOpts { Name = "A", Port = -1 },
            Gateway = new GatewayOpts { Name = "B", Port = -1 },
        };

        var err = NatsServer.ValidateOptions(opts);
        err.ShouldNotBeNull();
        err.ShouldBe(ServerErrors.ErrClusterNameConfigConflict);
    }
}
